points in the ephem.
                if len(time_val) > 1:
                    int_val = (time_val.max() - time_val.min()) / (len(time_val) - 1)

            for idx in range(len(sou_dir)):
                source_table.addrows()
                source_table.putcell("NAME", row_count, sou_name)
                source_table.putcell("SOURCE_ID", row_count, sou_id)
                source_table.putcell("INTERVAL", row_count, int_val)
                source_table.putcell("SPECTRAL_WINDOW_ID", row_count, -1)
                source_table.putcell("NUM_LINES", row_count, 0)
                source_table.putcell("TIME", row_count, time_val[idx])
                source_table.putcell("DIRECTION", row_count, sou_dir[idx])
                source_table.putcell("PROPER_MOTION", row_count, pm_dir)
                row_count += 1

        # We have one final thing we need to do here, which is to link the SOURCE table
        # to the main table, since it's not included in the list of default subtables.
        # You are _supposed_ to be able to provide a string, but it looks like that
        # functionality is actually broken in CASA. Fortunately, supplying the whole
        # table does seem to work (as least w/ casscore v3.3.1).
        ms = tables.table(filepath, readonly=False, ack=False)
        ms.putkeyword("SOURCE", source_table)
        ms.done()

        source_table.done()

    def _write_ms_pointing(self, filepath):
        """
        Write out the pointing information into a CASA table.

        Parameters
        ----------
        filepath : str
            path to MS (without POINTING suffix)

        """
        if not casa_present:  # pragma: no cover
            raise ImportError(no_casa_message) from casa_error

        pointing_table = tables.table(
            filepath + "::POINTING", ack=False, readonly=False
        )
        nants = np.max(self.antenna_numbers) + 1
        antennas = np.arange(nants, dtype=np.int32)

        # We want the number of unique timestamps here, since CASA wants a
        # per-timestamp, per-antenna entry
        times = np.asarray(
            [
                Time(t, format="jd", scale="utc").mjd * 86400.0
                for t in np.unique(self.time_array)
            ]
        )

        # Same for the number of intervals
        intervals = np.array(
            [
                np.median(self.integration_time[self.time_array == timestamp])
                for timestamp in np.unique(self.time_array)
            ]
        )

        ntimes = len(times)

        antennas_full = np.tile(antennas, ntimes)
        times_full = np.repeat(times, nants)
        intervals_full = np.repeat(intervals, nants)

        nrows = len(antennas_full)
        assert len(times_full) == nrows
        assert len(intervals_full) == nrows

        # This extra step of adding a single row and plugging in values for DIRECTION
        # and TARGET are a workaround for a weird issue that pops up where, because the
        # values are not a fixed size (they are shape(2, Npoly), where Npoly is allowed
        # to vary from integration to integration), casacore seems to be very slow
        # filling in the data after the fact. By "pre-filling" the first row, the
        # addrows operation will automatically fill in an appropriately shaped array.
        # TODO: This works okay for steerable dishes, but less well for non-tracking
        # arrays (i.e., where primary beam center != phase center). Fix later.

        pointing_table.addrows(1)
        pointing_table.putcell("DIRECTION", 0, np.zeros((2, 1), dtype=np.float64))
        pointing_table.putcell("TARGET", 0, np.zeros((2, 1), dtype=np.float64))
        pointing_table.addrows(nrows - 1)
        pointing_table.done()

        pointing_table = tables.table(
            filepath + "::POINTING", ack=False, readonly=False
        )

        pointing_table.putcol("ANTENNA_ID", antennas_full, nrow=nrows)
        pointing_table.putcol("TIME", times_full, nrow=nrows)
        pointing_table.putcol("INTERVAL", intervals_full, nrow=nrows)

        name_col = np.asarray(["ZENITH"] * nrows, dtype=np.bytes_)
        pointing_table.putcol("NAME", name_col, nrow=nrows)

        num_poly_col = np.zeros(nrows, dtype=np.int32)
        pointing_table.putcol("NUM_POLY", num_poly_col, nrow=nrows)

        time_origin_col = times_full
        pointing_table.putcol("TIME_ORIGIN", time_origin_col, nrow=nrows)

        # we always "point" at zenith
        direction_col = np.zeros((nrows, 2, 1), dtype=np.float64)
        direction_col[:, 1, :] = np.pi / 2
        pointing_table.putcol("DIRECTION", direction_col, nrow=nrows)

        # just reuse "direction" for "target"
        pointing_table.putcol("TARGET", direction_col, nrow=nrows)

        # set tracking info to `False`
        tracking_col = np.zeros(nrows, dtype=np.bool_)
        pointing_table.putcol("TRACKING", tracking_col, nrow=nrows)

        pointing_table.done()

    def _write_ms_spectralwindow(self, filepath):
        """
        Write out the spectral information into a CASA table.

        Parameters
        ----------
        filepath : str
            path to MS (without SPECTRAL_WINDOW suffix)

        """
        if not casa_present:  # pragma: no cover
            raise ImportError(no_casa_message) from casa_error

        sw_table = tables.table(
            filepath + "::SPECTRAL_WINDOW", ack=False, readonly=False
        )

        if self.future_array_shapes:
            freq_array = self.freq_array
            ch_width = self.channel_width
        else:
            freq_array = self.freq_array[0]
            ch_width = np.zeros_like(freq_array) + self.channel_width

        spwidcoldesc = tables.makearrcoldesc(
            "ASSOC_SPW_ID",
            0,
            valuetype="int",
            ndim=-1,
            datamanagertype="StandardStMan",
            datamanagergroup="SpW optional column Standard Manager",
            comment="Associated spectral window id",
        )
        del spwidcoldesc["desc"]["shape"]
        sw_table.addcols(spwidcoldesc)

        spwassoccoldesc = tables.makearrcoldesc(
            "ASSOC_NATURE",
            "",
            valuetype="string",
            ndim=-1,
            datamanagertype="StandardStMan",
            datamanagergroup="SpW optional column Standard Manager",
            comment="Nature of association with other spectral window",
        )
        sw_table.addcols(spwassoccoldesc)

        for idx, spw_id in enumerate(self.spw_array):
            if self.flex_spw:
                ch_mask = self.flex_spw_id_array == spw_id
            else:
                ch_mask = np.ones(freq_array.shape, dtype=bool)
            sw_table.addrows()
            sw_table.putcell("NUM_CHAN", idx, np.sum(ch_mask))
            sw_table.putcell("NAME", idx, "SPW%d" % spw_id)
            sw_table.putcell("ASSOC_SPW_ID", idx, spw_id)
            sw_table.putcell("ASSOC_NATURE", idx, "")  # Blank for now
            sw_table.putcell("CHAN_FREQ", idx, freq_array[ch_mask])
            sw_table.putcell("CHAN_WIDTH", idx, ch_width[ch_mask])
            sw_table.putcell("EFFECTIVE_BW", idx, ch_width[ch_mask])
            sw_table.putcell("TOTAL_BANDWIDTH", idx, np.sum(ch_width[ch_mask]))
            sw_table.putcell("RESOLUTION", idx, ch_width[ch_mask])
            # TODO: These are placeholders for now, but should be replaced with
            # actual frequency reference info (once UVData handles that)
            sw_table.putcell("MEAS_FREQ_REF", idx, VEL_DICT["TOPO"])
            sw_table.putcell("REF_FREQUENCY", idx, freq_array[0])

        sw_table.done()

    def _write_ms_observation(self, filepath):
        """
        Write out the observation information into a CASA table.

        Parameters
        ----------
        filepath : str
            path to MS (without OBSERVATION suffix)

        """
        if not casa_present:  # pragma: no cover
            raise ImportError(no_casa_message) from casa_error

        observation_table = tables.table(
            filepath + "::OBSERVATION", ack=False, readonly=False
        )
        observation_table.addrows()
        observation_table.putcell("TELESCOPE_NAME", 0, self.telescope_name)

        # It appears that measurement sets do not have a concept of a telescope location
        # We add it here as a non-standard column in order to round trip it properly
        name_col_desc = tableutil.makearrcoldesc(
            "TELESCOPE_LOCATION",
            self.telescope_location[0],
            shape=[3],
            valuetype="double",
        )
        observation_table.addcols(name_col_desc)
        observation_table.putcell("TELESCOPE_LOCATION", 0, self.telescope_location)

        extra_upper = [key.upper() for key in self.extra_keywords.keys()]
        if "OBSERVER" in extra_upper:
            key_ind = extra_upper.index("OBSERVER")
            key = list(self.extra_keywords.keys())[key_ind]
            observation_table.putcell("OBSERVER", 0, self.extra_keywords[key])
        else:
            observation_table.putcell("OBSERVER", 0, self.telescope_name)

        observation_table.done()

    def _write_ms_polarization(self, filepath):
        """
        Write out the polarization information into a CASA table.

        Parameters
        ----------
        filepath : str
            path to MS (without POLARIZATION suffix)

        """
        if not casa_present:  # pragma: no cover
            raise ImportError(no_casa_message) from casa_error

        pol_table = tables.table(filepath + "::POLARIZATION", ack=False, readonly=False)

        if self.flex_spw_polarization_array is None:
            pol_str = uvutils.polnum2str(self.polarization_array)
            feed_pols = {
                feed for pol in pol_str for feed in uvutils.POL_TO_FEED_DICT[pol]
            }
            pol_types = [pol.lower() for pol in sorted(feed_pols)]
            pol_tuples = np.asarray(
                [(pol_types.index(i), pol_types.index(j)) for i, j in pol_str],
                dtype=np.int32,
            )

            pol_table.addrows()
            pol_table.putcell(
                "CORR_TYPE",
                0,
                np.array(
                    [POL_AIPS2CASA_DICT[aipspol] for aipspol in self.polarization_array]
                ),
            )
            pol_table.putcell("CORR_PRODUCT", 0, pol_tuples)
            pol_table.putcell("NUM_CORR", 0, self.Npols)
        else:
            for idx, spw_pol in enumerate(np.unique(self.flex_spw_polarization_array)):
                pol_str = uvutils.polnum2str([spw_pol])
                feed_pols = {
                    feed for pol in pol_str for feed in uvutils.POL_TO_FEED_DICT[pol]
                }
                pol_types = [pol.lower() for pol in sorted(feed_pols)]
                pol_tuples = np.asarray(
                    [(pol_types.index(i), pol_types.index(j)) for i, j in pol_str],
                    dtype=np.int32,
                )

                pol_table.addrows()
                pol_table.putcell(
                    "CORR_TYPE", idx, np.array([POL_AIPS2CASA_DICT[spw_pol]])
                )
                pol_table.putcell("CORR_PRODUCT", idx, pol_tuples)
                pol_table.putcell("NUM_CORR", idx, self.Npols)

        pol_table.done()

    def _ms_hist_to_string(self, history_table):
        """
        Convert a CASA history table into a string for the uvdata history parameter.

        Also stores messages column as a list for consitency with other uvdata types

        Parameters
        ----------
        history_table : a casa table object
            CASA table with history information.

        Returns
        -------
        str
            string enconding complete casa history table converted with a new
            line denoting rows and a ';' denoting column breaks.
        pyuvdata_written :  bool
            boolean indicating whether or not this MS was written by pyuvdata.

        """
        # string to store all the casa history info
        history_str = ""
        pyuvdata_written = False

        # Do not touch the history table if it has no information
        if history_table.nrows() > 0:
            history_str = "Begin measurement set history\n"

            try:
                app_params = history_table.getcol("APP_PARAMS")["array"]
                history_str += "APP_PARAMS;"
            except RuntimeError:
                app_params = None
            try:
                cli_command = history_table.getcol("CLI_COMMAND")["array"]
                history_str += "CLI_COMMAND;"
            except RuntimeError:
                cli_command = None
            application = history_table.getcol("APPLICATION")
            message = history_table.getcol("MESSAGE")
            obj_id = history_table.getcol("OBJECT_ID")
            obs_id = history_table.getcol("OBSERVATION_ID")
            origin = history_table.getcol("ORIGIN")
            priority = history_table.getcol("PRIORITY")
            times = history_table.getcol("TIME")
            history_str += (
                "APPLICATION;MESSAGE;OBJECT_ID;OBSERVATION_ID;ORIGIN;PRIORITY;TIME\n"
            )
            # Now loop through columns and generate history string
            ntimes = len(times)
            cols = [application, message, obj_id, obs_id, origin, priority, times]
            if cli_command is not None:
                cols.insert(0, cli_command)
            if app_params is not None:
                cols.insert(0, app_params)

            # if this MS was written by pyuvdata, some history that originated in
            # pyuvdata is in the MS history table. We separate that out since it doesn't
            # really belong to the MS history block (and so round tripping works)
            pyuvdata_line_nos = [
                line_no
                for line_no, line in enumerate(application)
                if "pyuvdata" in line
            ]

            for tbrow in range(ntimes):
                # first check to see if this row was put in by pyuvdata.
                # If so, don't mix them in with the MS stuff
                if tbrow in pyuvdata_line_nos:
                    continue

                newline = ";".join([str(col[tbrow]) for col in cols]) + "\n"
                history_str += newline

            history_str += "End measurement set history.\n"

            # make a list of lines that are MS specific (i.e. not pyuvdata lines)
            ms_line_nos = np.arange(ntimes).tolist()
            for count, line_no in enumerate(pyuvdata_line_nos):
                # Subtract off the count, since the line_no will effectively
                # change with each call to pop on the list
                ms_line_nos.pop(line_no - count)

            # Handle the case where there is no history but pyuvdata
            if len(ms_line_nos) == 0:
                ms_line_nos = [-1]

            if len(pyuvdata_line_nos) > 0:
                pyuvdata_written = True
                for line_no in pyuvdata_line_nos:
                    if line_no < min(ms_line_nos):
                        # prepend these lines to the history
                        history_str = message[line_no] + "\n" + history_str
                    else:
                        # append these lines to the history
                        history_str += message[line_no] + "\n"

        return history_str, pyuvdata_written

    def _write_ms_history(self, filepath):
        """
        Parse the history into an MS history table.

        If the history string contains output from `_ms_hist_to_string`, parse that back
        into the MS history table.

        Parameters
        ----------
        filepath : str
            path to MS (without HISTORY suffix)
        history : str
            A history string that may or may not contain output from
            `_ms_hist_to_string`.

        """
        if not casa_present:  # pragma: no cover
            raise ImportError(no_casa_message) from casa_error

        app_params = []
        cli_command = []
        application = []
        message = []
        obj_id = []
        obs_id = []
        origin = []
        priority = []
        times = []
        if "APP_PARAMS;CLI_COMMAND;APPLICATION;MESSAGE" in self.history:
            # this history contains info from an MS history table. Need to parse it.

            ms_header_line_no = None
            ms_end_line_no = None
            pre_ms_history_lines = []
            post_ms_history_lines = []
            for line_no, line in enumerate(self.history.splitlines()):
                if "APP_PARAMS;CLI_COMMAND;APPLICATION;MESSAGE" in line:
                    ms_header_line_no = line_no
                    # we don't need this line anywhere below so continue
                    continue

                if "End measurement set history" in line:
                    ms_end_line_no = line_no
                    # we don't need this line anywhere below so continue
                    continue

                if ms_header_line_no is not None and ms_end_line_no is None:
                    # this is part of the MS history block. Parse it.
                    line_parts = line.split(";")

                    app_params.append(line_parts[0])
                    cli_command.append(line_parts[1])
                    application.append(line_parts[2])
                    message.append(line_parts[3])
                    obj_id.append(int(line_parts[4]))
                    obs_id.append(int(line_parts[5]))
                    origin.append(line_parts[6])
                    priority.append(line_parts[7])
                    times.append(np.float64(line_parts[8]))
                elif ms_header_line_no is None:
                    # this is before the MS block
                    if "Begin measurement set history" not in line:
                        pre_ms_history_lines.append(line)
                else:
                    # this is after the MS block
                    post_ms_history_lines.append(line)

            for line_no, line in enumerate(pre_ms_history_lines):
                app_params.insert(line_no, "")
                cli_command.insert(line_no, "")
                application.insert(line_no, "pyuvdata")
                message.insert(line_no, line)
                obj_id.insert(line_no, 0)
                obs_id.insert(line_no, -1)
                origin.insert(line_no, "pyuvdata")
                priority.insert(line_no, "INFO")
                times.insert(line_no, Time.now().mjd * 3600.0 * 24.0)

            for line in post_ms_history_lines:
                app_params.append("")
                cli_command.append("")
                application.append("pyuvdata")
                message.append(line)
                obj_id.append(0)
                obs_id.append(-1)
                origin.append("pyuvdata")
                priority.append("INFO")
                times.append(Time.now().mjd * 3600.0 * 24.0)

        else:
            # no prior MS history detected in the history. Put all of our history in
            # the message column
            for line in self.history.splitlines():
                app_params.append("")
                cli_command.append("")
                application.append("pyuvdata")
                message.append(line)
                obj_id.append(0)
                obs_id.append(-1)
                origin.append("pyuvdata")
                priority.append("INFO")
                times.append(Time.now().mjd * 3600.0 * 24.0)

        history_table = tables.table(filepath + "::HISTORY", ack=False, readonly=False)

        nrows = len(message)
        history_table.addrows(nrows)

        # the first two lines below break on python-casacore < 3.1.0
        history_table.putcol("APP_PARAMS", np.asarray(app_params)[:, np.newaxis])
        history_table.putcol("CLI_COMMAND", np.asarray(cli_command)[:, np.newaxis])
        history_table.putcol("APPLICATION", application)
        history_table.putcol("MESSAGE", message)
        history_table.putcol("OBJECT_ID", obj_id)
        history_table.putcol("OBSERVATION_ID", obs_id)
        history_table.putcol("ORIGIN", origin)
        history_table.putcol("PRIORITY", priority)
        history_table.putcol("TIME", times)

        history_table.done()

    def write_ms(
        self,
        filepath,
        force_phase=False,
        clobber=False,
        run_check=True,
        check_extra=True,
        run_check_acceptability=True,
        strict_uvw_antpos_check=False,
        check_autos=True,
        fix_autos=False,
    ):
        """
        Write a CASA measurement set (MS).

        Parameters
        ----------
        filepath : str
            The MS file path to write to.
        force_phase : bool
            Option to automatically phase drift scan data to zenith of the first
            timestamp.
        clobber : bool
            Option to overwrite the file if it already exists.
        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.
        """
        if not casa_present:  # pragma: no cover
            raise ImportError(no_casa_message) from casa_error

        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(filepath):
            if clobber:
                print("File exists; clobbering")
            else:
                raise IOError("File exists; skipping")

        # CASA does not have a way to handle "unprojected" data in the way that UVData
        # objects can, so we need to check here whether or not any such data exists
        # (and if need be, fix it).
        # TODO: I thought CASA could handle driftscan data. Are we sure it can't handle
        # unprojected data?
        unprojected_blts = self._check_for_cat_type("unprojected")
        if np.any(unprojected_blts):
            if force_phase:
                print(
                    "The data are unprojected. Phasing to zenith of the first "
                    "timestamp."
                )
                phase_time = Time(self.time_array[0], format="jd")
                self.phase_to_time(phase_time, select_mask=unprojected_blts)
            else:
                raise ValueError(
                    "The data are unprojected. "
                    "Set force_phase to true to phase the data "
                    "to zenith of the first timestamp before "
                    "writing a measurement set file."
                )

        # If scan numbers are not already defined from reading an MS,
        # group integrations (rows) into scan numbers.
        if self.scan_number_array is None:
            self._set_scan_numbers()

        # Initialize a skelton measurement set
        ms = self._init_ms_file(filepath)

        attr_list = ["data_array", "nsample_array", "flag_array"]
        col_list = ["DATA", "WEIGHT_SPECTRUM", "FLAG"]

        # Some tasks in CASA require a band-representative (band-averaged?) value for
        # the weights and noise for all channels in each row in the MAIN table, which
        # we will roughly calculate in temp_weights below.
        temp_weights = np.zeros((self.Nblts * self.Nspws, self.Npols), dtype=float)
        data_desc_array = np.zeros((self.Nblts * self.Nspws))

        # astropys Time has some overheads associated with it, so use unique to run
        # this date conversion as few times as possible. Note that the default for MS
        # is MJD UTC seconds, versus JD UTC days for UVData.
        time_array, time_ind = np.unique(self.time_array, return_inverse=True)
        # TODO: Verify this should actually be UTC, and not some other scale
        time_array = (Time(time_array, format="jd", scale="utc").mjd * 86400.0)[
            time_ind
        ]

        # Add all the rows we need up front, which will allow us to fill the
        # columns all in one shot.
        ms.addrows(self.Nblts * self.Nspws)
        if self.Nspws == 1:
            # If we only have one spectral window, there is nothing we need to worry
            # about ordering, so just write the data-related arrays as is to disk
            for attr, col in zip(attr_list, col_list):
                if self.future_array_shapes:
                    ms.putcol(col, getattr(self, attr))
                else:
                    ms.putcol(col, np.squeeze(getattr(self, attr), axis=1))

            # Band-averaged weights are used for some things in CASA - calculate them
            # here using median nsamples.
            if self.future_array_shapes:
                temp_weights = np.median(self.nsample_array, axis=1)
            else:
                temp_weights = np.squeeze(np.median(self.nsample_array, axis=2), axis=1)

            # Grab pointers for the per-blt record arrays
            ant_1_array = self.ant_1_array
            ant_2_array = self.ant_2_array
            integration_time = self.integration_time
            uvw_array = self.uvw_array
            scan_number_array = self.scan_number_array
        else:
            # If we have _more_ than one spectral window, then we need to handle each
            # window separately, since they can have differing numbers of channels.
            # (n.b., tables.putvarcol can write complex tables like these, but its
            # slower and more memory-intensive than putcol).

            # Since muliple records trace back to a single baseline-time, we use this
            # array to map from arrays that store on a per-record basis to positions
            # within arrays that record metadata on a per-blt basis.
            blt_map_array = np.zeros((self.Nblts * self.Nspws), dtype=int)

            # we will select out individual spectral windows several times, so create
            # these masks once up front before we enter the loop.
            spw_sel_dict = {}
            for spw_id in self.spw_array:
                spw_sel_dict[spw_id] = self.flex_spw_id_array == spw_id

            # Based on some analysis of ALMA/ACA data, various routines in CASA appear
            # to prefer data be grouped together on a "per-scan" basis, then per-spw,
            # and then the more usual selections of per-time, per-ant1, etc.
            last_row = 0
            for scan_num in sorted(np.unique(self.scan_number_array)):
                # Select all data from the scan
                scan_screen = self.scan_number_array == scan_num

                # Get the number of records inside the scan, where 1 record = 1 spw in
                # 1 baseline at 1 time
                Nrecs = np.sum(scan_screen)

                # Record which SPW/"Data Description" this data is matched to
                data_desc_array[last_row : last_row + (Nrecs * self.Nspws)] = np.repeat(
                    np.arange(self.Nspws), Nrecs
                )

                # Record index positions
                blt_map_array[last_row : last_row + (Nrecs * self.Nspws)] = np.tile(
                    np.where(scan_screen)[0], self.Nspws
                )

                # Extract out the relevant data out of our data-like arrays that
                # belong to this scan number.
                val_dict = {}
                for attr, col in zip(attr_list, col_list):
                    if self.future_array_shapes:
                        val_dict[col] = getattr(self, attr)[scan_screen]
                    else:
                        val_dict[col] = np.squeeze(
                            getattr(self, attr)[scan_screen], axis=1
                        )

                # This is where the bulk of the heavy lifting is - use the per-spw
                # channel masks to record one spectral window at a time.
                for spw_num in self.spw_array:
                    for col in col_list:
                        ms.putcol(
                            col,
                            val_dict[col][:, spw_sel_dict[spw_num]],
                            last_row,
                            Nrecs,
                        )

                    # Tally here the "wideband" weights for the whole spectral window,
                    # which is used in some CASA routines.
                    temp_weights[last_row : last_row + Nrecs] = np.median(
                        val_dict["WEIGHT_SPECTRUM"][:, spw_sel_dict[spw_num]], axis=1
                    )
                    last_row += Nrecs

            # Now that we have an array to map baselime-time to individual records,
            # use our indexing array to map various metadata.
            ant_1_array = self.ant_1_array[blt_map_array]
            ant_2_array = self.ant_2_array[blt_map_array]
            integration_time = self.integration_time[blt_map_array]
            time_array = time_array[blt_map_array]
            uvw_array = self.uvw_array[blt_map_array]
            scan_number_array = self.scan_number_array[blt_map_array]

        # Write out the units of the visibilities, post a warning if its not in Jy since
        # we don't know how every CASA program may react
        ms.putcolkeyword("DATA", "QuantumUnits", self.vis_units)
        if self.vis_units != "Jy":
            warnings.warn(
                "Writing in the MS file that the units of the data are %s, although "
                "some CASA process will ignore this and assume the units are all in Jy "
                "(or may not know how to handle data in these units)." % self.vis_units
            )

        # TODO: If/when UVData objects can store visibility noise estimates, update
        # the code below to capture those.
        ms.putcol("WEIGHT", temp_weights)
        ms.putcol("SIGMA", np.power(temp_weights, -0.5, where=temp_weights != 0))

        ms.putcol("ANTENNA1", ant_1_array)
        ms.putcol("ANTENNA2", ant_2_array)

        # "INVERVAL" refers to "width" of the window of time time over which data was
        # collected, while "EXPOSURE" is the sum total of integration time.  UVData
        # does not differentiate between these concepts, hence why one array is used
        # for both values.
        ms.putcol("INTERVAL", integration_time)
        ms.putcol("EXPOSURE", integration_time)

        ms.putcol("DATA_DESC_ID", data_desc_array)
        ms.putcol("SCAN_NUMBER", scan_number_array)
        ms.putcol("TIME", time_array)
        ms.putcol("TIME_CENTROID", time_array)

        # FITS uvw direction convention is opposite ours and Miriad's.
        # CASA's convention is unclear: the docs contradict themselves,
        # but after a question to the helpdesk we got a clear response that
        # the convention is antenna2_pos - antenna1_pos, so the convention is the
        # same as ours & Miriad's
        ms.putcol("UVW", uvw_array)

        # We have to do an extra bit of work here, as CASA won't accept arbitrary
        # values for field ID (rather, the ID number matches to the row number in
        # the FIELD subtable). When we write out the fields, we use sort so that
        # we can reproduce the same ordering here.
        field_ids = np.empty_like(self.phase_center_id_array)
        for idx, cat_id in enumerate(self.phase_center_catalog):
            field_ids[self.phase_center_id_array == cat_id] = idx

        ms.putcol("FIELD_ID", field_ids[blt_map_array] if self.Nspws > 1 else field_ids)

        # Finally, record extra keywords and x_orientation, both of which the MS format
        # doesn't quite have equivalent fields to stuff data into (and instead is put
        # into the main header as a keyword).
        if len(self.extra_keywords) != 0:
            ms.putkeyword("pyuvdata_extra", self.extra_keywords)

        if self.x_orientation is not None:
            ms.putkeyword("pyuvdata_xorient", self.x_orientation)

        ms.done()

        self._write_ms_antenna(filepath)
        self._write_ms_data_description(filepath)
        self._write_ms_feed(filepath)
        self._write_ms_field(filepath)
        self._write_ms_source(filepath)
        self._write_ms_spectralwindow(filepath)
        self._write_ms_pointing(filepath)
        self._write_ms_polarization(filepath)
        self._write_ms_observation(filepath)
        self._write_ms_history(filepath)

    def _parse_casa_frame_ref(self, ref_name, raise_error=True):
        """
        Interpret a CASA frame into an astropy-friendly frame and epoch.

        Parameters
        ----------
        ref_name : str
            Name of the CASA-based spatial coordinate reference frame.
        raise_error : bool
            Whether to raise an error if the name has no match. Default is True, if set
            to false will raise a warning instead.

        Returns
        -------
        frame_name : str
            Name of the frame. Typically matched to the UVData attribute
            `phase_center_frame`.
        epoch_val : float
            Epoch value for the given frame, in Julian years unless `frame_name="FK4"`,
            in which case the value is in Besselian years. Typically matched to the
            UVData attribute `phase_center_epoch`.

        Raises
        ------
        ValueError
            If the CASA coordinate frame does not match the known supported list of
            frames for CASA.
        NotImplementedError
            If using a CASA coordinate frame that does not yet have a corresponding
            astropy frame that is supported by pyuvdata.
        """
        frame_name = None
        epoch_val = None
        try:
            frame_tuple = COORD_UVDATA2CASA_DICT[ref_name]
            if frame_tuple is None:
                message = f"Support for the {ref_name} frame is not yet supported."
                if raise_error:
                    raise NotImplementedError(message)
                else:
                    warnings.warn(message)
            else:
                frame_name = frame_tuple[0]
                epoch_val = frame_tuple[1]
        except KeyError as err:
            message = (
                f"The coordinate frame {ref_name} is not one of the supported frames "
                "for measurement sets."
            )
            if raise_error:
                raise ValueError(message) from err
            else:
                warnings.warn(message)

        return frame_name, epoch_val

    def _parse_pyuvdata_frame_ref(self, frame_name, epoch_val, raise_error=True):
        """
        Interpret a UVData pair of frame + epoch into a CASA frame name.

        Parameters
        ----------
        frame_name : str
            Name of the frame. Typically matched to the UVData attribute
            `phase_center_frame`.
        epoch_val : float
            Epoch value for the given frame, in Julian years unless `frame_name="FK4"`,
            in which case the value is in Besselian years. Typically matched to the
            UVData attribute `phase_center_epoch`.
        raise_error : bool
            Whether to raise an error if the name has no match. Default is True, if set
            to false will raise a warning instead.

        Returns
        -------
        ref_name : str
            Name of the CASA-based spatial coordinate reference frame.

        Raises
        ------
        ValueError
            If the provided coordinate frame and/or epoch value has no matching
            counterpart to those supported in CASA.

        """
        # N.B. -- this is something of a stub for a more sophisticated function to
        # handle this. For now, this just does a reverse lookup on the limited frames
        # supported by UVData objects, although eventually it can be expanded to support
        # more native MS frames.
        reverse_dict = {ref: key for key, ref in COORD_UVDATA2CASA_DICT.items()}

        ref_name = None
        try:
            ref_name = reverse_dict[
                (str(frame_name), 2000.0 if (epoch_val is None) else float(epoch_val))
            ]
        except KeyError as err:
            epoch_msg = (
                "no epoch" if epoch_val is None else f"epoch {format(epoch_val,'g')}"
            )
            message = (
                f"Frame {frame_name} ({epoch_msg}) does not have a "
                "corresponding match to supported frames in the MS file format."
            )
            if raise_error:
                raise ValueError(message) from err
            else:
                warnings.warn(message)

        return ref_name

    def _read_ms_main(
        self,
        filepath,
        data_column,
        data_desc_dict,
        read_weights=True,
        flip_conj=False,
        raise_error=True,
        allow_flex_pol=False,
    ):
        """
        Read data from the main table of a MS file.

        This method is not meant to be called by users, and is instead a utility
        function for the `read_ms` method (which users should call instead).

        Parameters
        ----------
        filepath : str
            The measurement set root directory to read from.
        data_column : str
            name of CASA data column to read into data_array. Options are:
            'DATA', 'MODEL', or 'CORRECTED_DATA'
        data_desc_dict : dict
            Dictionary describing the various rows in the DATA_DESCRIPTION table of
            an MS file. Keys match to the individual rows, and the values are themselves
            dicts containing several keys (including "CORR_TYPE", "SPW_ID", "NUM_CORR",
            "NUM_CHAN").
        read_weights : bool
            Read in the weights from the MS file, default is True. If false, the method
            will set the `nsamples_array` to the same uniform value (namely 1.0).
        flip_conj : bool
            On read, whether to flip the conjugation of the baselines. Normally the MS
            format is the same as what is used for pyuvdata (ant2 - ant1), hence the
            default is False.
        raise_error : bool
            On read, whether to raise an error if different records (i.e.,
            different spectral windows) report different metadata for the same
            time-baseline combination (which CASA allows but UVData does not) or if the
            timescale is not supported by astropy. Default is True, if set to False will
            raise a warning instead.
        allow_flex_pol : bool
            If only one polarization per spectral window is read (and the polarization
            differs from window to window), compress down the polarization-axis of
            various attributes (e.g, `data_array`, `flag_array`) to be of length 1.
            Default is True.

        Returns
        -------
        spw_list : list of int
            List of SPW numbers present in the data set, equivalent to the attribute
            `spw_array` in a UVData object.
        field_list : list of int
            List of field IDs present in the data set. Matched to rows in the FIELD
            table for the measurement set.
        pol_list : list of int
            List of polarization IDs (in the AIPS convention) present in the data set.
            Equivalent to the attribute `polarization_array` in a UVData object.
        flex_pol : list of int
            If `allow_flex_pol=True`, and only one polarization per spectral window is
            read (differing window-to-window), list of the polarization IDs present
            for each window. Equivalent to the attribute `flex_spw_polarization_array`
            in a UVData object.

        Raises
        ------
        ValueError
            If the `data_column` is not set to an allowed value.
            If the MS file contains data from multiple subarrays.
        """
        tb_main = tables.table(filepath, ack=False)

        main_keywords = tb_main.getkeywords()
        if "pyuvdata_extra" in main_keywords.keys():
            self.extra_keywords = main_keywords["pyuvdata_extra"]

        if "pyuvdata_xorient" in main_keywords.keys():
            self.x_orientation = main_keywords["pyuvdata_xorient"]

        default_vis_units = {"DATA": "uncalib", "CORRECTED_DATA": "Jy", "MODEL": "Jy"}

        # make sure user requests a valid data_column
        if data_column not in default_vis_units.keys():
            raise ValueError(
                "Invalid data_column value supplied. Use 'Data','MODEL' or"
                " 'CORRECTED_DATA'"
            )

        # set visibility units
        try:
            self.vis_units = tb_main.getcolkeywords(data_column)["QuantumUnits"]
        except KeyError:
            self.vis_units = default_vis_units[data_column]

        # limit length of extra_keywords keys to 8 characters to match uvfits & miriad
        self.extra_keywords["DATA_COL"] = data_column

        time_arr = tb_main.getcol("TIME")
        timescale = tb_main.getcolkeyword("TIME", "MEASINFO")["Ref"]
        if timescale.lower() not in Time.SCALES:
            msg = (
                "This file has a timescale that is not supported by astropy. "
                "If you need support for this timescale please make an issue on our "
                "GitHub repo."
            )
            if raise_error:
                raise ValueError(
                    msg + " To bypass this error, you can set raise_error=False, which "
                    "will raise a warning instead and treat the time as being in UTC."
                )
            else:
                warnings.warn(msg + " Defaulting to treating it as being in UTC.")
                timescale = "utc"
        # N.b., EXPOSURE is what's needed for noise calculation, but INTERVAL defines
        # the time period over which the data are collected
        int_arr = tb_main.getcol("EXPOSURE")
        ant_1_arr = tb_main.getcol("ANTENNA1")
        ant_2_arr = tb_main.getcol("ANTENNA2")
        field_arr = tb_main.getcol("FIELD_ID")
        scan_number_arr = tb_main.getcol("SCAN_NUMBER")
        uvw_arr = tb_main.getcol("UVW")
        data_desc_arr = tb_main.getcol("DATA_DESC_ID")
        subarr_arr = tb_main.getcol("ARRAY_ID")
        unique_data_desc = np.unique(data_desc_arr)

        if len(np.unique(subarr_arr)) > 1:
            raise ValueError(
                "This file appears to have multiple subarray "
                "values; only files with one subarray are "
                "supported."
            )

        data_desc_count = np.sum(np.isin(list(data_desc_dict.keys()), unique_data_desc))

        if data_desc_count == 0:
            # If there are no records selected, then there isnt a whole lot to do
            return None, None, None, None
        elif data_desc_count == 1:
            # If we only have a single spectral window, then we can bypass a whole lot
            # of slicing and dicing on account of there being a one-to-one releationship
            # in rows of the MS to the per-blt records of UVData objects.
            self.time_array = Time(
                time_arr / 86400.0, format="mjd", scale=timescale.lower()
            ).utc.jd
            self.integration_time = int_arr
            self.ant_1_array = ant_1_arr
            self.ant_2_array = ant_2_arr
            self.uvw_array = uvw_arr * ((-1) ** flip_conj)
            self.phase_center_id_array = field_arr
            self.scan_number_array = scan_number_arr

            self.flag_array = tb_main.getcol("FLAG")

            if flip_conj:
                self.data_array = np.conj(tb_main.getcol(data_column))
            else:
                self.data_array = tb_main.getcol(data_column)

            if read_weights:
                try:
                    self.nsample_array = tb_main.getcol("WEIGHT_SPECTRUM")
                except RuntimeError:
                    self.nsample_array = np.repeat(
                        np.expand_dims(tb_main.getcol("WEIGHT"), axis=1),
                        self.data_array.shape[1],
                        axis=1,
                    )
            else:
                self.nsample_array = np.ones_like(self.data_array, dtype=float)

            data_desc_key = np.intersect1d(
                unique_data_desc, list(data_desc_dict.keys())
            )[0]
            spw_list = [data_desc_dict[data_desc_key]["SPW_ID"]]
            self.flex_spw_id_array = spw_list[0] + np.zeros(
                data_desc_dict[data_desc_key]["NUM_CHAN"], dtype=int
            )

            field_list = np.unique(field_arr)
            pol_list = [
                POL_CASA2AIPS_DICT[key]
                for key in data_desc_dict[data_desc_key]["CORR_TYPE"]
            ]

            tb_main.close()
            return spw_list, field_list, pol_list, None

        tb_main.close()

        # If you are at this point, it means that we potentially have multiple spectral
        # windows to deal with, and so some additional care is required since MS does
        # NOT require data from all windows to be present simultaneously.

        use_row = np.zeros_like(time_arr, dtype=bool)
        data_dict = {}
        for key in data_desc_dict.keys():
            sel_mask = data_desc_arr == key

            if not np.any(sel_mask):
                continue

            use_row[sel_mask] = True
            data_dict[key] = dict(data_desc_dict[key])
            data_dict[key]["TIME"] = time_arr[sel_mask]  # Midpoint time in mjd seconds
            data_dict[key]["EXPOSURE"] = int_arr[sel_mask]  # Int time in sec
            data_dict[key]["ANTENNA1"] = ant_1_arr[sel_mask]  # First antenna
            data_dict[key]["ANTENNA2"] = ant_2_arr[sel_mask]  # Second antenna
            data_dict[key]["FIELD_ID"] = field_arr[sel_mask]  # Source ID
            data_dict[key]["SCAN_NUMBER"] = scan_number_arr[sel_mask]  # Scan number
            data_dict[key]["UVW"] = uvw_arr[sel_mask]  # UVW coords

        time_arr = time_arr[use_row]
        ant_1_arr = ant_1_arr[use_row]
        ant_2_arr = ant_2_arr[use_row]

        unique_blts = sorted(
            {
                (time, ant1, ant2)
                for time, ant1, ant2 in zip(time_arr, ant_1_arr, ant_2_arr)
            }
        )

        blt_dict = {}
        for idx, blt_tuple in enumerate(unique_blts):
            blt_dict[blt_tuple] = idx

        nblts = len(unique_blts)
        pol_list = np.unique([data_dict[key]["CORR_TYPE"] for key in data_dict.keys()])
        npols = len(pol_list)

        spw_dict = {
            data_dict[key]["SPW_ID"]: {
                "DATA_DICT_KEY": key,
                "NUM_CHAN": data_dict[key]["NUM_CHAN"],
            }
            for key in data_dict.keys()
        }
        spw_list = sorted(spw_dict.keys())

        # Here we sort out where the various spectral windows are starting and stoping
        # in our flex_spw spectrum, if applicable. By default, data are sorted in
        # spw-number order.
        nfreqs = 0
        spw_id_array = np.array([], dtype=int)
        for key in sorted(spw_dict.keys()):
            assert len(data_dict) == len(
                spw_dict
            ), "This is a bug, please make an issue in our issue log."
            data_dict_key = spw_dict[key]["DATA_DICT_KEY"]
            nchan = spw_dict[key]["NUM_CHAN"]
            data_dict[data_dict_key]["STARTCHAN"] = nfreqs
            data_dict[data_dict_key]["STOPCHAN"] = nfreqs + nchan
            data_dict[data_dict_key]["NUM_CHAN"] = nchan
            spw_id_array = np.append(spw_id_array, [key] * nchan)
            nfreqs += nchan

        all_single_pol = True
        for key in sorted(data_dict.keys()):
            blt_idx = [
                blt_dict[(time, ant1, ant2)]
                for time, ant1, ant2 in zip(
                    data_dict[key]["TIME"],
                    data_dict[key]["ANTENNA1"],
                    data_dict[key]["ANTENNA2"],
                )
            ]

            data_dict[key]["BLT_IDX"] = np.array(blt_idx, dtype=int)
            data_dict[key]["NBLTS"] = len(blt_idx)

            pol_idx = np.intersect1d(
                pol_list,
                data_desc_dict[key]["CORR_TYPE"],
                assume_unique=True,
                return_indices=True,
            )[1]
            data_dict[key]["POL_IDX"] = pol_idx.astype(int)
            all_single_pol = all_single_pol and (len(pol_idx) == 1)

        pol_list = [POL_CASA2AIPS_DICT[key] for key in pol_list]
        flex_pol = None

        if (
            allow_flex_pol
            and all_single_pol
            and ((len(pol_list) > 1) and (len(data_desc_dict) == len(spw_dict)))
        ):
            for key in data_dict.keys():
                spw_dict[data_dict[key]["SPW_ID"]]["POL"] = pol_list[
                    data_dict[key]["POL_IDX"][0]
                ]
                data_dict[key]["POL_IDX"] = np.array([0])
            pol_list = np.array([0])
            flex_pol = np.array(
                [spw_dict[key]["POL"] for key in sorted(spw_dict.keys())], dtype=int
            )

        # We have all of the meta-information linked the various data desc IDs,
        # so now we can finally get to the business of filling in the actual data.
        data_array = np.zeros((nblts, nfreqs, npols), dtype=complex)
        nsample_array = np.ones((nblts, nfreqs, npols))
        flag_array = np.ones((nblts, nfreqs, npols), dtype=bool)

        # We will also fill in our own metadata on a per-blt basis here
        time_arr = np.zeros(nblts)
        int_arr = np.zeros(nblts)
        ant_1_arr = np.zeros(nblts, dtype=int)
        ant_2_arr = np.zeros(nblts, dtype=int)
        field_arr = np.zeros(nblts, dtype=int)
        scan_number_arr = np.zeros(nblts, dtype=int)
        uvw_arr = np.zeros((nblts, 3))

        # Since each data description (i.e., spectral window) record can technically
        # have its own values for time, int-time, etc, we want to check and verify that
        # the values are consistent on a per-blt basis (since that's the most granular
        # pyuvdata can store that information).
        has_data = np.zeros(nblts, dtype=bool)

        arr_tuple = (
            time_arr,
            int_arr,
            ant_1_arr,
            ant_2_arr,
            field_arr,
            scan_number_arr,
            uvw_arr,
        )
        name_tuple = (
            "TIME",
            "EXPOSURE",
            "ANTENNA1",
            "ANTENNA2",
            "FIELD_ID",
            "SCAN_NUMBER",
            "UVW",
        )
        vary_list = []
        for key in data_dict.keys():
            # Get the indexing information for the data array
            blt_idx = data_dict[key]["BLT_IDX"]
            startchan = data_dict[key]["STARTCHAN"]
            stopchan = data_dict[key]["STOPCHAN"]
            pol_idx = data_dict[key]["POL_IDX"]

            # Identify which values have already been populated with data, so we know
            # which values to check.
            check_mask = has_data[blt_idx]
            check_idx = blt_idx[check_mask]

            # Loop through the metadata fields we intend to populate
            for arr, name in zip(arr_tuple, name_tuple):
                if not np.allclose(data_dict[key][name][check_mask], arr[check_idx]):
                    if raise_error:
                        raise ValueError(
                            "Column %s appears to vary on between windows, which is "
                            "not permitted for UVData objects. To bypass this error, "
                            "you can set raise_error=False, which will raise a warning "
                            "instead and use the first recorded value." % name
                        )
                    elif name not in vary_list:
                        # If not raising an error, then at least warn the user that
                        # discrepant data were detected.
                        warnings.warn(
                            "Column %s appears to vary on between windows, defaulting "
                            "to first recorded value." % name
                        )
                        # Add to a list so we don't spew multiple warnings for one
                        # column (which could just flood the terminal).
                        vary_list.append(name)

                arr[blt_idx[~check_mask]] = data_dict[key][name][~check_mask]

            # Can has data now please?
            has_data[blt_idx] = True

            # Remove a slice out of the larger arrays for us to populate with an MS read
            # operation. This has the advantage that if different data descrips contain
            # different polarizations (which is allowed), it will populate the arrays
            # correctly, although for most files (where all pols are written in one
            # data descrip), this shouldn't matter.
            temp_data = data_array[blt_idx, startchan:stopchan]
            temp_flags = flag_array[blt_idx, startchan:stopchan]
            if read_weights:
                temp_weights = nsample_array[blt_idx, startchan:stopchan]

            # This TaQL call allows the low-level C++ routines to handle mapping data
            # access, and returns a table object that _only_ has records matching our
            # request. This allows one to do a simple and fast getcol for reading the
            # data, flags, and weights, since they should all be the same shape on a
            # per-row basis for the same data description. Alternative read methods
            # w/ getcell, getvarcol, and per-row getcols produced way slower code.
            tb_main_sel = tables.taql(
                f"select from {filepath} where DATA_DESC_ID == {key}"  # nosec
            )

            # Fill in the temp arrays, and then plug them back into the main array.
            # Note that this operation has to be split in two because you can only use
            # advanced slicing on one axis (which both blt_idx and pol_idx require).
            if flip_conj:
                temp_data[:, :, pol_idx] = np.conj(tb_main_sel.getcol("DATA"))
            else:
                temp_data[:, :, pol_idx] = tb_main_sel.getcol("DATA")

            temp_flags[:, :, pol_idx] = tb_main_sel.getcol("FLAG")

            data_array[blt_idx, startchan:stopchan] = temp_data
            flag_array[blt_idx, startchan:stopchan] = temp_flags

            if read_weights:
                # The weights can be stored in a couple of different columns, but we
                # use a try/except here to capture two separate cases (that both will
                # produce runtime errors) -- when WEIGHT_SPECTRUM isn't a column, and
                # when it is BUT its unfilled (which causes getcol to throw an error).
                try:
                    temp_weights[:, :, pol_idx] = tb_main_sel.getcol("WEIGHT_SPECTRUM")
                except RuntimeError:
                    temp_weights[:, :, pol_idx] = np.repeat(
                        np.expand_dims(tb_main_sel.getcol("WEIGHT"), axis=1),
                        data_desc_dict[key]["NUM_CHAN"],
                        axis=1,
                    )
                nsample_array[blt_idx, startchan:stopchan, :] = temp_weights

            # Close the table, get ready for the next loop
            tb_main_sel.close()

        self.data_array = data_array
        self.flag_array = flag_array
        self.nsample_array = nsample_array

        self.ant_1_array = ant_1_arr
        self.ant_2_array = ant_2_arr

        self.time_array = Time(
            time_arr / 86400.0, format="mjd", scale=timescale.lower()
        ).utc.jd
        self.integration_time = int_arr
        self.uvw_array = uvw_arr * ((-1) ** flip_conj)
        self.phase_center_id_array = field_arr
        self.phase_center_id_array = field_arr
        self.scan_number_array = scan_number_arr
        self.flex_spw_id_array = spw_id_array

        field_list = np.unique(field_arr).astype(int).tolist()

        return spw_list, field_list, pol_list, flex_pol

    def read_ms(
        self,
        filepath,
        data_column="DATA",
        pol_order="AIPS",
        background_lsts=True,
        run_check=True,
        check_extra=True,
        run_check_acceptability=True,
        strict_uvw_antpos_check=False,
        ignore_single_chan=True,
        raise_error=True,
        read_weights=True,
        allow_flex_pol=False,
        check_autos=True,
        fix_autos=True,
        use_future_array_shapes=False,
    ):
        """
        Read in a casa measurement set.

        Parameters
        ----------
        filepath : str
            The measurement set root directory to read from.
        data_column : str
            name of CASA data column to read into data_array. Options are:
            'DATA', 'MODEL', or 'CORRECTED_DATA'
        pol_order : str
            Option to specify polarizations order convention, options are
            'CASA' or 'AIPS'.
        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 after reading in the file (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
            reading in the file (the default is True, meaning the acceptable
            range check will be done).
        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.
        ignore_single_chan : bool
            Some measurement sets (e.g., those from ALMA) use single channel spectral
            windows for recording pseudo-continuum channels or storing other metadata
            in the track when the telescopes are not on source. Because of the way
            the object is strutured (where all spectral windows are assumed to be
            simultaneously recorded), this can significantly inflate the size and memory
            footprint of UVData objects. By default, single channel windows are ignored
            to avoid this issue, although they can be included if setting this parameter
            equal to True.
        raise_error : bool
            The measurement set format allows for different spectral windows and
            polarizations to have different metdata for the same time-baseline
            combination, but UVData objects do not. It also allows for timescales that
            are not supported by astropy. If any of these problems are detected, by
            default the reader will throw an error. However, if set to False, the reader
            will simply give a warning and try to do the best it can. If the problem is
            with differing metadata, it will use the first value read in the file as the
            "correct" metadata in the UVData object. If the problem is with the
            timescale, it will just assume UTC.
        read_weights : bool
            Read in the weights from the MS file, default is True. If false, the method
            will set the `nsamples_array` to the same uniform value (namely 1.0).
        allow_flex_pol : bool
            If only one polarization per spectral window is read (and the polarization
            differs from window to window), allow for the `UVData` object to use
            "flexible polarization", which compresses the polarization-axis of various
            attributes to be of length 1, sets the `flex_spw_polarization_array`
            attribute to define the polarization per spectral window.  Default is True.
        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 True.
        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.

        Raises
        ------
        IOError
            If root file directory doesn't exist.
        ValueError
            If the `data_column` is not set to an allowed value.
            If the data are have multiple subarrays or are multi source or have
            multiple spectral windows.
            If the data have multiple data description ID values.

        """
        if not casa_present:  # pragma: no cover
            raise ImportError(no_casa_message) from casa_error

        if not os.path.exists(filepath):
            raise IOError(filepath + " not found")
        # set filename variable
        basename = filepath.rstrip("/")
        self.filename = [os.path.basename(basename)]
        self._filename.form = (1,)

        # get the history info
        pyuvdata_written = False
        if tables.tableexists(filepath + "/HISTORY"):
            self.history, pyuvdata_written = self._ms_hist_to_string(
                tables.table(filepath + "/HISTORY", ack=False)
            )
            if not uvutils._check_history_version(
                self.history, self.pyuvdata_version_str
            ):
                self.history += self.pyuvdata_version_str
        else:
            self.history = self.pyuvdata_version_str

        data_desc_dict = {}
        tb_desc = tables.table(filepath + "/DATA_DESCRIPTION", ack=False)
        for idx in range(tb_desc.nrows()):
            data_desc_dict[idx] = {
                "SPECTRAL_WINDOW_ID": tb_desc.getcell("SPECTRAL_WINDOW_ID", idx),
                "POLARIZATION_ID": tb_desc.getcell("POLARIZATION_ID", idx),
                "FLAG_ROW": tb_desc.getcell("FLAG_ROW", idx),
            }
        tb_desc.close()

        # Polarization array
        tb_pol = tables.table(filepath + "/POLARIZATION", ack=False)
        for key in data_desc_dict.keys():
            pol_id = data_desc_dict[key]["POLARIZATION_ID"]
            data_desc_dict[key]["CORR_TYPE"] = tb_pol.getcell(
                "CORR_TYPE", pol_id
            ).astype(int)
            data_desc_dict[key]["NUM_CORR"] = tb_pol.getcell("NUM_CORR", pol_id)
        tb_pol.close()

        tb_spws = tables.table(filepath + "/SPECTRAL_WINDOW", ack=False)

        try:
            spw_id = tb_spws.getcol("ASSOC_SPW_ID")
            use_assoc_id = True
        except RuntimeError:
            use_assoc_id = False

        single_chan_list = []
        for key in data_desc_dict.keys():
            spw_id = data_desc_dict[key]["SPECTRAL_WINDOW_ID"]
            data_desc_dict[key]["CHAN_FREQ"] = tb_spws.getcell("CHAN_FREQ", spw_id)
            # beware! There are possibly 3 columns here that might be the correct one
            # to use: CHAN_WIDTH, EFFECTIVE_BW, RESOLUTION
            data_desc_dict[key]["CHAN_WIDTH"] = tb_spws.getcell("CHAN_WIDTH", spw_id)
            data_desc_dict[key]["NUM_CHAN"] = tb_spws.getcell("NUM_CHAN", spw_id)
            if data_desc_dict[key]["NUM_CHAN"] == 1:
                single_chan_list.append(key)
            if use_assoc_id:
                data_desc_dict[key]["SPW_ID"] = int(
                    tb_spws.getcell("ASSOC_SPW_ID", spw_id)[0]
                )
            else:
                data_desc_dict[key]["SPW_ID"] = spw_id

        if ignore_single_chan:
            for key in single_chan_list:
                del data_desc_dict[key]

        # FITS uvw direction convention is opposite ours and Miriad's.
        # CASA's convention is unclear: the docs contradict themselves,
        # but after a question to the helpdesk we got a clear response that
        # the convention is antenna2_pos - antenna1_pos, so the convention is the
        # same as ours & Miriad's
        # HOWEVER, it appears that CASA's importuvfits task does not make this
        # convention change. So if the data in the MS came via that task and was not
        # written by pyuvdata, we do need to flip the uvws & conjugate the data
        flip_conj = ("importuvfits" in self.history) and (not pyuvdata_written)
        spw_list, field_list, pol_list, flex_pol = self._read_ms_main(
            filepath,
            data_column,
            data_desc_dict,
            read_weights=read_weights,
            flip_conj=flip_conj,
            raise_error=raise_error,
            allow_flex_pol=allow_flex_pol,
        )

        if (spw_list is None) and (field_list is None) and (pol_list is None):
            raise ValueError(
                "No valid data available in the MS file. If this file contains "
                "single channel data, set ignore_single_channel=True when calling "
                "read_ms."
            )

        self.Npols = len(pol_list)
        self.polarization_array = np.array(pol_list, dtype=np.int64)
        self.Nspws = len(spw_list)
        self.spw_array = np.array(spw_list, dtype=np.int64)
        self.flex_spw_polarization_array = flex_pol

        self.Nfreqs = len(self.flex_spw_id_array)
        self.freq_array = np.zeros(self.Nfreqs)
        self.channel_width = np.zeros(self.Nfreqs)

        for key in data_desc_dict.keys():
            sel_mask = self.flex_spw_id_array == data_desc_dict[key]["SPW_ID"]
            self.freq_array[sel_mask] = data_desc_dict[key]["CHAN_FREQ"]
            self.channel_width[sel_mask] = data_desc_dict[key]["CHAN_WIDTH"]

        # This if/else can go away in version 3.0 when channel width will always be an
        # array and the flex_spw_id_array will always be required.
        if (np.unique(self.channel_width).size == 1) and (len(spw_list) == 1):
            self.channel_width = float(self.channel_width[0])
        else:
            self._set_flex_spw()

        self.Ntimes = int(np.unique(self.time_array).size)
        self.Nblts = int(self.data_array.shape[0])
        self.Nants_data = len(
            np.unique(
                np.concatenate(
                    (np.unique(self.ant_1_array), np.unique(self.ant_2_array))
                )
            )
        )
        self.baseline_array = self.antnums_to_baseline(
            self.ant_1_array, self.ant_2_array
        )
        self.Nbls = len(np.unique(self.baseline_array))

        # open table with antenna location information
        tb_ant = tables.table(filepath + "/ANTENNA", ack=False)
        tb_obs = tables.table(filepath + "/OBSERVATION", ack=False)
        self.telescope_name = tb_obs.getcol("TELESCOPE_NAME")[0]
        self.instrument = tb_obs.getcol("TELESCOPE_NAME")[0]
        self.extra_keywords["observer"] = tb_obs.getcol("OBSERVER")[0]
        full_antenna_positions = tb_ant.getcol("POSITION")
        n_ants_table = full_antenna_positions.shape[0]
        xyz_telescope_frame = tb_ant.getcolkeyword("POSITION", "MEASINFO")["Ref"]

        # Note: measurement sets use the antenna number as an index into the antenna
        # table. This means that if the antenna numbers do not start from 0 and/or are
        # not contiguous, empty rows are inserted into the antenna table
        # these 'dummy' rows have positions of zero and need to be removed.
        # (this is somewhat similar to miriad)
        ant_good_position = np.nonzero(np.linalg.norm(full_antenna_positions, axis=1))[
            0
        ]
        full_antenna_positions = full_antenna_positions[ant_good_position, :]
        self.antenna_numbers = np.arange(n_ants_table)[ant_good_position]

        # check to see if a TELESCOPE_LOCATION column is present in the observation
        # table. This is non-standard, but inserted by pyuvdata
        if "TELESCOPE_LOCATION" in tb_obs.colnames():
            self.telescope_location = np.squeeze(tb_obs.getcol("TELESCOPE_LOCATION"))
        else:
            if self.telescope_name in self.known_telescopes():
                # Try to get it from the known telescopes
                self.set_telescope_params()
            elif xyz_telescope_frame == "ITRF":
                # Set it to be the mean of the antenna positions (this is not ideal!)
                self.telescope_location = np.array(
                    np.mean(full_antenna_positions, axis=0)
                )
            else:
                raise ValueError(
                    "Telescope frame is not ITRF and telescope is not "
                    "in known_telescopes, so telescope_location is not set."
                )
        tb_obs.close()

        # antenna names
        ant_names = np.asarray(tb_ant.getcol("NAME"))[ant_good_position].tolist()
        station_names = np.asarray(tb_ant.getcol("STATION"))[ant_good_position].tolist()
        antenna_diameters = tb_ant.getcol("DISH_DIAMETER")[ant_good_position]
        if np.any(antenna_diameters > 0):
            self.antenna_diameters = antenna_diameters

        # importuvfits measurement sets store antenna names in the STATION column.
        # cotter measurement sets store antenna names in the NAME column, which is
        # inline with the MS definition doc. In that case all the station names are
        # the same. Default to using what the MS definition doc specifies, unless
        # we read importuvfits in the history, or if the antenna column is not filled.
        if ("importuvfits" not in self.history) and (
            len(ant_names) == len(np.unique(ant_names)) and ("" not in ant_names)
        ):
            self.antenna_names = ant_names
        else:
            self.antenna_names = station_names

        self.Nants_telescope = len(self.antenna_names)

        relative_positions = np.zeros_like(full_antenna_positions)
        relative_positions = full_antenna_positions - self.telescope_location.reshape(
            1, 3
        )
        self.antenna_positions = relative_positions

        tb_ant.close()

        # set LST array from times and itrf
        proc = self.set_lsts_from_time_array(background=background_lsts)

        tb_field = tables.table(filepath + "/FIELD", ack=False)

        # Error if the phase_dir has a polynomial term because we don't know
        # how to handle that
        message = (
            "PHASE_DIR is expressed as a polynomial. "
            "We do not currently support this mode, please make an issue."
        )
        assert tb_field.getcol("PHASE_DIR").shape[1] == 1, message

        # The SOURCE table is optional in MS, but can contain some relevant metadata
        # about
        tb_sou_dict = {}
        try:
            tb_source = tables.table(filepath + "/SOURCE", ack=False)
        except RuntimeError:
            # The SOURCE table is optional, so if not found a RuntimeError will be
            # thrown, and we should forgo trying to associate SOURCE table entries with
            # the FIELD table.
            pass
        else:
            for idx in range(tb_source.nrows()):
                sou_id = tb_source.getcell("SOURCE_ID", idx)
                pm_vec = tb_source.getcell("PROPER_MOTION", idx)
                time_stamp = tb_source.getcell("TIME", idx)
                sou_vec = tb_source.getcell("DIRECTION", idx)
                try:
                    for idx in np.where(
                        np.isclose(
                            tb_sou_dict[sou_id]["cat_times"],
                            time_stamp,
                            rtol=0,
                            atol=1e-3,
                        )
                    )[0]:
                        if not (
                            (tb_sou_dict[sou_id]["cat_ra"][idx] == sou_vec[0])
                            and (tb_sou_dict[sou_id]["cat_dec"][idx] == sou_vec[1])
                            and (tb_sou_dict[sou_id]["cat_pm_ra"][idx] == pm_vec[0])
                            and (tb_sou_dict[sou_id]["cat_pm_dec"][idx] == pm_vec[1])
                        ):
                            warnings.warn(
                                "Different windows in this MS file contain different "
                                "metadata for the same integration. Be aware that "
                                "UVData objects do not allow for this, and thus will "
                                "default to using the metadata from the last row read "
                                "from the SOURCE table." + reporting_request
                            )
                        _ = tb_sou_dict[sou_id]["cat_times"].pop(idx)
                        _ = tb_sou_dict[sou_id]["cat_ra"].pop(idx)
                        _ = tb_sou_dict[sou_id]["cat_dec"].pop(idx)
                        _ = tb_sou_dict[sou_id]["cat_pm_ra"].pop(idx)
                        _ = tb_sou_dict[sou_id]["cat_pm_dec"].pop(idx)
                    tb_sou_dict[sou_id]["cat_times"].append(time_stamp)
                    tb_sou_dict[sou_id]["cat_ra"].append(sou_vec[0])
                    tb_sou_dict[sou_id]["cat_dec"].append(sou_vec[1])
                    tb_sou_dict[sou_id]["cat_pm_ra"].append(pm_vec[0])
                    tb_sou_dict[sou_id]["cat_pm_dec"].append(pm_vec[1])
                except KeyError:
                    tb_sou_dict[sou_id] = {
                        "cat_times": [time_stamp],
                        "cat_ra": [sou_vec[0]],
                        "cat_dec": [sou_vec[1]],
                        "cat_pm_ra": [pm_vec[0]],
                        "cat_pm_dec": [pm_vec[1]],
                    }

            for cat_dict in tb_sou_dict.values():
                make_arr = len(cat_dict["cat_times"]) != 1
                if not make_arr:
                    del cat_dict["cat_times"]

                for key in cat_dict:
                    if make_arr:
                        cat_dict[key] = np.array(cat_dict[key])
                    else:
                        cat_dict[key] = cat_dict[key][0]
                if np.allclose(cat_dict["cat_pm_ra"], 0):
                    if np.allclose(cat_dict["cat_pm_dec"], 0):
                        cat_dict["cat_pm_ra"] = cat_dict["cat_pm_dec"] = None

        # MSv2.0 appears to assume J2000. Not sure how to specifiy otherwise
        measinfo_keyword = tb_field.getcolkeyword("PHASE_DIR", "MEASINFO")

        ref_dir_colname = None
        ref_dir_dict = None
        if "VarRefCol" in measinfo_keyword.keys():
            # This seems to be a yet-undocumented feature for CASA, which allows one
            # to specify an additional (optional?) column that defines the reference
            # frame on a per-source basis.
            ref_dir_colname = measinfo_keyword["VarRefCol"]
            ref_dir_dict = dict(
                zip(measinfo_keyword["TabRefCodes"], measinfo_keyword["TabRefTypes"])
            )

        if "Ref" in measinfo_keyword.keys():
            frame_tuple = self._parse_casa_frame_ref(measinfo_keyword["Ref"])
            phase_center_frame = frame_tuple[0]
            phase_center_epoch = frame_tuple[1]
        else:
            warnings.warn("Coordinate reference frame not detected, defaulting to ICRS")
            phase_center_frame = "icrs"
            phase_center_epoch = 2000.0

        field_id_dict = {field_idx: field_idx for field_idx in field_list}
        try:
            id_arr = tb_field.getcol("SOURCE_ID")
            if np.all(id_arr >= 0) and len(np.unique(id_arr)) == len(id_arr):
                for idx, sou_id in enumerate(id_arr):
                    if idx in field_list:
                        field_id_dict[idx] = sou_id
        except RuntimeError:
            # Reach here if no column named SOURCE_ID exists, or if it does exist
            # but is completely unfilled. Nothing to do at this point but move on.
            tb_sou_dict = {}
            pass
        # Field names are allowed to be the same in CASA, so if we detect
        # conflicting names here we use the FIELD row numbers to try and
        # differentiate between them.
        field_name_list = tb_field.getcol("NAME")
        uniq_names, uniq_count = np.unique(field_name_list, return_counts=True)
        rep_name_list = uniq_names[uniq_count > 1]
        for rep_name in rep_name_list:
            rep_count = 0
            for idx in range(len(field_name_list)):
                if field_name_list[idx] == rep_name:
                    field_name_list[idx] = "%s-%03i" % (field_name_list[idx], rep_count)
                    rep_count += 1

        for field_idx in field_list:
            ra_val, dec_val = tb_field.getcell("PHASE_DIR", field_idx)[0]
            field_name = field_name_list[field_idx]
            if ref_dir_colname is not None:
                frame_tuple = self._parse_casa_frame_ref(
                    ref_dir_dict[tb_field.getcell(ref_dir_colname, field_list[0])]
                )
            else:
                frame_tuple = (phase_center_frame, phase_center_epoch)

            pm_ra = pm_dec = ephem_times = None
            if field_id_dict[field_idx] in tb_sou_dict:
                cat_dict = tb_sou_dict[field_id_dict[field_idx]]
                ra_val, dec_val = cat_dict["cat_ra"], cat_dict["cat_dec"]
                pm_ra, pm_dec = cat_dict["cat_pm_ra"], cat_dict["cat_pm_dec"]
                if "cat_times" in cat_dict:
                    ephem_times = Time(
                        cat_dict["cat_times"] / 86400, format="mjd", scale="utc"
                    ).jd

            self._add_phase_center(
                field_name,
                cat_type="sidereal" if (ephem_times is None) else "ephem",
                cat_lon=ra_val,
                cat_lat=dec_val,
                cat_times=ephem_times,  # Convert Julian secs to JD
                cat_frame=frame_tuple[0],
                cat_epoch=frame_tuple[1],
                cat_pm_ra=pm_ra,
                cat_pm_dec=pm_dec,
                info_source="file",
                cat_id=field_id_dict[field_idx],
            )
        # Only thing left to do is to update the IDs for the per-BLT records
        self.phase_center_id_array = np.array(
            [field_id_dict[sou_id] for sou_id in self.phase_center_id_array], dtype=int
        )

        tb_field.close()

        if proc is not None:
            proc.join()
        # Fill in the apparent coordinates here
        self._set_app_coords_helper()

        self.data_array = np.expand_dims(self.data_array, 1)
        self.nsample_array = np.expand_dims(self.nsample_array, 1)
        self.flag_array = np.expand_dims(self.flag_array, 1)
        self.freq_array = np.expand_dims(self.freq_array, 0)

        # order polarizations
        self.reorder_pols(order=pol_order, run_check=False)

        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,
                strict_uvw_antpos_check=strict_uvw_antpos_check,
                allow_flip_conj=True,
                check_autos=check_autos,
                fix_autos=fix_autos,
            )


# pyuvdata/uvdata/mwa_corr_fits.py

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

"""Class for reading MWA correlator FITS files."""

import itertools
import os
import warnings

import h5py
import numpy as np
from astropy import constants as const
from astropy.io import fits
from astropy.time import Time
from scipy.integrate import simps
from scipy.special import erf

from pyuvdata.data import DATA_PATH

from .. import _corr_fits
from .. import utils as uvutils
from .uvdata import UVData, _future_array_shapes_warning

__all__ = ["input_output_mapping", "MWACorrFITS"]


def input_output_mapping():
    """Build a mapping dictionary from pfb input to output numbers."""
    # the polyphase filter bank maps inputs to outputs, which the MWA
    # correlator then records as the antenna indices.
    # the following is taken from mwa_build_lfiles/mwac_utils.c
    # inputs are mapped to outputs via pfb_mapper as follows
    # (from mwa_build_lfiles/antenna_mapping.h):
    # floor(index/4) + index%4 * 16 = input
    # for the first 64 outputs, pfb_mapper[output] = input
    return _corr_fits.input_output_mapping()


def sighat_vector(x):
    """
    Generate quantized sigma using Van Vleck relation.

    For an explanation of the Van Vleck relations used and their implementation
    in this code, see the memos at
    https://github.com/EoRImaging/Memos/blob/master/PDFs/007_Van_Vleck_A.pdf and
    https://github.com/EoRImaging/Memos/blob/master/PDFs/008_Van_Vleck_B.pdf

    Parameters
    ----------
    x : numpy array
        Array of sigma inputs.

    Returns
    -------
    sighat : numpy array
        Array of corresponding sigmas of quantized values.

    """
    yy = np.arange(7)[:, np.newaxis]
    z = (2 * yy + 1) * erf((yy + 0.5) / (x * np.sqrt(2)))
    z = z.sum(axis=0)
    sighat = np.sqrt(7**2 - z)
    return sighat


def sighat_vector_prime(x):
    """
    Calculate the derivative of sighat_vector.

    Parameters
    ----------
    x : numpy array
        Array of sigma inputs.

    Returns
    -------
    sighat : numpy array
        Array of corresponding derivatives with respect to sigma inputs.

    """
    yy = np.arange(7)[:, np.newaxis]
    z = (
        (2 * yy + 1)
        * (yy + 0.5)
        * np.exp(-((yy + 0.5) ** 2) / (2 * (x**2)))
        / (np.sqrt(2 * np.pi) * (x**2))
    )
    sighat_prime = z.sum(axis=0)
    sighat_prime /= sighat_vector(x)
    return sighat_prime


def corrcorrect_simps(rho, sig1, sig2):
    """
    Generate quantized kappa using the Van Vleck relation.

    For an explanation of the Van Vleck relations used and their implementation
    in this code, see the memos at
    https://github.com/EoRImaging/Memos/blob/master/PDFs/007_Van_Vleck_A.pdf and
    https://github.com/EoRImaging/Memos/blob/master/PDFs/008_Van_Vleck_B.pdf

    Parameters
    ----------
    rho : numpy array
        Array of rho inputs.
    sig1 : numpy array
        Array of sigma inputs corresponding to antenna 1.
    sig2: numpy array
        Array of sigma inputs corresponding to antenna 2.

    Returns
    -------
    integrated_khat : numpy array
        Array of cross-correlations of quantized values.

    """
    x = np.linspace(0, rho, 11, dtype=np.float64)
    khat = np.zeros((11, rho.size), dtype=np.float64)
    khat = _corr_fits.get_khat(x, sig1, sig2)
    integrated_khat = simps(khat, x, axis=0)
    return integrated_khat


def corrcorrect_vect_prime(rho, sig1, sig2):
    """
    Calculate the derivative of corrcorrect_simps.

    Parameters
    ----------
    rho : numpy array
        Array of rho inputs.
    sig1 : numpy array
        Array of sigma inputs corresponding to antenna 1.
    sig2: numpy array
        Array of sigma inputs corresponding to antenna 2.

    """
    return _corr_fits.get_khat(rho, sig1, sig2)


def van_vleck_autos(sighat_arr):
    """
    Use Newton's method to solve the inverse of sighat_vector.

    For an explanation of the Van Vleck corrections used and their implementation
    in this code, see the memos at
    https://github.com/EoRImaging/Memos/blob/master/PDFs/007_Van_Vleck_A.pdf and
    https://github.com/EoRImaging/Memos/blob/master/PDFs/008_Van_Vleck_B.pdf

    Parameters
    ----------
    sighat_arr : numpy array
        Array of quantized sigma to be corrected.

    Returns
    -------
    sighat_arr : numpy array
        Array of Van Vleck corrected scaled auto-correlations.

    """
    # cut off small sigmas that will not converge
    cutoff_inds = np.where(sighat_arr > 0.5)[0]
    sighat = sighat_arr[cutoff_inds]
    if len(sighat) > 0:
        guess = np.copy(sighat)
        inds = np.where(np.abs(sighat_vector(guess) - sighat) > 1e-10)[0]
        while len(inds) != 0:
            guess[inds] -= (
                sighat_vector(guess[inds]) - sighat[inds]
            ) / sighat_vector_prime(guess[inds])
            inds = np.where(np.abs(sighat_vector(guess) - sighat) > 1e-10)[0]
        sighat_arr[cutoff_inds] = guess

    return sighat_arr


def van_vleck_crosses_int(k_arr, sig1_arr, sig2_arr, cheby_approx):
    """
    Use Newton's method to solve the inverse of corrcorrect_simps.

    For an explanation of the Van Vleck corrections used and their implementation
    in this code, see the memos at
    https://github.com/EoRImaging/Memos/blob/master/PDFs/007_Van_Vleck_A.pdf and
    https://github.com/EoRImaging/Memos/blob/master/PDFs/008_Van_Vleck_B.pdf

    Parameters
    ----------
    k_arr : numpy array
        Array of quantized kappa to be corrected.
    sig1_arr : numpy array
        Array of sigma inputs corresponding to antenna 1.
    sig2_arr: numpy array
        Array of sigma inputs corresponding to antenna 2.
    cheby_approx : bool
        Flag to warn if chebyshev approximation is being used.

    Returns
    -------
    k_arr : numpy array
        Array of Van Vleck corrected scaled cross-correlations.

    """
    nonzero_inds = np.where((k_arr != 0) & (sig1_arr != 0) & (sig2_arr != 0))[0]
    if len(nonzero_inds) > 0.0:
        if cheby_approx:
            warnings.warn(
                str(len(nonzero_inds))
                + " values are being corrected with the van vleck integral"
            )
        neg_inds = np.where(k_arr < 0.0)[0]
        khat = np.abs(k_arr[nonzero_inds])
        sig1 = sig1_arr[nonzero_inds]
        sig2 = sig2_arr[nonzero_inds]
        x0 = khat / (sig1 * sig2)
        corr = corrcorrect_simps(x0, sig1, sig2) - khat
        x0 -= corr / corrcorrect_vect_prime(x0, sig1, sig2)
        inds = np.where(np.abs(corr) > 1e-8)[0]
        while len(inds) != 0:
            corr = corrcorrect_simps(x0[inds], sig1[inds], sig2[inds]) - khat[inds]
            x0[inds] -= corr / corrcorrect_vect_prime(x0[inds], sig1[inds], sig2[inds])
            inds2 = np.where(np.abs(corr) > 1e-8)[0]
            inds = inds[inds2]
        k_arr[nonzero_inds] = x0 * sig1 * sig2
        k_arr[neg_inds] = np.negative(k_arr[neg_inds])

    return k_arr


def van_vleck_crosses_cheby(
    khat,
    sig1,
    sig2,
    broad_inds,
    rho_coeff,
    sv_inds_right1,
    sv_inds_right2,
    ds1,
    ds2,
    cheby_approx,
):
    """
    Compute a chebyshev approximation of corrcorrect_simps.

    Uses a bilinear interpolation to find chebyshev coefficients. Assumes distance
    between points of interpolation grid is 0.01. If sig1 or sig2 falls outside
    the interpolation grid, the corresponding values are corrected using
    van_vleck_crosses_int.

    For an explanation of the Van Vleck corrections used and their implementation
    in this code, see the memos at
    https://github.com/EoRImaging/Memos/blob/master/PDFs/007_Van_Vleck_A.pdf and
    https://github.com/EoRImaging/Memos/blob/master/PDFs/008_Van_Vleck_B.pdf

    Parameters
    ----------
    khat : numpy array
        Array of quantized kappa to be corrected.
    sig1 : numpy array
        Array of sigma inputs corresponding to antenna 1.
    sig2: numpy array
        Array of sigma inputs corresponding to antenna 2.
    broad_inds : numpy array
        Array indexing sigmas within the chebyshev approximation range.
    rho_coeff : numpy array
        Array of chebyshev polynomial coefficients.
    sv_inds_right1 : numpy array
        Array of right indices for sig1 for bilinear interpolation.
    sv_inds_right2 : numpy array
        Array of right indices for sig2 for bilinear interpolation.
    ds1 : numpy array
        Distance between sig1 and right-indexed value for bilinear interpolation.
    ds2 : numpy array
        Distance between sig2 and right-indexed value for bilinear interpolation.
    cheby_approx : bool
        Flag to warn if chebyshev approximation is being used.

    Returns
    -------
    khat : numpy array
        Array of Van Vleck corrected scaled cross-correlations.

    """
    kap = np.array([khat[broad_inds].real, khat[broad_inds].imag])
    _corr_fits.van_vleck_cheby(kap, rho_coeff, sv_inds_right1, sv_inds_right2, ds1, ds2)
    khat[broad_inds] = (kap[0, :] + 1j * kap[1, :]) * (
        sig1[broad_inds] * sig2[broad_inds]
    )
    khat[~broad_inds] = van_vleck_crosses_int(
        khat.real[~broad_inds], sig1[~broad_inds], sig2[~broad_inds], cheby_approx
    ) + 1j * van_vleck_crosses_int(
        khat.imag[~broad_inds], sig1[~broad_inds], sig2[~broad_inds], cheby_approx
    )

    return khat


class MWACorrFITS(UVData):
    """
    UVData subclass for reading MWA correlator fits files.

    This class should not be interacted with directly; instead use the
    read_mwa_corr_fits method on the UVData class.
    """

    def correct_cable_length(self, cable_lens, ant_1_inds, ant_2_inds):
        """
        Apply a cable length correction to the data array.

        Parameters
        ----------
        cable_lens : list of strings
            A list of strings containing the cable lengths for each antenna.
        ant_1_inds : array
            An array of indices for antenna 1
        ant_2_inds : array
            An array of indices for antenna 2

        """
        # as of version 0.29.X cython does not handle numpy arrays of strings
        # particularly efficiently. Casting to bytes, then into this demonic
        # form is a workaround found here: https://stackoverflow.com/a/28777163
        cable_lens = np.asarray(cable_lens).astype(np.string_)
        cable_lens = cable_lens.view("uint8").reshape(
            cable_lens.size, cable_lens.dtype.itemsize
        )
        # from MWA_Tools/CONV2UVFITS/convutils.h
        cable_len_diffs = _corr_fits.get_cable_len_diffs(
            ant_1_inds, ant_2_inds, cable_lens
        )
        self.data_array *= np.exp(
            -1j
            * 2
            * np.pi
            * cable_len_diffs.reshape(self.Nblts, 1)
            / const.c.to("m/s").value
            * self.freq_array.reshape(1, self.Nfreqs)
        )[:, :, None]
        history_add_string = " Applied cable length correction."
        self.history += history_add_string

    def flag_init(
        self,
        num_fine_chan,
        edge_width=80e3,
        start_flag=2.0,
        end_flag=0.0,
        flag_dc_offset=True,
    ):
        """
        Apply routine flagging to the MWA Correlator FITS file data.

        Includes options to flag the coarse channel edges, beginning and end
        of obs, as well as the center fine channel of each coarse channel.

        Parameters
        ----------
        edge_width: float
            The width to flag on the edge of each coarse channel, in hz. Set to
            0 for no edge flagging.
        start_flag: float
            The number of seconds to flag at the beginning of the observation.
            Set to 0 for no flagging.
        end_flag: floats
            The number of seconds to flag at the end of the observation. Set to
            0 for no flagging.
        flag_dc_offset: bool
            Set to True to flag the center fine channel of each coarse channel.

        Raises
        ------
        ValueError
            If edge_width is not an integer multiple of the channel_width of
            the data (0 also acceptable).
            If start_flag is not an integer multiple of the integration time
            (0 also acceptable).
            If end_flag is not an integer multiple of the integration time
            (0 also acceptable).

        """
        if (edge_width % self.channel_width[0]) > 0:
            raise ValueError(
                "The edge_width must be an integer multiple of the "
                "channel_width of the data or zero."
            )
        if (start_flag % self.integration_time[0]) > 0:
            raise ValueError(
                "The start_flag must be an integer multiple of the "
                "integration_time of the data or zero."
            )
        if (end_flag % self.integration_time[0]) > 0:
            raise ValueError(
                "The end_flag must be an integer multiple of the "
                "integration_time of the data or zero."
            )

        num_ch_flag = int(edge_width / self.channel_width[0])
        num_start_flag = int(start_flag / self.integration_time[0])
        num_end_flag = int(end_flag / self.integration_time[0])

        shape = self.flag_array.shape
        reshape = [self.Ntimes, self.Nbls, self.Nfreqs, self.Npols]

        self.flag_array = (
            self.flag_array
            if (shape == reshape)
            else np.reshape(self.flag_array, reshape)
        )

        bad_chan_inds = []
        if num_ch_flag > 0:
            for ch_count in range(num_ch_flag):
                # count up from the left
                left_chans = list(range(ch_count, self.Nfreqs, num_fine_chan))
                # count down from the right
                right_chans = list(range(self.Nfreqs - 1 - ch_count, 0, -num_fine_chan))
                bad_chan_inds += left_chans + right_chans

        if flag_dc_offset:
            bad_chan_inds += list(range(num_fine_chan // 2, self.Nfreqs, num_fine_chan))

        if len(bad_chan_inds) != 0:
            self.flag_array[:, :, bad_chan_inds, :] = True

        if (num_start_flag > 0) or (num_end_flag > 0):
            if num_start_flag > 0:
                self.flag_array[:num_start_flag] = True
            if num_end_flag > 0:
                self.flag_array[-num_end_flag:] = True
            self.flag_array = np.reshape(self.flag_array, shape)

        self.flag_array = (
            self.flag_array
            if (shape == reshape)
            else np.reshape(self.flag_array, shape)
        )

    def _read_fits_file(
        self,
        filename,
        time_array,
        file_nums,
        num_fine_chans,
        int_time,
        mwax,
        map_inds,
        conj,
        pol_index_array,
    ):
        """
        Read the fits file and populate into memory.

        This is an internal function and should not regularly be called except
        by read_mwa_corr_fits function.

        It is designed to close the fits files, headers, and all associated pointers.
        Without this read in a function, reading files has a large memory footprint.

        Parameters
        ----------
        filename : str
            The mwa gpubox fits file to read
        time_array : array of floats
            The time_array object constructed during read_mwa_corr_fits call
        file_nums : array
            List of included file numbers ordered by coarse channel
        num_fine_chans : int
            Number of fine channels in each data file
        int_time : float
            The integration time of each observation.
        map_inds : array
            Indices for reordering data_array from weird correlator packing.
        conj : array
            Indices for conjugating data_array from weird correlator packing.
        pol_index_array : array
            Indices for reordering polarizations to the 'AIPS' convention

        """
        # get the file number from the file name
        if mwax:
            file_num = int(filename.split("_")[-2][-3:])
        else:
            file_num = int(filename.split("_")[-2][-2:])
        # map file number to frequency index
        freq_ind = np.where(file_nums == file_num)[0][0] * num_fine_chans
        # get a coarse channel index for flag array
        coarse_ind = np.where(file_nums == file_num)[0][0]
        # create an intermediate array for data
        if mwax:
            coarse_chan_data = np.zeros(
                (self.Ntimes, self.Nbls, num_fine_chans * self.Npols),
                dtype=np.complex64,
            )
        else:
            coarse_chan_data = np.zeros(
                (self.Ntimes, num_fine_chans, self.Nbls * self.Npols),
                dtype=np.complex64,
            )
        with fits.open(filename, mode="denywrite") as hdu_list:
            # if mwax, data is in every other hdu
            if mwax:
                hdu_list = hdu_list[1::2]
            for hdu in hdu_list:
                # entry 0 is a header, so we skip it.
                if hdu.data is None:
                    continue
                time = (
                    hdu.header["TIME"]
                    + hdu.header["MILLITIM"] / 1000.0
                    + int_time / 2.0
                )
                time_ind = np.where(time_array == time)[0][0]
                # dump data into matrix
                # and take data from real to complex numbers
                coarse_chan_data[time_ind, :, :] = (
                    hdu.data[:, 0::2] + 1j * hdu.data[:, 1::2]
                )
                # fill nsample and flag arrays
                # think about using the mwax weights array in the future
                self.nsample_array[
                    time_ind, :, freq_ind : freq_ind + num_fine_chans, :
                ] = 1.0
                self.flag_array[time_ind, :, coarse_ind, :] = False
        if not mwax:
            # do mapping and reshaping here to avoid copying whole data_array
            np.take(coarse_chan_data, map_inds, axis=2, out=coarse_chan_data)
            # conjugate data
            coarse_chan_data[:, :, conj] = np.conj(coarse_chan_data[:, :, conj])
        # reshape
        if mwax:
            coarse_chan_data = coarse_chan_data.reshape(
                (self.Ntimes, self.Nbls, num_fine_chans, self.Npols)
            )
        else:
            coarse_chan_data = coarse_chan_data.reshape(
                (self.Ntimes, num_fine_chans, self.Nbls, self.Npols)
            )
            coarse_chan_data = np.swapaxes(coarse_chan_data, 1, 2)
        coarse_chan_data = coarse_chan_data.reshape(
            self.Nblts, num_fine_chans, self.Npols
        )
        # reorder pols here to avoid memory spike from self.reorder_pols
        np.take(coarse_chan_data, pol_index_array, axis=-1, out=coarse_chan_data)
        # make a mask where data actually is so coarse channels that
        # are split into two files don't overwrite eachother
        data_mask = coarse_chan_data != 0
        self.data_array[:, freq_ind : freq_ind + num_fine_chans, :][
            data_mask
        ] = coarse_chan_data[data_mask]

        return

    def _read_flag_file(self, filename, file_nums, num_fine_chans):
        """
        Read aoflagger flag file into flag_array.

        Parameters
        ----------
        filename : str
            The aoflagger fits file to read.
        file_nums : array
            List of included file numbers ordered by coarse channel.
        num_fine_chans : int
            Number of fine channels in each data file.

        """
        flag_num = int(filename.split("_")[-1][0:2])
        # map file number to frequency index
        freq_ind = np.where(file_nums == flag_num)[0][0] * num_fine_chans
        with fits.open(filename, mode="denywrite") as aoflags:
            flags = aoflags[1].data.field("FLAGS")
        # some flag files are longer than data; crop the ends
        flags = flags[: self.Nblts, :]
        # some flag files are shorter than data; assume same end time
        blt_ind = self.Nblts - len(flags)
        flags = flags[:, :, np.newaxis]
        self.flag_array[
            blt_ind:, freq_ind : freq_ind + num_fine_chans, :
        ] = np.logical_or(
            self.flag_array[blt_ind:, freq_ind : freq_ind + num_fine_chans, :], flags
        )

    def van_vleck_correction(
        self, ant_1_inds, ant_2_inds, flagged_ant_inds, cheby_approx, data_array_dtype
    ):
        """
        Apply a van vleck correction to the data array.

        For an explanation of the Van Vleck corrections used and their implementation
        in this code, see the memos at
        https://github.com/EoRImaging/Memos/blob/master/PDFs/007_Van_Vleck_A.pdf and
        https://github.com/EoRImaging/Memos/blob/master/PDFs/008_Van_Vleck_B.pdf

        Parameters
        ----------
        ant_1_inds : array
            An array of indices for antenna 1.
        ant_2_inds : array
            An array of indices for antenna 2.
        flagged_ant_inds : numpy array of type int
            List of indices of flagged antennas.
        cheby_approx : bool
            Option to implement the van vleck correction with a chebyshev polynomial.
            approximation.
        data_array_dtype : numpy dtype
            Datatype to store the output data_array as.

        """
        history_add_string = " Applied Van Vleck correction."
        # reshape to (nbls, ntimes, nfreqs, npols)
        self.data_array = self.data_array.reshape(
            self.Ntimes, self.Nbls, self.Nfreqs, self.Npols
        )
        self.data_array = np.swapaxes(self.data_array, 0, 1)
        # combine axes
        self.data_array = self.data_array.reshape(
            (self.Nbls, self.Nfreqs * self.Ntimes, self.Npols)
        )
        # need data array to have 64 bit precision
        # work on this in the future to only change precision where necessary
        if self.data_array.dtype != np.complex128:
            self.data_array = self.data_array.astype(np.complex128)

        # scale the data
        # number of samples per fine channel is equal to channel width (Hz)
        # multiplied be the integration time (s)
        # circular symmetry gives a factor of two
        nsamples = self.channel_width[0] * self.integration_time[0] * 2
        self.data_array /= nsamples
        # get indices for autos
        autos = np.where(
            self.ant_1_array[0 : self.Nbls] == self.ant_2_array[0 : self.Nbls]
        )[0]
        # get indices for crosses
        crosses = np.where(
            self.ant_1_array[0 : self.Nbls] != self.ant_2_array[0 : self.Nbls]
        )[0]
        # find polarizations
        xx = np.where(self.polarization_array == -5)[0][0]
        yy = np.where(self.polarization_array == -6)[0][0]
        xy = np.where(self.polarization_array == -7)[0][0]
        yx = np.where(self.polarization_array == -8)[0][0]
        pols = np.array([yy, xx])
        # square root autos
        auto_inds = autos[:, np.newaxis]
        self.data_array.real[auto_inds, :, pols] = np.sqrt(
            self.data_array.real[auto_inds, :, pols]
        )
        # get unflagged autos
        good_autos = np.delete(autos, flagged_ant_inds)
        sighat = self.data_array.real[good_autos[:, np.newaxis], :, pols].flatten()
        # correct autos
        sigma = van_vleck_autos(sighat)
        self.data_array.real[good_autos[:, np.newaxis], :, pols] = sigma.reshape(
            len(good_autos), len(pols), self.Ntimes * self.Nfreqs
        )
        # get good crosses
        bad_ant_inds = np.nonzero(
            np.logical_or(
                np.isin(ant_1_inds[0 : self.Nbls], flagged_ant_inds),
                np.isin(ant_2_inds[0 : self.Nbls], flagged_ant_inds),
            )
        )[0]
        crosses = np.delete(crosses, np.nonzero(np.isin(crosses, bad_ant_inds))[0])
        # correct crosses
        if cheby_approx:
            history_add_string += " Used Van Vleck Chebychev approximation."
            # load in interpolation files
            with h5py.File(DATA_PATH + "/mwa_config_data/Chebychev_coeff.h5", "r") as f:
                rho_coeff = f["rho_data"][:]
            with h5py.File(DATA_PATH + "/mwa_config_data/sigma1.h5", "r") as f:
                sig_vec = f["sig_data"][:]
            sigs = self.data_array.real[autos[:, np.newaxis], :, pols]
            # find sigmas within interpolation range
            in_inds = np.logical_and(sigs > 0.9, sigs <= 4.5)
            # get indices and distances for bilinear interpolation
            sv_inds_right = np.zeros(in_inds.shape, dtype=np.int64)
            ds = np.zeros(in_inds.shape)
            sv_inds_right[in_inds] = np.searchsorted(sig_vec, sigs[in_inds])
            ds[in_inds] = sig_vec[sv_inds_right[in_inds]] - sigs[in_inds]
            # get indices for sigmas corresponding to crosses
            sig1_inds = ant_1_inds[crosses]
            sig2_inds = ant_2_inds[crosses]
            # iterate over polarization
            pol_dict = {
                yy: [(yy, yy), (0, 0)],
                yx: [(yy, xx), (0, 1)],
                xy: [(xx, yy), (1, 0)],
                xx: [(xx, xx), (1, 1)],
            }
            for i in [xx, yy, xy, yx]:
                (pol1, pol2) = pol_dict[i][1]
                (sig1_pol, sig2_pol) = pol_dict[i][0]
                # broadcast in_inds
                broad_inds = np.logical_and(
                    in_inds[sig1_inds, pol1, :], in_inds[sig2_inds, pol2, :]
                )
                # broadcast indices and distances for bilinear interpolation
                sv_inds_right1 = sv_inds_right[sig1_inds, pol1, :][broad_inds]
                sv_inds_right2 = sv_inds_right[sig2_inds, pol2, :][broad_inds]
                ds1 = ds[sig1_inds, pol1, :][broad_inds]
                ds2 = ds[sig2_inds, pol2, :][broad_inds]
                self.data_array[crosses, :, i] = van_vleck_crosses_cheby(
                    self.data_array[crosses, :, i],
                    self.data_array.real[autos[sig1_inds], :, sig1_pol],
                    self.data_array.real[autos[sig2_inds], :, sig2_pol],
                    broad_inds,
                    rho_coeff,
                    sv_inds_right1,
                    sv_inds_right2,
                    ds1,
                    ds2,
                    cheby_approx,
                )
            # correct yx autos
            sig_inds = ant_1_inds[good_autos]
            broad_inds = np.logical_and(
                in_inds[sig_inds, 0, :], in_inds[sig_inds, 1, :]
            )
            sv_inds_right1 = sv_inds_right[sig_inds, 0, :][broad_inds]
            sv_inds_right2 = sv_inds_right[sig_inds, 1, :][broad_inds]
            ds1 = ds[sig_inds, 0, :][broad_inds]
            ds2 = ds[sig_inds, 1, :][broad_inds]
            self.data_array[good_autos, :, yx] = van_vleck_crosses_cheby(
                self.data_array[good_autos, :, yx],
                self.data_array.real[good_autos, :, yy],
                self.data_array.real[good_autos, :, xx],
                broad_inds,
                rho_coeff,
                sv_inds_right1,
                sv_inds_right2,
                ds1,
                ds2,
                cheby_approx,
            )
            # add back in frequency axis
            self.data_array = self.data_array.reshape(
                (self.Nbls, self.Ntimes, self.Nfreqs, self.Npols)
            )
        # solve integral directly
        else:
            # add back in frequency axis
            self.data_array = self.data_array.reshape(
                (self.Nbls, self.Ntimes, self.Nfreqs, self.Npols)
            )
            for k in crosses:
                auto1 = autos[ant_1_inds[k]]
                auto2 = autos[ant_2_inds[k]]
                for j in range(self.Nfreqs):
                    # get data
                    sig1 = self.data_array.real[
                        auto1, :, j, np.array([yy, yy, xx, xx])
                    ].flatten()
                    sig2 = self.data_array.real[
                        auto2, :, j, np.array([yy, xx, yy, xx])
                    ].flatten()
                    khat = self.data_array[
                        k, :, j, np.array([yy, yx, xy, xx])
                    ].flatten()
                    # correct real
                    kap = van_vleck_crosses_int(khat.real, sig1, sig2, cheby_approx)
                    self.data_array.real[
                        k, :, j, np.array([yy, yx, xy, xx])
                    ] = kap.reshape(self.Npols, self.Ntimes)
                    # correct imaginary
                    kap = van_vleck_crosses_int(khat.imag, sig1, sig2, cheby_approx)
                    self.data_array.imag[
                        k, :, j, np.array([yy, yx, xy, xx])
                    ] = kap.reshape(self.Npols, self.Ntimes)
            # correct yx autos
            for k in good_autos:
                for j in range(self.Nfreqs):
                    # get data
                    sig1 = self.data_array.real[k, :, j, yy]
                    sig2 = self.data_array.real[k, :, j, xx]
                    khat = self.data_array[k, :, j, yx]
                    # correct real
                    kap = van_vleck_crosses_int(khat.real, sig1, sig2, cheby_approx)
                    self.data_array.real[k, :, j, yx] = kap
                    # correct imaginary
                    kap = van_vleck_crosses_int(khat.imag, sig1, sig2, cheby_approx)
                    self.data_array.imag[k, :, j, yx] = kap
        # correct xy autos
        self.data_array[good_autos, :, :, xy] = np.conj(
            self.data_array[good_autos, :, :, yx]
        )
        # square autos
        self.data_array.real[auto_inds, :, :, pols] = (
            self.data_array.real[auto_inds, :, :, pols] ** 2
        )
        # reshape to (nblts, nfreqs, npols)
        self.data_array = np.swapaxes(self.data_array, 0, 1)
        self.data_array = self.data_array.reshape(self.Nblts, self.Nfreqs, self.Npols)
        # rescale the data
        self.data_array *= nsamples
        # return data array to desired precision
        if self.data_array.dtype != data_array_dtype:
            self.data_array = self.data_array.astype(data_array_dtype)
        self.history += history_add_string

    def _flag_small_auto_ants(
        self, nsamples, flag_small_auto_ants, ant_1_inds, ant_2_inds, flagged_ant_inds
    ):
        """
        Find and flag autocorrelations below a threshold.

        Specifically, look for autocorrelations < 0.5 * channel_width * int_time,
        as these have been found by the Van Vleck correction to indicate bad data.
        If flag_small_auto_ants is True, then antennas with autos below the
        threshold will be flagged completely. Otherwise, antennas will be flagged
        at only the times and frequencies at which their autos are below the threshold.

        Parameters
        ----------
        nsamples : int
            Twice the numkber of electric field samples in an autocorrelation; equal
            to 2 * channel_width * int_time. The auto divided by nsamples is equal to
            the expectation value of the electric field samples squared.
        flag_small_auto_ants : bool
            Keyword option to flag antenna entirely or only at specific times and
            frequencies.
        ant_1_inds : numpy array of type int
            Indices of antenna 1 corresponding the the baseline-time axis.
        ant_2_inds : numpy array of type int
            Indices of antenna 2 corresponding the the baseline-time axis.
        flagged_ant_inds : numpy array of type int
            List of indices of flagged antennas.

        Returns
        -------
        flagged_ant_inds : numpy array of type int
            Updated list of indices of flagged antennas.

        """
        # calculate threshold so that average cross multiply = 0.25
        threshold = 0.25 * nsamples
        # look for small autos and flag
        auto_inds = self.ant_1_array == self.ant_2_array
        autos = self.data_array.real[auto_inds, :, 0:2]
        # auto_flags = self.flag_array[auto_inds, :, 0:2]
        autos = autos.reshape(self.Ntimes, self.Nants_data, self.Nfreqs, 2)
        # find autos below threshold
        small_auto_flags = np.logical_and(autos != 0, autos <= threshold)
        if flag_small_auto_ants:
            # find antenna indices for small sig ants and add to flagged_ant_inds
            ant_inds = np.unique(np.nonzero(small_auto_flags)[1])
            ant_inds = ant_inds[~np.in1d(ant_inds, flagged_ant_inds)]
            if len(ant_inds) != 0:
                self.history += (
                    " The following antennas were flagged by the Van Vleck correction: "
                    + str(ant_inds)
                    + "."
                )
                flagged_ant_inds = np.concatenate((flagged_ant_inds, ant_inds))
        else:
            # get flags for small auto ants and add to flag array
            small_auto_flags = np.logical_or(
                small_auto_flags[:, :, :, 0], small_auto_flags[:, :, :, 1]
            )
            # broadcast autos flags to corresponding crosses
            small_auto_flags = np.logical_or(
                small_auto_flags[:, ant_1_inds[: self.Nbls], :],
                small_auto_flags[:, ant_2_inds[: self.Nbls], :],
            )
            small_auto_flags = small_auto_flags.reshape(self.Nblts, self.Nfreqs)
            self.flag_array = np.logical_or(
                self.flag_array, small_auto_flags[:, :, np.newaxis]
            )
        return flagged_ant_inds

    def _get_pfb_shape(self, avg_factor, mwax):
        """
        Get pfb shape from file and apply appropriate averaging.

        Parameters
        ----------
        avg_factor : int
            Factor by which frequency channels have been averaged.

        Returns
        -------
        cb_array : numpy array of type float
            Array corresponding to pfb shape for a coarse band.
        """
        if mwax:
            if self.channel_width[0] == 40000:
                with h5py.File(
                    DATA_PATH + "/mwa_config_data/mwax_pfb_bandpass_40kHz.h5", "r"
                ) as f:
                    cb_array = f["coarse_band"][:]
            elif self.channel_width[0] == 80000:
                with h5py.File(
                    DATA_PATH + "/mwa_config_data/mwax_pfb_bandpass_80kHz.h5", "r"
                ) as f:
                    cb_array = f["coarse_band"][:]
            else:
                raise ValueError(
                    "mwax passband shapes are only available for 40 kHz and 80 kHz"
                    "fine channel widths. To request a passband for a different fine "
                    "channel width create an issue on the pyuvdata repository. To run "
                    "without a passband, resubmit with correct_coarse_band=False"
                )
        else:
            with h5py.File(
                DATA_PATH + "/mwa_config_data/MWA_rev_cb_10khz_doubles.h5", "r"
            ) as f:
                cb = f["coarse_band"][:]
            cb_array = cb.reshape(int(128 / avg_factor), int(avg_factor))
            cb_array = np.average(cb_array, axis=1)

        return cb_array

    def _correct_coarse_band(
        self,
        cb_num,
        ant_1_inds,
        ant_2_inds,
        cb_array,
        dig_gains,
        nsamples,
        num_fine_chans,
        correct_van_vleck,
        remove_coarse_band,
        remove_dig_gains,
    ):
        """
        Apply pfb, digital gain, and Van Vleck corrections to a coarse band.

        Parameters
        ----------
        cb_num : int
            Index of coarse band.
        ant_1_inds : numpy array of type int
            Indices of antenna 1 corresponding the the baseline-time axis.
        ant_2_inds : numpy array of type int
            Indices of antenna 2 corresponding the the baseline-time axis.
        cb_array : numpy array of type float
            Array corresponding to pfb shape for a coarse band.
        dig_gains : numpy array of type float
            Array corresponding to digital gains for each antenna and coarse band.
        nsamples : int
            Twice the numkber of electric field samples in an autocorrelation; equal
            to 2 * channel_width * int_time. The auto divided by nsamples is equal to
            the expectation value of the electric field sample squared.
        num_fine_chans : int
            Number of fine channels in each data file.
        correct_van_vleck : bool
            Option to apply Van Vleck correction to data.
        remove_coarse_band : bool
            Option to remove pfb coarse band shape from data.
        remove_dig_gains : bool
            Option to remove digital gains from data.

        """
        # get coarse band data as np.complex128
        cb_data = self.data_array[
            :, cb_num * num_fine_chans : (cb_num + 1) * num_fine_chans, :
        ].astype(np.complex128)
        # remove digital gains
        if remove_dig_gains:
            dig_gains1 = dig_gains[ant_1_inds, cb_num, np.newaxis, np.newaxis]
            dig_gains2 = dig_gains[ant_2_inds, cb_num, np.newaxis, np.newaxis]
            cb_data /= dig_gains1
            cb_data /= dig_gains2
        # remove coarse band
        if remove_coarse_band:
            cb_data /= cb_array[:num_fine_chans, np.newaxis]
        # put corrected data back into data array
        self.data_array[
            :, cb_num * num_fine_chans : (cb_num + 1) * num_fine_chans, :
        ] = cb_data

    def _apply_corrections(
        self,
        mwax,
        ant_1_inds,
        ant_2_inds,
        avg_factor,
        dig_gains,
        spw_inds,
        num_fine_chans,
        flagged_ant_inds,
        cheby_approx,
        data_array_dtype,
        flag_small_auto_ants,
        correct_van_vleck,
        remove_coarse_band,
        remove_dig_gains,
    ):
        """
        Prepare and apply pfb, digital gain, and Van Vleck corrections.

        Parameters
        ----------
        ant_1_inds : numpy array of type int
            Indices of antenna 1 corresponding the the baseline-time axis.
        ant_2_inds : numpy array of type int
            Indices of antenna 2 corresponding the the baseline-time axis.
        avg_factor : int
            Factor by which frequency channels have been averaged.
        dig_gains : array
            Array of digital gains with shape (Nants, Ncoarse_chans).
        spw_inds : array of type int
            Array of coarse band numbers.
        num_fine_chans : int
            Number of fine channels in each data file.
        flagged_ant_inds : numpy array of type int
            List of indices of flagged antennas.
        cheby_approx : bool
            Option to use chebyshev approximation for Van Vleck correction.
        data_array_dtype : numpy dtype
            Datatype to store the output data_array as.
        flag_small_auto_ants : bool
            Option to completely flag antennas found by _flag_small_auto_ants.
        correct_van_vleck : bool
            Option to apply Van Vleck correction to data.
        remove_coarse_band : bool
            Option to remove pfb coarse band shape from data.
        remove_dig_gains : bool
            Option to remove digital gains from data.

        Returns
        -------
        flagged_ant_inds : numpy array of type int
            Updated list of indices of flagged antennas

        """
        # get nsamples and check for small auto ants
        if correct_van_vleck:
            self.history += " Applied Van Vleck correction."
            # calculate number of samples going into real or imaginary part
            # factor of two comes from variables being circularly-symmetric
            nsamples = self.channel_width[0] * self.integration_time[0] * 2
            # look for small auto data and flag
            flagged_ant_inds = self._flag_small_auto_ants(
                nsamples, flag_small_auto_ants, ant_1_inds, ant_2_inds, flagged_ant_inds
            )
        else:
            nsamples = None
        # get digital gains
        if remove_dig_gains:
            self.history += " Divided out digital gains."
            # get gains for included coarse channels
            # During commissioning a shift in the bit selection in the digital
            # receiver was implemented which changed the data scaling by
            # a factor of 64. To be compatible with the earlier scaling scheme,
            # the digital gains are divided by a factor of 64 here.
            # For a more detailed explanation, see PR #908.
            dig_gains = dig_gains[:, spw_inds] / 64
        else:
            dig_gains = None
        # get pfb response shape
        if remove_coarse_band:
            self.history += " Divided out pfb coarse channel bandpass."
            cb_array = self._get_pfb_shape(avg_factor, mwax)
        else:
            cb_array = None

        # apply corrections to each coarse band
        for i in range(len(spw_inds)):
            self._correct_coarse_band(
                i,
                ant_1_inds,
                ant_2_inds,
                cb_array,
                dig_gains,
                nsamples,
                num_fine_chans,
                correct_van_vleck,
                remove_coarse_band,
                remove_dig_gains,
            )

        return flagged_ant_inds

    def read_mwa_corr_fits(
        self,
        filelist,
        use_aoflagger_flags=None,
        remove_dig_gains=True,
        remove_coarse_band=True,
        correct_cable_len=None,
        correct_van_vleck=False,
        cheby_approx=True,
        flag_small_auto_ants=True,
        phase_to_pointing_center=False,
        propagate_coarse_flags=True,
        flag_init=True,
        edge_width=80e3,
        start_flag="goodtime",
        end_flag=0.0,
        flag_dc_offset=True,
        remove_flagged_ants=True,
        background_lsts=True,
        read_data=True,
        data_array_dtype=np.complex64,
        nsample_array_dtype=np.float32,
        run_check=True,
        check_extra=True,
        run_check_acceptability=True,
        strict_uvw_antpos_check=False,
        check_autos=True,
        fix_autos=True,
        use_future_array_shapes=False,
    ):
        """
        Read in MWA correlator gpu box files.

        The default settings remove some of the instrumental effects in the bandpass
        by dividing out the coarse band shape (for legacy data only) and the digital
        gains, and applying a cable length correction.
        If the desired output is raw correlator data, set remove_dig_gains=False,
        remove_coarse_band=False, correct_cable_len=False, and
        phase_to_pointing_center=False.

        Parameters
        ----------
        filelist : list of str
            The list of MWA correlator files to read from. Must include at
            least one fits file and only one metafits file per data set.
            Can also be a list of lists to read multiple data sets.
        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: 'blt', 'freq', 'polarization'. Only used if
            multiple files are passed.
        use_aoflagger_flags : bool
            Option to use aoflagger mwaf flag files. Defaults to true if aoflagger
            flag files are submitted.
        remove_dig_gains : bool
            Option to divide out digital gains.
        remove_coarse_band : bool
            Option to divide out coarse band shape.
        correct_cable_len : bool
            Option to apply a cable delay correction.
        correct_van_vleck : bool
            Option to apply a van vleck correction.
        cheby_approx : bool
            Only used if correct_van_vleck is True. Option to implement the van
            vleck correction with a chebyshev polynomial approximation.
        flag_small_auto_ants : bool
            Only used if correct_van_vleck is True. Option to completely flag any
            antenna for which the autocorrelation falls below a threshold found by
            the Van Vleck correction to indicate bad data. Specifically, the
            threshold used is 0.5 * integration_time * channel_width. If set to False,
            only the times and frequencies at which the auto is below the
            threshold will be flagged for the antenna.
        phase_to_pointing_center : bool
            Option to phase to the observation pointing center.
        propagate_coarse_flags : bool
            Option to propagate flags for missing coarse channel integrations
            across frequency.
        flag_init: bool
            Set to True in order to do routine flagging of coarse channel edges,
            start or end integrations, or the center fine channel of each coarse
            channel. See associated keywords.
        edge_width: float
            Only used if flag_init is True. The width to flag on the edge of
            each coarse channel, in hz. Errors if not equal to integer multiple
            of channel_width. Set to 0 for no edge flagging.
        start_flag: float or str
            Only used if flag_init is True. The number of seconds to flag at the
            beginning of the observation. Set to 0 for no flagging. Default is
            'goodtime', which uses information in the metafits file to determine
            the length of time that should be flagged. Errors if input is not a
            float or 'goodtime'. Errors if float input is not equal to an
            integer multiple of the integration time.
        end_flag: floats
            Only used if flag_init is True. The number of seconds to flag at the
            end of the observation. Set to 0 for no flagging. Errors if not
            equal to an integer multiple of the integration time.
        flag_dc_offset: bool
            Only used if flag_init is True. Set to True to flag the center fine
            channel of each coarse channel.
        remove_flagged_ants : bool
            Option to perform a select to remove antennas flagged in the metafits
            file. If correct_van_vleck and flag_small_auto_ants are both True then
            antennas flagged by the Van Vleck correction are also removed.
        background_lsts : bool
            When set to True, the lst_array is calculated in a background thread.
        read_data : bool
            Read in the visibility, nsample 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.
        data_array_dtype : numpy dtype
            Datatype to store the output data_array as. Must be either
            np.complex64 (single-precision real and imaginary) or np.complex128
            (double-precision real and imaginary).
        nsample_array_dtype : numpy dtype
            Datatype to store the output nsample_array as. Must be either
            np.float64 (double-precision), np.float32 (single-precision), or
            np.float16 (half-precision). Half-precision is only recommended for
            cases where no sampling or averaging of baselines will occur,
            because round-off errors can be quite large (~1e-3).
        run_check : bool
            Option to check for the existence and proper shapes of parameters
            after after reading in the file (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
            reading in the file (the default is True, meaning the acceptable
            range check will be done).
        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 True.
        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.

        Raises
        ------
        ValueError
            If required files are missing or multiple files metafits files are
            included in filelist.
            If files from different observations are included in filelist.
            If files in fileslist have different fine channel widths
            If file types other than fits, metafits, and mwaf files are included
            in filelist.

        """
        metafits_file = None
        ppds_file = None
        obs_id = None
        file_dict = {}
        start_time = 0.0
        end_time = 0.0
        included_file_nums = []
        included_flag_nums = []
        aoflagger_warning = False
        num_fine_chans = 0
        mwax = None

        # do datatype checks
        if data_array_dtype not in (np.complex64, np.complex128):
            raise ValueError("data_array_dtype must be np.complex64 or np.complex128")
        if nsample_array_dtype not in (np.float64, np.float32, np.float16):
            raise ValueError(
                "nsample_array_dtype must be one of: np.float64, np.float32, np.float16"
            )
        # do start_flag check
        if not isinstance(start_flag, (int, float)):
            if start_flag != "goodtime":
                raise ValueError("start_flag must be int or float or 'goodtime'")

        # set future array shapes
        self._set_future_array_shapes()

        # iterate through files and organize
        # create a list of included file numbers
        # find the first and last times that have data
        for filename in filelist:
            # update filename attribute
            basename = os.path.basename(filename)
            self.filename = uvutils._combine_filenames(self.filename, [basename])
            self._filename.form = (len(self.filename),)

            if filename.lower().endswith(".metafits"):
                # force only one metafits file
                if metafits_file is not None:
                    raise ValueError("multiple metafits files in filelist")
                metafits_file = filename
            elif filename.lower().endswith(".fits"):
                with fits.open(filename, memmap=True) as hdu_list:
                    hdunames = uvutils._fits_indexhdus(hdu_list)
                    if "PPDS" in hdunames.keys():
                        ppds_file = filename
                        ppd_meta_header = hdu_list[0].header
                        ppd_extra_keywords = uvutils._get_fits_extra_keywords(
                            ppd_meta_header,
                            keywords_to_skip=["DATE-OBS", "TELESCOP", "INSTRUME"],
                        )
                    else:
                        # check obsid
                        head0 = hdu_list[0].header
                        if obs_id is None:
                            obs_id = head0["OBSID"]
                        else:
                            if head0["OBSID"] != obs_id:
                                raise ValueError(
                                    "files from different observations submitted "
                                    "in same list"
                                )
                        # check if mwax
                        if mwax is None:
                            if "CORR_VER" in head0.keys():
                                mwax = True
                                # save mwax version #s into extra_keywords
                                self.extra_keywords["U2S_VER"] = head0["U2S_VER"]
                                self.extra_keywords["CBF_VER"] = head0["CBF_VER"]
                                self.extra_keywords["DB2F_VER"] = head0["DB2F_VER"]
                            else:
                                mwax = False
                        # check headers for first and last times containing data
                        headstart = hdu_list[1].header
                        headfin = hdu_list[-1].header
                        first_time = headstart["TIME"] + headstart["MILLITIM"] / 1000.0
                        last_time = headfin["TIME"] + headfin["MILLITIM"] / 1000.0
                        if start_time == 0.0:
                            start_time = first_time
                        # check that files with a timing offset can be aligned
                        elif np.abs(start_time - first_time) % head0["INTTIME"] != 0.0:
                            raise ValueError(
                                "coarse channel start times are misaligned by an "
                                "amount =that is not an integer multiple of the "
                                "integration time"
                            )
                        elif start_time > first_time:
                            start_time = first_time
                        if end_time < last_time:
                            end_time = last_time
                        # get number of fine channels in each coarse channel
                        if num_fine_chans == 0:
                            if mwax:
                                # number of fine channels is multiplied by 4 (pols)
                                # and by 2 (real and imaginary parts)
                                num_fine_chans = int(headstart["NAXIS1"] / 8)
                            else:
                                num_fine_chans = headstart["NAXIS2"]
                        else:
                            if mwax:
                                if num_fine_chans != int(headstart["NAXIS1"] / 8):
                                    raise ValueError(
                                        "files submitted have different numbers of "
                                        "fine channels"
                                    )
                            else:
                                if num_fine_chans != headstart["NAXIS2"]:
                                    raise ValueError(
                                        "files submitted have different numbers of "
                                        "fine channels"
                                    )

                        # get the file number from the file name;
                        # this will later be mapped to a coarse channel
                        if mwax:
                            file_num = int(filename.split("_")[-2][-3:])
                        else:
                            file_num = int(filename.split("_")[-2][-2:])
                        if file_num not in included_file_nums:
                            included_file_nums.append(file_num)
                        # organize files
                        if "data" not in file_dict.keys():
                            file_dict["data"] = [filename]
                        else:
                            file_dict["data"].append(filename)

                        # save bscale keyword
                        # look for bscale in the first hdu, as some data does not
                        # record it in the zeroth hdu
                        if not mwax:
                            if "SCALEFAC" not in self.extra_keywords.keys():
                                if "BSCALE" in headstart.keys():
                                    self.extra_keywords["SCALEFAC"] = headstart[
                                        "BSCALE"
                                    ]
                                else:
                                    # correlator did a divide by 4 before october 2014
                                    self.extra_keywords["SCALEFAC"] = 0.25

            # look for flag files
            elif filename.lower().endswith(".mwaf"):
                if use_aoflagger_flags is None:
                    use_aoflagger_flags = True
                flag_num = int(filename.split("_")[-1][0:2])
                included_flag_nums.append(flag_num)
                if use_aoflagger_flags is False and aoflagger_warning is False:
                    warnings.warn("mwaf files submitted with use_aoflagger_flags=False")
                    aoflagger_warning = True
                elif "flags" not in file_dict.keys():
                    file_dict["flags"] = [filename]
                else:
                    file_dict["flags"].append(filename)
            else:
                raise ValueError("only fits, metafits, and mwaf files supported")

        # checks:
        if metafits_file is None and ppds_file is None:
            raise ValueError("no metafits file submitted")
        elif metafits_file is None:
            metafits_file = ppds_file
        if "data" not in file_dict.keys():
            raise ValueError("no data files submitted")
        if "flags" not in file_dict.keys() and use_aoflagger_flags:
            raise ValueError(
                "no flag files submitted. Rerun with flag files or "
                "use_aoflagger_flags=False"
            )

        # reorder file numbers
        included_file_nums = sorted(included_file_nums)
        included_flag_nums = sorted(included_flag_nums)

        # first set parameters that are always true
        self.Nspws = 1
        self.spw_array = np.array([0])
        self.vis_units = "uncalib"
        self.Npols = 4
        self.xorientation = "east"

        # get information from metafits file
        with fits.open(metafits_file, memmap=True) as meta:
            meta_hdr = meta[0].header

            # get a list of coarse channels
            coarse_chans = meta_hdr["CHANNELS"].split(",")
            coarse_chans = np.array(sorted(int(i) for i in coarse_chans))
            # fine channel width
            channel_width = float(meta_hdr.pop("FINECHAN") * 1000)
            # number of fine channels in observation
            obs_num_fine_chans = meta_hdr["NCHANS"]
            # calculate number of fine channels per coarse channel
            coarse_num_fine_chans = obs_num_fine_chans / len(coarse_chans)

            # center frequency of first fine channel of center coarse channel in hertz
            # For the legacy correlator, the metafits file includes the observation
            # frequency center, which is the center frequency of the first fine
            # channel of the center coarse channel. (If there are an even number of
            # coarse channels, the center channel is to the right).
            # For mwax, the center frequency of the first fine channel of a coarse
            # channel is the leftmost edge of the coarse channel if the number of
            # fine channels per coarse channel is even. Otherwise it is offset by
            # half of the fine channel width.
            if mwax:
                # calculate coarse channel width in MHz
                coarse_chan_width = meta_hdr["BANDWDTH"] / len(coarse_chans)
                # coarse channel center freq is channel number * coarse channel width
                center_coarse_chan_center = (
                    meta_hdr["CENTCHAN"] * coarse_chan_width * 1e6
                )
                # calculate center of first fine channel; this works if the number of
                # fine channels is even or odd
                obs_freq_center = (
                    center_coarse_chan_center
                    - int(coarse_num_fine_chans / 2) * channel_width
                )
            else:
                obs_freq_center = meta_hdr["FREQCENT"] * 1e6

            # frequency averaging factor
            avg_factor = meta_hdr["NAV_FREQ"]

            # integration time in seconds
            int_time = meta_hdr["INTTIME"]

            # pointing center in degrees
            ra_deg = meta_hdr["RA"]
            dec_deg = meta_hdr["DEC"]
            ra_rad = np.pi * ra_deg / 180
            dec_rad = np.pi * dec_deg / 180

            # set start_flag with goodtime
            if flag_init and start_flag == "goodtime":
                # ppds file does not contain this key
                if "GOODTIME" not in meta_hdr:
                    raise ValueError(
                        "To use start_flag='goodtime', a .metafits file must be "
                        "submitted"
                    )
                if meta_hdr["GOODTIME"] > start_time:
                    start_flag = meta_hdr["GOODTIME"] - start_time
                    # round start_flag up to nearest multiple of int_time
                    if start_flag % int_time > 0:
                        start_flag = (1 + int(start_flag / int_time)) * int_time
                else:
                    start_flag = 0.0

            if "HISTORY" in meta_hdr:
                self.history = str(meta_hdr["HISTORY"])
                meta_hdr.remove("HISTORY", remove_all=True)
            else:
                self.history = ""
            if not uvutils._check_history_version(
                self.history, self.pyuvdata_version_str
            ):
                self.history += self.pyuvdata_version_str
            self.instrument = meta_hdr["TELESCOP"]
            self.telescope_name = meta_hdr.pop("TELESCOP")
            self.Nants_telescope = int(meta_hdr.pop("INSTRUME")[0:3])
            object_name = meta_hdr.pop("FILENAME")

            # if not mwax, remove mwax-specific keys
            mwax_keys_to_skip = []
            if not mwax:
                mwax_keys_to_skip = [
                    "DELAYMOD",
                    "DELDESC",
                    "CABLEDEL",
                    "GEODEL",
                    "CALIBDEL",
                ]
            # store remaining keys in extra keywords
            meta_extra_keywords = uvutils._get_fits_extra_keywords(
                meta_hdr, keywords_to_skip=["DATE-OBS"] + mwax_keys_to_skip
            )
            for key, value in meta_extra_keywords.items():
                self.extra_keywords[key] = value
            if ppds_file is not None:
                # get any unique ones from ppd file
                for key, value in ppd_extra_keywords.items():
                    if key not in self.extra_keywords.keys():
                        self.extra_keywords[key] = value
            # get antenna data from metafits file table
            meta_tbl = meta[1].data

            # because of polarization, each antenna # is listed twice
            antenna_inds = meta_tbl["Antenna"][1::2]
            antenna_numbers = meta_tbl["Tile"][1::2]
            antenna_names = meta_tbl["TileName"][1::2]
            flagged_ant_inds = antenna_inds[meta_tbl["Flag"][1::2] == 1]
            cable_lens = np.asarray(meta_tbl["Length"][1::2]).astype(np.str_)
            dig_gains = meta_tbl["Gains"][1::2, :].astype(np.float64)

            # get antenna postions in enu coordinates
            antenna_positions = np.zeros((len(antenna_numbers), 3))
            antenna_positions[:, 0] = meta_tbl["East"][1::2]
            antenna_positions[:, 1] = meta_tbl["North"][1::2]
            antenna_positions[:, 2] = meta_tbl["Height"][1::2]

        # reorder antenna parameters from metafits ordering
        reordered_inds = antenna_inds.argsort()
        self.antenna_numbers = antenna_numbers[reordered_inds]
        self.antenna_names = list(antenna_names[reordered_inds])
        antenna_positions = antenna_positions[reordered_inds, :]
        cable_lens = cable_lens[reordered_inds]
        dig_gains = dig_gains[reordered_inds, :]

        # set parameters from other parameters
        self.Nants_data = len(self.antenna_numbers)
        self.Nbls = int(
            len(self.antenna_numbers) * (len(self.antenna_numbers) + 1) / 2.0
        )
        if phase_to_pointing_center:
            # use another name to prevent name collision in phase call below
            cat_name = "unprojected"
        else:
            cat_name = object_name
        cat_id = self._add_phase_center(cat_name=cat_name, cat_type="unprojected")

        # get telescope parameters
        self.set_telescope_params(warn=False)

        # build time array of centers
        time_array = np.arange(
            start_time + int_time / 2.0, end_time + int_time / 2.0 + int_time, int_time
        )

        # convert to time to jd floats
        float_time_array = Time(time_array, format="unix", scale="utc").jd.astype(float)
        # build into time array
        self.time_array = np.repeat(float_time_array, self.Nbls)

        self.Ntimes = len(time_array)

        self.Nblts = int(self.Nbls * self.Ntimes)
        self.phase_center_id_array = np.zeros(self.Nblts, dtype=int) + cat_id

        # convert times to lst
        proc = self.set_lsts_from_time_array(background=background_lsts)

        self.integration_time = np.full((self.Nblts), int_time)

        # convert antenna positions from enu to ecef
        # antenna positions are "relative to
        # the centre of the array in local topocentric \"east\", \"north\",
        # \"height\". Units are meters."
        antenna_positions_ecef = uvutils.ECEF_from_ENU(
            antenna_positions, *self.telescope_location_lat_lon_alt
        )
        # make antenna positions relative to telescope location
        self.antenna_positions = antenna_positions_ecef - self.telescope_location

        # make initial antenna arrays, where ant_1 <= ant_2
        # itertools.combinations_with_replacement returns
        # all pairs in the range 0...Nants_telescope
        # including pairs with the same number (e.g. (0,0) auto-correlation).
        # this is a little faster than having nested for-loops moving over the
        # upper triangle of antenna-pair combinations matrix.
        ant_1_array, ant_2_array = np.transpose(
            list(itertools.combinations_with_replacement(self.antenna_numbers, 2))
        )

        self.ant_1_array = np.tile(np.array(ant_1_array), self.Ntimes)
        self.ant_2_array = np.tile(np.array(ant_2_array), self.Ntimes)

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

        # make antenna index arrays
        ant_1_inds, ant_2_inds = np.transpose(
            list(itertools.combinations_with_replacement(np.arange(self.Nants_data), 2))
        )
        ant_1_inds = np.tile(np.array(ant_1_inds), self.Ntimes).astype(np.int_)
        ant_2_inds = np.tile(np.array(ant_2_inds), self.Ntimes).astype(np.int_)

        if not mwax:
            # coarse channel mapping for the legacy correlator:
            # channels in group 0-128 are assigned to files in order;
            # channels in group 129-155 are assigned in reverse order
            # that is, if the lowest channel is 127, it will be assigned to the
            # first file
            # channel 128 will be assigned to the second file
            # then the highest channel will be assigned to the third file
            # and the next hightest channel assigned to the fourth file, and so on
            mapped_coarse_chans = np.concatenate(
                (
                    coarse_chans[coarse_chans <= 128],
                    np.flip(coarse_chans[coarse_chans > 128]),
                )
            )
            ordered_file_nums = np.arange(len(coarse_chans))[
                np.argsort(mapped_coarse_chans)
            ]
            ordered_file_nums += 1
        else:
            # for mwax, the file numbers are the coarse channel numbers
            ordered_file_nums = coarse_chans
        file_mask = np.isin(ordered_file_nums, included_file_nums)
        # get included file numbers in coarse band order
        file_nums = ordered_file_nums[file_mask]
        self.Nfreqs = len(included_file_nums) * num_fine_chans

        # check that coarse channels are contiguous.
        spw_inds = np.nonzero(file_mask)[0]
        if np.any(np.diff(spw_inds) > 1):
            warnings.warn("coarse channels are not contiguous for this observation")
            # add spectral windows
            self._set_flex_spw()
            self.Nspws = len(spw_inds)
            self.spw_array = coarse_chans[spw_inds]
            self.flex_spw_id_array = np.repeat(self.spw_array, num_fine_chans)
        else:
            # future proof: always set the fles_spw_id_array
            self.flex_spw_id_array = np.full(self.Nfreqs, self.spw_array[0], dtype=int)

        # warn user if not all coarse channels are included
        if len(included_file_nums) != len(coarse_chans):
            warnings.warn("some coarse channel files were not submitted")

        # build frequency array
        self.freq_array = np.zeros(self.Nfreqs)
        self.channel_width = np.full(self.Nfreqs, channel_width)
        # Use the center frequency of the first fine channel of the center coarse
        # channel to get the frequency range for each included coarse channel.
        center_coarse_chan = int(len(coarse_chans) / 2)
        for i in range(len(spw_inds)):
            first_coarse_freq = (
                obs_freq_center
                + (spw_inds[i] - center_coarse_chan)
                * coarse_num_fine_chans
                * channel_width
            )
            last_coarse_freq = first_coarse_freq + num_fine_chans * channel_width
            self.freq_array[i * num_fine_chans : (i + 1) * num_fine_chans] = np.arange(
                first_coarse_freq, last_coarse_freq, channel_width
            )
        # for mwax, polarizations are ordered xx, xy, yx, yy
        if mwax:
            self.polarization_array = np.array([-5, -7, -8, -6])
        # otherwise, polarizations are ordered yy, yx, xy, xx
        else:
            self.polarization_array = np.array([-6, -8, -7, -5])
        # get index array for AIPS reordering
        pol_index_array = np.argsort(np.abs(self.polarization_array))
        # reorder polarization_array here to avoid memory spike from self.reorder_pols
        self.polarization_array = self.polarization_array[pol_index_array]

        if read_data:
            if not mwax:
                # build mapper from antenna numbers and polarizations to pfb inputs
                corr_ants_to_pfb_inputs = {}
                for i in range(len(antenna_inds)):
                    for p in range(2):
                        corr_ants_to_pfb_inputs[(antenna_inds[i], p)] = 2 * i + p

                # for mapping, start with a pair of antennas/polarizations
                # this is the pair we want to find the data for
                # map the pair to the corresponding coarse pfb input indices
                # map the coarse pfb input indices to the fine pfb output indices
                # these are the indices for the data corresponding to the initial
                # antenna/pol pair

                # These two 1D arrays will be both C and F contiguous
                # but we are explicitly declaring C to be consistent with the rest
                # of the python which interacts with the C/Cython code.
                # generate a mapping index array
                map_inds = np.zeros((self.Nbls * self.Npols), dtype=np.int32, order="C")
                # generate a conjugation array
                conj = np.full(
                    (self.Nbls * self.Npols), False, dtype=np.bool_, order="C"
                )

                _corr_fits.generate_map(corr_ants_to_pfb_inputs, map_inds, conj)
            else:
                map_inds = None
                conj = None
            # create arrays for data, nsamples, and flags
            self.data_array = np.zeros(
                (self.Nblts, self.Nfreqs, self.Npols), dtype=data_array_dtype
            )
            self.nsample_array = np.zeros(
                (self.Ntimes, self.Nbls, self.Nfreqs, self.Npols),
                dtype=nsample_array_dtype,
            )
            self.flag_array = np.full(
                (self.Ntimes, self.Nbls, len(spw_inds), self.Npols), True
            )

            # read data files
            for filename in file_dict["data"]:
                self._read_fits_file(
                    filename,
                    time_array,
                    file_nums,
                    num_fine_chans,
                    int_time,
                    mwax,
                    map_inds,
                    conj,
                    pol_index_array,
                )

            # propagate coarse flags
            if propagate_coarse_flags:
                self.flag_array = np.any(self.flag_array, axis=2)
                self.flag_array = np.repeat(
                    self.flag_array[:, :, np.newaxis, :], self.Nfreqs, axis=2
                )
            else:
                self.flag_array = np.repeat(self.flag_array, num_fine_chans, axis=2)

            if flag_init:
                self.flag_init(
                    num_fine_chans,
                    edge_width=edge_width,
                    start_flag=start_flag,
                    end_flag=end_flag,
                    flag_dc_offset=flag_dc_offset,
                )

            # flag bad ants
            bad_ant_inds = np.logical_or(
                np.isin(ant_1_inds[: self.Nbls], flagged_ant_inds),
                np.isin(ant_2_inds[: self.Nbls], flagged_ant_inds),
            )
            self.flag_array[:, bad_ant_inds, :, :] = True
            # reshape arrays
            self.flag_array = self.flag_array.reshape(
                (self.Nblts, self.Nfreqs, self.Npols)
            )
            self.nsample_array = self.nsample_array.reshape(
                (self.Nblts, self.Nfreqs, self.Npols)
            )

            # When MWA data is cast to float for the correlator, the division
            # by 127 introduces small errors that are mitigated when the data
            # is cast back into integer.
            # this needs to happen before the van vleck correction
            if not mwax:
                self.data_array /= self.extra_keywords["SCALEFAC"]
                np.rint(self.data_array, out=self.data_array)

            # van vleck correction
            if correct_van_vleck:
                self.van_vleck_correction(
                    ant_1_inds,
                    ant_2_inds,
                    flagged_ant_inds,
                    cheby_approx=cheby_approx,
                    data_array_dtype=data_array_dtype,
                )

            # apply corrections
            if np.any([correct_van_vleck, remove_coarse_band, remove_dig_gains]):
                flagged_ant_inds = self._apply_corrections(
                    mwax,
                    ant_1_inds,
                    ant_2_inds,
                    avg_factor,
                    dig_gains,
                    spw_inds,
                    num_fine_chans,
                    flagged_ant_inds,
                    cheby_approx=cheby_approx,
                    data_array_dtype=data_array_dtype,
                    flag_small_auto_ants=flag_small_auto_ants,
                    correct_van_vleck=correct_van_vleck,
                    remove_coarse_band=remove_coarse_band,
                    remove_dig_gains=remove_dig_gains,
                )

            # rescale data
            # this needs to happen after the van vleck correction
            if not mwax:
                self.data_array *= self.extra_keywords["SCALEFAC"]

            # cable delay corrections
            if correct_cable_len is None:
                correct_cable_len = True
                warnings.warn(
                    "cable length correction is now defaulted to True rather than "
                    "False. To read in files without applying the correction set "
                    "correct_cable_len=False. This warning will be removed in v2.4"
                )
            if correct_cable_len:
                self.correct_cable_length(cable_lens, ant_1_inds, ant_2_inds)
            # add aoflagger flags to flag_array
            if use_aoflagger_flags:
                # throw an error if matching files not submitted
                if included_file_nums != included_flag_nums:
                    raise ValueError(
                        "flag file coarse bands do not match data file coarse bands"
                    )
                warnings.warn(
                    "coarse channel, start time, and end time flagging will default "
                    "to the more aggressive of flag_init and AOFlagger"
                )
                for filename in file_dict["flags"]:
                    self._read_flag_file(filename, file_nums, num_fine_chans)

            # to account for discrepancies between file conventions, in order
            # to be consistent with the uvw vector direction, all the data must
            # be conjugated
            np.conj(self.data_array, out=self.data_array)

        # wait for LSTs if set in background
        if proc is not None:
            proc.join()

        self._set_app_coords_helper()

        # create self.uvw_array
        self.set_uvws_from_antenna_positions(allow_phasing=False)

        # remove bad antennas
        # select must be called after lst thread is re-joined
        if remove_flagged_ants:
            good_ants = np.delete(np.array(self.antenna_numbers), flagged_ant_inds)
            self.select(antenna_nums=good_ants, run_check=False)

        # phasing
        if phase_to_pointing_center:
            self.phase(
                lon=ra_rad,
                lat=dec_rad,
                epoch="J2000",
                phase_frame="fk5",
                cat_name=object_name,
            )

        # switch to current_array_shape
        if not use_future_array_shapes:
            warnings.warn(_future_array_shapes_warning, DeprecationWarning)
            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()

        # check if object is self-consistent
        # uvws are calcuated using pyuvdata, so turn off the check for speed.
        if run_check:
            self.check(
                check_extra=check_extra,
                run_check_acceptability=run_check_acceptability,
                strict_uvw_antpos_check=strict_uvw_antpos_check,
                allow_flip_conj=True,
                check_autos=check_autos,
                fix_autos=fix_autos,
            )


# pyuvdata/uvdata/uvdata.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 datasets."""
from __future__ import annotations

import copy
import os
import threading
import warnings
from collections.abc import Iterable
from typing import Literal

import astropy.units as units
import numpy as np
from astropy import constants as const
from astropy import coordinates as coord
from astropy.coordinates import FK5, Angle, EarthLocation, SkyCoord
from astropy.time import Time
from scipy import ndimage as nd

from .. import parameter as uvp
from .. import telescopes as uvtel
from .. import utils as uvutils
from ..uvbase import UVBase

__all__ = ["UVData"]
import logging

logger = logging.getLogger(__name__)

# standard angle tolerance: 1 mas in radians.
radian_tol = 1 * 2 * np.pi * 1e-3 / (60.0 * 60.0 * 360.0)

reporting_request = (
    " Please report this in our issue log, we have not been able to find a file with "
    "this feature, we would like to investigate this more."
)

_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 UVData tutorial on ReadTheDocs for more details about these "
    "shape changes."
)

old_phase_attrs = [
    "phase_type",
    "phase_center_ra",
    "phase_center_dec",
    "phase_center_frame",
    "phase_center_epoch",
    "object_name",
]


def _warn_old_phase_attr(__name):
    warn_str = (
        "The older phase attributes, including "
        + ", ".join(old_phase_attrs)
        + ", are deprecated in favor of representing phasing using the "
        "phase_center_catalog."
    )

    if __name == "phase_type":
        warn_str += (
            " The phase_type is now represented as the 'cat_type' in the "
            "phase_center_catalog (the old 'drift' type corresponds to the "
            "new 'unprojected' type, the old 'phased' type corresponds to the "
            "new 'sidereal' type, and there is also now support for an 'ephem' "
            "type to support moving objects and a new 'driftscan' type which "
            "can point at any alt/az (not just zenith) and which always has "
            "w-projection applied)."
        )
    elif __name == "phase_center_ra":
        warn_str += (
            " The phase_center_ra is now represented as the 'cat_lon' in the "
            "phase_center_catalog."
        )
    elif __name == "phase_center_dec":
        warn_str += (
            " The phase_center_dec is now represented as the 'cat_lat' in the "
            "phase_center_catalog."
        )
    elif __name == "phase_center_frame":
        warn_str += (
            " The phase_center_frame is now represented as the 'cat_frame' in "
            "the phase_center_catalog."
        )
    elif __name == "phase_center_epoch":
        warn_str += (
            " The phase_center_epoch is now represented as the 'cat_epoch' in "
            "the phase_center_catalog."
        )
    elif __name == "object_name":
        warn_str += (
            " The object_name is now represented as the 'cat_name' in "
            "the phase_center_catalog."
        )
    warn_str += (
        " To see a nice representation of the phase_center_catalog, use the "
        "`print_phase_center_info` method. The older attributes will be "
        "removed in version 3.0"
    )
    warnings.warn(warn_str, DeprecationWarning)


class UVData(UVBase):
    """
    A class for defining a radio interferometer dataset.

    Attributes
    ----------
    UVParameter objects :
        For full list see the documentation on ReadTheDocs:
        http://pyuvdata.readthedocs.io/en/latest/.
        Some are always required, some are required only for flex_spw data
        and others are always optional.

    """

    def __init__(self):
        """Create a new UVData object."""
        # add the UVParameters to the class

        self._Ntimes = uvp.UVParameter(
            "Ntimes", description="Number of times.", expected_type=int
        )
        self._Nbls = uvp.UVParameter(
            "Nbls", description="Number of baselines.", expected_type=int
        )
        desc = (
            "Number of baseline-times (i.e. number of spectra). Not necessarily "
            "equal to Nbls * Ntimes."
        )
        self._Nblts = uvp.UVParameter("Nblts", description=desc, expected_type=int)
        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 = (
            "Array of the visibility data, shape: (Nblts, 1, Nfreqs, "
            "Npols) or (Nblts, Nfreqs, Npols) if future_array_shapes=True, "
            "type = complex float, in units of self.vis_units."
        )
        # TODO: Spw axis to be collapsed in future release
        self._data_array = uvp.UVParameter(
            "data_array",
            description=desc,
            form=("Nblts", 1, "Nfreqs", "Npols"),
            expected_type=complex,
        )

        desc = 'Visibility units, options are: "uncalib", "Jy" or "K str".'
        self._vis_units = uvp.UVParameter(
            "vis_units",
            description=desc,
            form="str",
            expected_type=str,
            acceptable_vals=["uncalib", "Jy", "K str"],
        )

        desc = (
            "Number of data points averaged into each data element, "
            "NOT required to be an integer, type = float, same shape as data_array."
            "The product of the integration_time and the nsample_array "
            "value for a visibility reflects the total amount of time "
            "that went into the visibility. Best practice is for the "
            "nsample_array to be used to track flagging within an integration_time "
            "(leading to a decrease of the nsample array value below 1) and "
            "LST averaging (leading to an increase in the nsample array "
            "value). So datasets that have not been LST averaged should "
            "have nsample array values less than or equal to 1."
            "Note that many files do not follow this convention, but it is "
            "safe to assume that the product of the integration_time and "
            "the nsample_array is the total amount of time included in a visibility."
        )
        self._nsample_array = uvp.UVParameter(
            "nsample_array",
            description=desc,
            form=("Nblts", 1, "Nfreqs", "Npols"),
            expected_type=float,
        )

        desc = "Boolean flag, True is flagged, same shape as data_array."
        self._flag_array = uvp.UVParameter(
            "flag_array",
            description=desc,
            form=("Nblts", 1, "Nfreqs", "Npols"),
            expected_type=bool,
        )

        self._Nspws = uvp.UVParameter(
            "Nspws",
            description="Number of spectral windows "
            "(ie non-contiguous spectral chunks). ",
            expected_type=int,
        )

        self._spw_array = uvp.UVParameter(
            "spw_array",
            description="Array of spectral window numbers, shape (Nspws).",
            form=("Nspws",),
            expected_type=int,
        )

        desc = (
            "Projected baseline vectors relative to phase center, "
            "shape (Nblts, 3), units meters. "
            "Convention is: uvw = xyz(ant2) - xyz(ant1)."
            "Note that this is the Miriad convention but it is different "
            "from the AIPS/FITS convention (where uvw = xyz(ant1) - xyz(ant2))."
        )
        self._uvw_array = uvp.UVParameter(
            "uvw_array",
            description=desc,
            form=("Nblts", 3),
            expected_type=np.float64,
            strict_type_check=True,
            acceptable_range=(0, 1e8),
            tols=1e-3,
        )

        desc = (
            "Array of times, center of integration, shape (Nblts), units Julian Date."
        )
        self._time_array = uvp.UVParameter(
            "time_array",
            description=desc,
            form=("Nblts",),
            expected_type=np.float64,
            strict_type_check=True,
            tols=1e-3 / (60.0 * 60.0 * 24.0),
        )  # 1 ms in days

        desc = (
            "Array of local apparent sidereal times (LAST) at the center of "
            "integration, shape (Nblts), units radians."
        )
        self._lst_array = uvp.UVParameter(
            "lst_array",
            description=desc,
            form=("Nblts",),
            expected_type=np.float64,
            strict_type_check=True,
            tols=radian_tol,
        )

        desc = (
            "Array of numbers for the first antenna, which is matched to that in "
            "the antenna_numbers attribute. Shape (Nblts), type = int."
        )
        self._ant_1_array = uvp.UVParameter(
            "ant_1_array",
            description=desc,
            expected_type=int,
            form=("Nblts",),
            acceptable_range=(0, 2147483647),
        )

        desc = (
            "Array of numbers for the second antenna, which is matched to that in "
            "the antenna_numbers attribute. Shape (Nblts), type = int."
        )
        self._ant_2_array = uvp.UVParameter(
            "ant_2_array",
            description=desc,
            expected_type=int,
            form=("Nblts",),
            acceptable_range=(0, 2147483647),
        )

        desc = (
            "Array of baseline numbers, shape (Nblts), type = int; "
            "by default baseline = 2048 * ant1 + ant2 + 2^16, "
            "though other conventions are available."
        )
        self._baseline_array = uvp.UVParameter(
            "baseline_array",
            description=desc,
            expected_type=int,
            form=("Nblts",),
            acceptable_range=(0, 4611686018498691072),
        )

        # this dimensionality of freq_array does not allow for different spws
        # to have different dimensions
        desc = (
            "Array of frequencies, center of the channel, "
            "shape (1, Nfreqs) or (Nfreqs,) if future_array_shapes=True, units Hz."
        )
        # 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 = (
            "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). A value of 0 indicates that "
            "the polarization is different for different spectral windows and is only "
            "allowed if flex_spw_polarization_array is defined (not None)."
        )
        self._polarization_array = uvp.UVParameter(
            "polarization_array",
            description=desc,
            expected_type=int,
            acceptable_vals=list(np.arange(-8, 5)),
            form=("Npols",),
        )

        desc = (
            "Length of the integration in seconds, shape (Nblts). "
            "The product of the integration_time and the nsample_array "
            "value for a visibility reflects the total amount of time "
            "that went into the visibility. Best practice is for the "
            "integration_time to reflect the length of time a visibility "
            "was integrated over (so it should vary in the case of "
            "baseline-dependent averaging and be a way to do selections "
            "for differently integrated baselines)."
            "Note that many files do not follow this convention, but it is "
            "safe to assume that the product of the integration_time and "
            "the nsample_array is the total amount of time included in a visibility."
        )
        self._integration_time = uvp.UVParameter(
            "integration_time",
            description=desc,
            form=("Nblts",),
            expected_type=float,
            tols=1e-3,
        )  # 1 ms

        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."
        )
        self._channel_width = uvp.UVParameter(
            "channel_width", description=desc, expected_type=float, tols=1e-3
        )  # 1 mHz

        self._telescope_name = uvp.UVParameter(
            "telescope_name",
            description="Name of telescope or array (string).",
            form="str",
            expected_type=str,
        )

        self._instrument = uvp.UVParameter(
            "instrument",
            description="Receiver or backend. Sometimes identical to telescope_name.",
            form="str",
            expected_type=str,
        )

        desc = (
            "Telescope location: xyz position in specified frame (default ITRS). "
            "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),
            frame="itrs",
            tols=1e-3,
        )

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

        # --- 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 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 = (
            "Optional, only used if flex_spw = True. Allows for labeling individual "
            "spectral windows with different polarizations. If set, Npols must be set "
            "to 1 (i.e., only one polarization per spectral window allowed). Shape "
            "(Nspws), type = int."
        )
        self._flex_spw_polarization_array = uvp.UVParameter(
            "flex_spw_polarization_array",
            description=desc,
            form=("Nspws",),
            expected_type=int,
            acceptable_vals=list(np.arange(-8, 0)) + list(np.arange(1, 5)),
            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
        )

        # --- phasing information ---

        desc = "Specifies the number of phase centers contained within the data set."
        self._Nphase = uvp.UVParameter("Nphase", description=desc, expected_type=int)

        desc = (
            "Dictionary that acts as a catalog, containing information on individual "
            "phase centers. Keys are the catalog IDs of the different phase centers in "
            "the object (matched to the parameter `phase_center_id_array`). At a "
            "minimum, each dictionary must contain the keys: "
            "'cat_name' giving the phase center name (this does not have to be unique, "
            "non-unique values can be used to indicate sets of phase centers that make "
            "up a mosaic observation), "
            "'cat_type', which can be 'sidereal' (fixed position in RA/Dec), 'ephem' "
            "(position in RA/Dec which moves with time), 'driftscan' (fixed postion in "
            "Az/El, NOT the same as the old `phase_type`='drift') or 'unprojected' "
            "(baseline coordinates in ENU, but data are not phased, similar to "
            "the old `phase_type`='drift')"
            "'cat_lon' (longitude coord, e.g. RA, either a single value or a one "
            "dimensional array of length Npts --the number of ephemeris data points-- "
            "for ephem type phase centers), "
            "'cat_lat' (latitude coord, e.g. Dec., either a single value or a one "
            "dimensional array of length Npts --the number of ephemeris data points-- "
            "for ephem type phase centers), "
            "'cat_frame' (coordinate frame, e.g. icrs, must be a frame supported by "
            "astropy). "
            "Other optional keys include "
            "'cat_epoch' (epoch and equinox of the coordinate frame, not needed for "
            "frames without an epoch (e.g. ICRS) unless the there is proper motion), "
            "'cat_times' (times for the coordinates, only used for 'ephem' types), "
            "'cat_pm_ra' (proper motion in RA), "
            "'cat_pm_dec' (proper motion in Dec), "
            "'cat_dist' (physical distance to the source in parsec, useful if parallax "
            "is important, either a single value or a one dimensional array of length "
            "Npts --the number of ephemeris data points-- for ephem type phase "
            "centers.), "
            "'cat_vrad' (rest frame velocity in km/s, either a single value or a one "
            "dimensional array of length Npts --the number of ephemeris data points-- "
            "for ephem type phase centers.), and "
            "'info_source' (describes where catalog info came from). "
            "See the documentation of the `phase` method for more details."
        )
        self._phase_center_catalog = uvp.UVParameter(
            "phase_center_catalog", description=desc, expected_type=dict
        )

        desc = (
            "Apparent right ascension of phase center in the topocentric frame of the "
            "observatory, units radians. Shape (Nblts,), type = float."
        )
        self._phase_center_app_ra = uvp.AngleParameter(
            "phase_center_app_ra",
            form=("Nblts",),
            expected_type=float,
            description=desc,
            tols=radian_tol,
        )

        desc = (
            "Apparent Declination of phase center in the topocentric frame of the "
            "observatory, units radians. Shape (Nblts,), type = float."
        )
        self._phase_center_app_dec = uvp.AngleParameter(
            "phase_center_app_dec",
            form=("Nblts",),
            expected_type=float,
            description=desc,
            tols=radian_tol,
        )

        desc = (
            "Position angle between the hour circle (which is a great circle that goes "
            "through the target postion and both poles) in the apparent/topocentric "
            "frame, and the frame given in the phase_center_frame attribute."
            "Shape (Nblts,), type = float."
        )
        # The tolerance here is set by the fact that is is calculated using an arctan,
        # the limiting precision of which happens around values of 1.
        self._phase_center_frame_pa = uvp.AngleParameter(
            "phase_center_frame_pa",
            form=("Nblts",),
            expected_type=float,
            description=desc,
            tols=2e-8,
        )

        desc = (
            "Maps individual indices along the Nblt axis to a key in "
            "`phase_center_catalog`, which maps to a dict "
            "containing the other metadata for each phase center."
            "Shape (Nblts), type = int."
        )
        self._phase_center_id_array = uvp.UVParameter(
            "phase_center_id_array",
            description=desc,
            form=("Nblts",),
            expected_type=int,
        )

        desc = (
            "Optional when reading a MS. Retains the scan number when reading a MS."
            " Shape (Nblts), type = int."
        )
        self._scan_number_array = uvp.UVParameter(
            "scan_number_array",
            description=desc,
            form=("Nblts",),
            expected_type=int,
            required=False,
        )

        # --- antenna information ----
        desc = (
            "Number of antennas with data present (i.e. number of unique "
            "entries in ant_1_array and ant_2_array). May be smaller "
            "than the number of antennas in the array."
        )
        self._Nants_data = uvp.UVParameter(
            "Nants_data", description=desc, expected_type=int
        )

        desc = (
            "Number of antennas in the array. May be larger "
            "than the number of antennas with data."
        )
        self._Nants_telescope = uvp.UVParameter(
            "Nants_telescope", description=desc, expected_type=int
        )

        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). There must be one entry "
            "here for each unique entry in ant_1_array and "
            "ant_2_array, but there may be extras as well. "
        )
        self._antenna_names = uvp.UVParameter(
            "antenna_names",
            description=desc,
            form=("Nants_telescope",),
            expected_type=str,
        )

        desc = (
            "List of integer antenna numbers corresponding to antenna_names, "
            "shape (Nants_telescope). There must be one "
            "entry here for each unique entry in ant_1_array and "
            "ant_2_array, 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,
        )

        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
        )

        # -------- extra, non-required parameters ----------
        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"],
        )

        blt_order_options = ["time", "baseline", "ant1", "ant2", "bda"]
        desc = (
            "Ordering of the data array along the blt axis. A tuple with "
            'the major and minor order (minor order is omitted if order is "bda"). '
            "The allowed values are: "
            + " ,".join([str(val) for val in blt_order_options])
            + "."
        )
        self._blt_order = uvp.UVParameter(
            "blt_order",
            description=desc,
            form=(2,),
            required=False,
            expected_type=str,
            acceptable_vals=blt_order_options,
            ignore_eq_none=True,
        )

        desc = (
            "Whether the baseline-time axis is rectangular. If not provided, the "
            "rectangularity will be determined from the data. This is a non-negligible"
            "operation, so if you know it, it can be provided."
        )
        self._blts_are_rectangular = uvp.UVParameter(
            "blts_are_rectangular",
            description=desc,
            required=False,
            expected_type=bool,
            ignore_eq_none=True,
        )

        desc = (
            "If blts are rectangular, this variable specifies whether the time axis is"
            "the fastest-moving virtual axis. Various reading/indexing functions "
            "benefit from knowing this, so if it is known, it should be provided."
        )
        self._time_axis_faster_than_bls = uvp.UVParameter(
            "time_axis_faster_than_bls",
            description=desc,
            required=False,
            expected_type=bool,
            ignore_eq_none=True,
        )

        desc = (
            "Any user supplied extra keywords, type=dict. Keys should be "
            "8 character or less strings if writing to uvfits or miriad 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 = (
            "Array of 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,
            form=("Nants_telescope",),
            expected_type=float,
            tols=1e-3,  # 1 mm
        )

        # --- other stuff ---
        # the below are copied from AIPS memo 117, but could be revised to
        # merge with other sources of data.
        self._gst0 = uvp.UVParameter(
            "gst0",
            required=False,
            description="Greenwich sidereal time at midnight on reference date.",
            spoof_val=0.0,
            expected_type=float,
        )
        self._rdate = uvp.UVParameter(
            "rdate",
            required=False,
            description="Date for which the GST0 applies.",
            spoof_val="",
            form="str",
        )
        self._earth_omega = uvp.UVParameter(
            "earth_omega",
            required=False,
            description="Earth's rotation rate in degrees per day.",
            spoof_val=360.985,
            expected_type=float,
        )
        self._dut1 = uvp.UVParameter(
            "dut1",
            required=False,
            description="DUT1 (google it) AIPS 117 calls it UT1UTC.",
            spoof_val=0.0,
            expected_type=float,
        )
        self._timesys = uvp.UVParameter(
            "timesys",
            required=False,
            description="We only support UTC.",
            spoof_val="UTC",
            form="str",
        )

        desc = (
            "FHD thing we do not understand, something about the time "
            "at which the phase center is normal to the chosen UV plane "
            "for phasing."
        )
        self._uvplane_reference_time = uvp.UVParameter(
            "uvplane_reference_time", required=False, description=desc, spoof_val=0
        )

        desc = "Per-antenna and per-frequency equalization coefficients."
        self._eq_coeffs = uvp.UVParameter(
            "eq_coeffs",
            required=False,
            description=desc,
            form=("Nants_telescope", "Nfreqs"),
            expected_type=float,
            spoof_val=1.0,
        )

        desc = "Convention for how to remove eq_coeffs from data."
        self._eq_coeffs_convention = uvp.UVParameter(
            "eq_coeffs_convention",
            required=False,
            description=desc,
            form="str",
            spoof_val="divide",
        )

        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(UVData, 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.
        """
        # 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_scan_numbers(self, override=False):
        """
        Set scan numbers by grouping consecutive integrations on the same phase center.

        This approach mimics the definition of scan number in measurement sets and is
        especially helpful for distinguishing between repeated visits to multiple
        phase centers.

        Parameters
        ----------
        override : bool
            When True, will redefine existing scan numbers. Default is False.
        """
        if self.scan_number_array is None or override:
            slice_list = []
            # This loops over phase centers, finds contiguous integrations with
            # ndimage.label, and then finds the slices to return those contiguous
            # integrations with nd.find_objects.
            for cat_id in self.phase_center_catalog:
                slice_list.extend(
                    nd.find_objects(nd.label(self.phase_center_id_array == cat_id)[0])
                )

            # Sort by start integration number, which we can extract from
            # the start of each slice in the list.
            slice_list_ord = sorted(slice_list, key=lambda x: x[0].start)

            # Incrementally increase the scan number with each group in
            # slice_list_ord
            scan_array = np.zeros_like(self.phase_center_id_array)
            for ii, slice_scan in enumerate(slice_list_ord):
                scan_array[slice_scan] = ii + 1

            self.scan_number_array = scan_array

    def _look_for_name(self, cat_name):
        """
        Look up catalog IDs which match a given name.

        Parameters
        ----------
        cat_name : str or list of str
            Name to match against entries in phase_center_catalog.

        Returns
        -------
        cat_id_list : list
            List of all catalog IDs which match the given name.
        """
        if isinstance(cat_name, str):
            return [
                pc_id
                for pc_id, pc_dict in self.phase_center_catalog.items()
                if pc_dict["cat_name"] == cat_name
            ]
        else:
            return [
                pc_id
                for pc_id, pc_dict in self.phase_center_catalog.items()
                if pc_dict["cat_name"] in cat_name
            ]

    def _look_in_catalog(
        self,
        cat_name=None,
        cat_type=None,
        cat_lon=None,
        cat_lat=None,
        cat_frame=None,
        cat_epoch=None,
        cat_times=None,
        cat_pm_ra=None,
        cat_pm_dec=None,
        cat_dist=None,
        cat_vrad=None,
        ignore_name=False,
        target_cat_id=None,
        phase_dict=None,
    ):
        """
        Check the catalog to see if an existing entry matches provided data.

        This is a helper function for verifying if an entry already exists within
        the catalog, contained within the attribute `phase_center_catalog`.

        Parameters
        ----------
        cat_name : str
            Name of the phase center, which should match a the value of "cat_name"
            inside an entry of `phase_center_catalog`.
        cat_type : str
            Type of phase center of the entry. Must be one of:
                "sidereal" (fixed RA/Dec),
                "ephem" (RA/Dec that moves with time),
                "driftscan" (fixed az/el position),
                "unprojected" (no w-projection, equivalent to the old
                `phase_type` == "drift").
        cat_lon : float or ndarray
            Value of the longitudinal coordinate (e.g., RA, Az, l) in radians of the
            phase center. No default unless `cat_type="unprojected"`, in which case the
            default is zero. Expected to be a float for sidereal and driftscan phase
            centers, and an ndarray of floats of shape (Npts,) for ephem phase centers.
        cat_lat : float or ndarray
            Value of the latitudinal coordinate (e.g., Dec, El, b) in radians of the
            phase center. No default unless `cat_type="unprojected"`, in which case the
            default is pi/2. Expected to be a float for sidereal and driftscan phase
            centers, and an ndarray of floats of shape (Npts,) for ephem phase centers.
        cat_frame : str
            Coordinate frame that cat_lon and cat_lat are given in. Only used for
            sidereal and ephem phase centers. Can be any of the several supported frames
            in astropy (a limited list: fk4, fk5, icrs, gcrs, cirs, galactic).
        cat_epoch : str or float
            Epoch of the coordinates, only used when cat_frame = fk4 or fk5. Given
            in units 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).
        cat_times : ndarray of floats
            Only used when `cat_type="ephem"`. Describes the time for which the values
            of `cat_lon` and `cat_lat` are caclulated, in units of JD. Shape is (Npts,).
        cat_pm_ra : float
            Proper motion in RA, in units of mas/year. Only used for sidereal phase
            centers.
        cat_pm_dec : float
            Proper motion in Dec, in units of mas/year. Only used for sidereal phase
            centers.
        cat_dist : float or ndarray of float
            Distance of the source, in units of pc. Only used for sidereal and ephem
            phase centers. Expected to be a float for sidereal and driftscan phase
            centers, and an ndarray of floats of shape (Npts,) for ephem phase centers.
        cat_vrad : float or ndarray of float
            Radial velocity of the source, in units of km/s. Only used for sidereal and
            ephem phase centers. Expected to be a float for sidereal and driftscan phase
            centers, and an ndarray of floats of shape (Npts,) for ephem phase centers.
        ignore_name : bool
            Nominally, `_look_in_catalog` will only look at entries where `cat_name`
            matches the name of an entry in the catalog. However, by setting this to
            True, the method will search all entries in the catalog and see if any
            match all of the provided data (excluding `cat_name`).
        target_cat_id : int
            Optional argument to specify a particular cat_id to check against.
        phase_dict : dict
            Instead of providing individual parameters, one may provide a dict which
            matches that format used within `phase_center_catalog` for checking for
            existing entries. If used, all other parameters (save for `ignore_name`
            and `cat_name`) are disregarded.

        Returns
        -------
        cat_id : int or None
            The unique ID number for the phase center added to the internal catalog.
            This value is used in the `phase_center_id_array` attribute to denote which
            source a given baseline-time corresponds to. If no catalog entry matches,
            then None is returned.
        cat_diffs : int
            The number of differences between the information provided and the catalog
            entry contained within `phase_center_catalog`. If everything matches, then
            `cat_diffs=0`.
        """
        # 1 marcsec tols
        radian_tols = (0, radian_tol)
        default_tols = (1e-5, 1e-8)
        match_id = None
        match_diffs = 99999

        if (cat_name is None) and (not ignore_name):
            if phase_dict is None:
                raise ValueError(
                    "Must specify either phase_dict or cat_name if ignore_name=False."
                )
            cat_name = phase_dict["cat_name"]

        if cat_type == "unphased" or cat_name == "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,
            )
            cat_type = "unprojected"
            if cat_name == "unphased":
                cat_name = "unprojected"

        # Emulate the defaults that are set if None is detected for
        # unprojected and driftscan types.
        if cat_type in ["unprojected", "driftscan"]:
            if cat_lon is None:
                cat_lon = 0.0
            if cat_lat is None:
                cat_lat = np.pi / 2
            if cat_frame is None:
                cat_frame = "altaz"

        if phase_dict is None:
            phase_dict = {
                "cat_type": cat_type,
                "cat_lon": cat_lon,
                "cat_lat": cat_lat,
                "cat_frame": cat_frame,
                "cat_epoch": cat_epoch,
                "cat_times": cat_times,
                "cat_pm_ra": cat_pm_ra,
                "cat_pm_dec": cat_pm_dec,
                "cat_dist": cat_dist,
                "cat_vrad": cat_vrad,
            }

        this_dict = self.phase_center_catalog

        tol_dict = {
            "cat_type": None,
            "cat_lon": radian_tols,
            "cat_lat": radian_tols,
            "cat_frame": None,
            "cat_epoch": None,
            "cat_times": default_tols,
            "cat_pm_ra": default_tols,
            "cat_pm_dec": default_tols,
            "cat_dist": default_tols,
            "cat_vrad": default_tols,
        }

        if target_cat_id is not None:
            if target_cat_id not in self.phase_center_catalog:
                raise ValueError(f"No phase center with ID number {target_cat_id}.")
            name_dict = {
                target_cat_id: self.phase_center_catalog[target_cat_id]["cat_name"]
            }
        else:
            name_dict = {
                key: cat_dict["cat_name"]
                for key, cat_dict in self.phase_center_catalog.items()
            }

        for cat_id, name in name_dict.items():
            cat_diffs = 0
            if (cat_name != name) and (not ignore_name):
                continue
            check_dict = this_dict[cat_id]
            for key in tol_dict.keys():
                if phase_dict.get(key) is not None:
                    if check_dict.get(key) is None:
                        cat_diffs += 1
                    elif tol_dict[key] is None:
                        # If no tolerance specified, expect attributes to be identical
                        cat_diffs += phase_dict.get(key) != check_dict.get(key)
                    else:
                        # allclose will throw a Value error if you have two arrays
                        # of different shape, which we can catch to flag that
                        # the two arrays are actually not within tolerance.
                        if np.shape(phase_dict[key]) != np.shape(check_dict[key]):
                            cat_diffs += 1
                        else:
                            cat_diffs += not np.allclose(
                                phase_dict[key],
                                check_dict[key],
                                tol_dict[key][0],
                                tol_dict[key][1],
                            )
                else:
                    cat_diffs += check_dict[key] is not None
            if (cat_diffs == 0) or (cat_name == name):
                if cat_diffs < match_diffs:
                    # If our current match is an improvement on any previous matches,
                    # then record it as the best match.
                    match_id = cat_id
                    match_diffs = cat_diffs
                if match_diffs == 0:
                    # If we have a total match, we can bail at this point
                    break

        return match_id, match_diffs

    def _add_phase_center(
        self,
        cat_name,
        cat_type=None,
        cat_lon=None,
        cat_lat=None,
        cat_frame=None,
        cat_epoch=None,
        cat_times=None,
        cat_pm_ra=None,
        cat_pm_dec=None,
        cat_dist=None,
        cat_vrad=None,
        info_source="user",
        force_update=False,
        cat_id=None,
    ):
        """
        Add an entry to the internal object/source catalog or find a matching one.

        This is a helper function for identifying a adding a phase center to the
        internal catalog, contained within the attribute `phase_center_catalog`, unless
        a phase center already exists that matches the passed parameters. If a matching
        phase center is found, the catalog ID associated with that phase center is
        returned.

        Parameters
        ----------
        cat_name : str
            Name of the phase center to be added.
        cat_type : str
            Type of phase center to be added. Must be one of:
                "sidereal" (fixed RA/Dec),
                "ephem" (RA/Dec that moves with time),
                "driftscan" (fixed az/el position),
                "unprojected" (no w-projection, equivalent to the old
                `phase_type` == "drift").
        cat_lon : float or ndarray
            Value of the longitudinal coordinate (e.g., RA, Az, l) in radians of the
            phase center. No default unless `cat_type="unprojected"`, in which case the
            default is zero. Expected to be a float for sidereal and driftscan phase
            centers, and an ndarray of floats of shape (Npts,) for ephem phase centers.
        cat_lat : float or ndarray
            Value of the latitudinal coordinate (e.g., Dec, El, b) in radians of the
            phase center. No default unless `cat_type="unprojected"`, in which case the
            default is pi/2. Expected to be a float for sidereal and driftscan phase
            centers, and an ndarray of floats of shape (Npts,) for ephem phase centers.
        cat_frame : str
            Coordinate frame that cat_lon and cat_lat are given in. Only used
            for sidereal and ephem targets. Can be any of the several supported frames
            in astropy (a limited list: fk4, fk5, icrs, gcrs, cirs, galactic).
        cat_epoch : str or float
            Epoch of the coordinates, only used when cat_frame = fk4 or fk5. Given
            in units 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).
        cat_times : ndarray of floats
            Only used when `cat_type="ephem"`. Describes the time for which the values
            of `cat_lon` and `cat_lat` are caclulated, in units of JD. Shape is (Npts,).
        cat_pm_ra : float
            Proper motion in RA, in units of mas/year. Only used for sidereal phase
            centers.
        cat_pm_dec : float
            Proper motion in Dec, in units of mas/year. Only used for sidereal phase
            centers.
        cat_dist : float or ndarray of float
            Distance of the source, in units of pc. Only used for sidereal and ephem
            phase centers. Expected to be a float for sidereal and driftscan phase
            centers, and an ndarray of floats of shape (Npts,) for ephem phase centers.
        cat_vrad : float or ndarray of float
            Radial velocity of the source, in units of km/s. Only used for sidereal and
            ephem phase centers. Expected to be a float for sidereal and driftscan phase
            centers, and an ndarray of floats of shape (Npts,) for ephem phase centers.
        info_source : str
            Optional string describing the source of the information provided. Used
            primarily in UVData to denote when an ephemeris has been supplied by the
            JPL-Horizons system, user-supplied, or read in by one of the various file
            interpreters. Default is 'user'.
        force_update : bool
            Normally, `_add_phase_center` will throw an error if there already exists a
            phase_center with the given cat_id. However, if one sets
            `force_update=True`, the method will overwrite the existing entry in
            `phase_center_catalog` with the parameters supplied. Note that doing this
            will _not_ update other atributes of the `UVData` object. Default is False.
        cat_id : int
            An integer signifying the ID number for the phase center, used in the
            `phase_center_id_array` attribute. If a matching phase center entry exists
            already, that phase center ID will be returned, which may be different than
            the value specified to this parameter. The default is for the method to
            assign this value automatically.

        Returns
        -------
        cat_id : int
            The unique ID number for the phase center that either matches the specified
            parameters or was added to the internal catalog. If a matching entry was
            found, this may not be the value passed to the `cat_id` parameter. This
            value is used in the `phase_center_id_array` attribute to denote which
            source a given baseline-time corresponds to.

        Raises
        ------
        ValueError
            If attempting to add a non-unique source name or if adding a sidereal
            source without coordinates.

        """
        if not isinstance(cat_name, str):
            raise ValueError("cat_name must be a string.")

        if cat_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,
            )
            cat_type = "unprojected"
            if cat_name == "unphased":
                cat_name = "unprojected"

        # We currently only have 4 supported types -- make sure the user supplied
        # one of those
        if cat_type not in ["sidereal", "ephem", "driftscan", "unprojected"]:
            raise ValueError(
                "Only sidereal, ephem, driftscan or unprojected may be used "
                "for cat_type."
            )

        # Both proper motion parameters need to be set together
        if (cat_pm_ra is None) != (cat_pm_dec is None):
            raise ValueError(
                "Must supply values for either both or neither of "
                "cat_pm_ra and cat_pm_dec."
            )

        # If left unset, unprojected and driftscan defaulted to Az, El = (0 deg, 90 deg)
        if cat_type in ["unprojected", "driftscan"]:
            if cat_lon is None:
                cat_lon = 0.0
            if cat_lat is None:
                cat_lat = np.pi / 2
            if cat_frame is None:
                cat_frame = "altaz"

        # check some case-specific things and make sure all the entries are acceptable
        if (cat_times is None) and (cat_type == "ephem"):
            raise ValueError("cat_times cannot be None for ephem object.")
        elif (cat_times is not None) and (cat_type != "ephem"):
            raise ValueError("cat_times cannot be used for non-ephem phase centers.")

        if (cat_lon is None) and (cat_type in ["sidereal", "ephem"]):
            raise ValueError(
                "cat_lon cannot be None for sidereal or ephem phase centers."
            )

        if (cat_lat is None) and (cat_type in ["sidereal", "ephem"]):
            raise ValueError(
                "cat_lat cannot be None for sidereal or ephem phase centers."
            )

        if (cat_frame is None) and (cat_type in ["sidereal", "ephem"]):
            raise ValueError(
                "cat_frame cannot be None for sidereal or ephem phase centers."
            )
        elif (cat_frame != "altaz") and (cat_type in ["driftscan", "unprojected"]):
            raise ValueError(
                "cat_frame must be either None or 'altaz' when the cat type "
                "is either driftscan or unprojected."
            )

        if (cat_type == "unprojected") and (cat_lon != 0.0):
            raise ValueError(
                "Catalog entries that are unprojected must have cat_lon set to either "
                "0 or None."
            )
        if (cat_type == "unprojected") and (cat_lat != (np.pi / 2)):
            raise ValueError(
                "Catalog entries that are unprojected must have cat_lat set to either "
                "pi/2 or None."
            )

        if (cat_type != "sidereal") and (
            (cat_pm_ra is not None) or (cat_pm_dec is not None)
        ):
            raise ValueError(
                "Non-zero proper motion values (cat_pm_ra, cat_pm_dec) "
                "for cat types other than sidereal are not supported."
            )

        if isinstance(cat_epoch, Time) or isinstance(cat_epoch, str):
            if cat_frame in ["fk4", "fk4noeterms"]:
                cat_epoch = Time(cat_epoch).byear
            else:
                cat_epoch = Time(cat_epoch).jyear
        elif cat_epoch is not None:
            cat_epoch = float(cat_epoch)

        if cat_type == "ephem":
            cat_times = np.array(cat_times, dtype=float).reshape(-1)
            cshape = cat_times.shape
            try:
                cat_lon = np.array(cat_lon, dtype=float).reshape(cshape)
                cat_lat = np.array(cat_lat, dtype=float).reshape(cshape)
                if cat_dist is not None:
                    cat_dist = np.array(cat_dist, dtype=float).reshape(cshape)
                if cat_vrad is not None:
                    cat_vrad = np.array(cat_vrad, dtype=float).reshape(cshape)
            except ValueError as err:
                raise ValueError(
                    "Object properties -- lon, lat, pm_ra, pm_dec, dist, vrad -- must "
                    "be of the same size as cat_times for ephem phase centers."
                ) from err
        else:
            if cat_lon is not None:
                cat_lon = float(cat_lon)
            cat_lon = None if cat_lon is None else float(cat_lon)
            cat_lat = None if cat_lat is None else float(cat_lat)
            cat_pm_ra = None if cat_pm_ra is None else float(cat_pm_ra)
            cat_pm_dec = None if cat_pm_dec is None else float(cat_pm_dec)
            cat_dist = None if cat_dist is None else float(cat_dist)
            cat_vrad = None if cat_vrad is None else float(cat_vrad)

        if self.phase_center_catalog is not None:
            # We want to create a unique ID for each source, for use in indexing arrays.
            # The logic below ensures that we pick the lowest positive integer that is
            # not currently being used by another source
            if cat_id is None:
                cat_id = (
                    set(range(self.Nphase + 1))
                    .difference(self.phase_center_catalog)
                    .pop()
                )
            elif cat_id in self.phase_center_catalog and not force_update:
                raise ValueError(
                    "Provided cat_id belongs to another source (%s)."
                    % self.phase_center_catalog[cat_id]["cat_name"]
                )

            # Let's warn if this entry has the same name as an existing one
            temp_id, cat_diffs = self._look_in_catalog(
                cat_name=cat_name,
                cat_type=cat_type,
                cat_lon=cat_lon,
                cat_lat=cat_lat,
                cat_frame=cat_frame,
                cat_epoch=cat_epoch,
                cat_times=cat_times,
                cat_pm_ra=cat_pm_ra,
                cat_pm_dec=cat_pm_dec,
                cat_dist=cat_dist,
                cat_vrad=cat_vrad,
            )

            # If the source does have the same name, check to see if all the
            # atributes match. If so, no problem, go about your business
            if temp_id is not None:
                if cat_diffs == 0:
                    # Everything matches, return the catalog ID of the matching entry
                    return temp_id
                else:
                    warnings.warn(
                        f"The provided name {cat_name} is already used but has "
                        "different parameters. Adding another entry with the same name "
                        "but a different ID and parameters."
                    )
        else:
            if cat_id is None:
                cat_id = 0
        # If source is unique, begin creating a dictionary for it
        phase_dict = {
            "cat_name": cat_name,
            "cat_type": cat_type,
            "cat_lon": cat_lon,
            "cat_lat": cat_lat,
            "cat_frame": cat_frame,
            "cat_epoch": cat_epoch,
            "cat_times": cat_times,
            "cat_pm_ra": cat_pm_ra,
            "cat_pm_dec": cat_pm_dec,
            "cat_vrad": cat_vrad,
            "cat_dist": cat_dist,
            "info_source": info_source,
        }

        if self.phase_center_catalog is not None:
            self.phase_center_catalog[cat_id] = phase_dict
        else:
            self.phase_center_catalog = {cat_id: phase_dict}

        self.Nphase = len(self.phase_center_catalog)

        return cat_id

    def _remove_phase_center(self, defunct_id):
        """
        Remove an entry from the internal object/source catalog.

        Removes an entry from the attribute `phase_center_catalog`.

        Parameters
        ----------
        defunct_id : int
            Catalog ID of the source to be removed

        Raises
        ------
        IndexError
            If the name provided is not found as a key in `phase_center_catalog`
        """
        if defunct_id not in self.phase_center_catalog:
            raise IndexError("No source by that ID contained in the catalog.")

        del self.phase_center_catalog[defunct_id]
        self.Nphase = len(self.phase_center_catalog)

    def _clear_unused_phase_centers(self):
        """
        Remove objects dictionaries and names that are no longer in use.

        Goes through the `phase_center_catalog` attribute in of a UVData object and
        clears out entries that are no longer being used, and appropriately updates
        `phase_center_id_array` accordingly. This function is not typically called
        by users, but instead is used by other methods.

        """
        unique_cat_ids = np.unique(self.phase_center_id_array)
        defunct_list = []
        Nphase = 0
        for cat_id in self.phase_center_catalog:
            if cat_id in unique_cat_ids:
                Nphase += 1
            else:
                defunct_list.append(cat_id)

        # Check the number of "good" sources we have -- if we haven't dropped any,
        # then we are free to bail, otherwise update the Nphase attribute
        if Nphase == self.Nphase:
            return

        # Time to kill the entries that are no longer in the source stack
        for defunct_id in defunct_list:
            self._remove_phase_center(defunct_id)

    def _check_for_cat_type(self, cat_type):
        """
        Check which Nblts have a cat_type in `cat_type`.

        This convenience method returns back a boolean mask to identify which data
        along the Blt axis contains data in cat_type.

        Parameters
        ----------
        cat_type : str or list of str
            Phase types to check for.

        Returns
        -------
        blt_mask : ndarray of bool
            A boolean mask for identifying which elements contain unprojected objects.
            True where not projected, False where projected.
        """
        # Check and see if we have any data with cat_type.
        if not isinstance(cat_type, (list, tuple, np.ndarray)):
            cat_type = [cat_type]
        cat_type_list = [
            cat_id
            for cat_id, cat_dict in self.phase_center_catalog.items()
            if cat_dict["cat_type"] in cat_type
        ]

        # Construct a bool mask
        blt_mask = np.isin(self.phase_center_id_array, cat_type_list)

        return blt_mask

    def rename_phase_center(self, catalog_identifier, new_name):
        """
        Rename a phase center/catalog entry within a multi phase center data set.

        Parameters
        ----------
        catalog_identifier : str or int or list of int
            Unique identifier of a phase center to be renamed. If supplied as a str,
            will be matched against the phase center name. Otherwise if an int or list
            of int, assumed to be the catalog ID number(s).
        new_name : str
            New name for the phase center.


        Raises
        ------
        ValueError
            If attempting to run the method on a non multi phase center data set, or if
            `catalog_identifier` is not found in `phase_center_catalog`.
        TypeError
            If `new_name` is not actually a string or if `catalog_identifier` is not a
            string or an integer.
        """
        if isinstance(catalog_identifier, (str, int)):
            pass
        elif isinstance(catalog_identifier, list) and all(
            isinstance(cat, int) for cat in catalog_identifier
        ):
            pass
        else:
            raise TypeError(
                "catalog_identifier must be a string, an integer or a list of integers."
            )

        if isinstance(catalog_identifier, str):
            cat_id = []
            for key, ps_dict in self.phase_center_catalog.items():
                if ps_dict["cat_name"] == catalog_identifier:
                    cat_id.append(key)
            if len(cat_id) == 0:
                raise ValueError(
                    f"No entry by the name {catalog_identifier} in the catalog."
                )
        else:
            # Force cat_id to be a list to make downstream code simpler. If cat_id is
            # an int, it will throw a TypeError on casting to list, which we can catch.
            try:
                cat_id = list(catalog_identifier)
            except TypeError:
                cat_id = [catalog_identifier]

            for key in cat_id:
                if key not in self.phase_center_catalog:
                    raise ValueError(f"No entry with the ID {key} in the catalog.")

        if not isinstance(new_name, str):
            raise TypeError("Value provided to new_name must be a string.")

        if (new_name == catalog_identifier) or (len(cat_id) == 0):
            # This is basically just a no-op, so return to user
            return

        for key in cat_id:
            self.phase_center_catalog[key]["cat_name"] = new_name

    def split_phase_center(
        self,
        catalog_identifier,
        new_name=None,
        select_mask=None,
        new_id=None,
        downselect=False,
    ):
        """
        Rename the phase center (but preserve other properties) of a subset of data.

        Allows you to rename a subset of the data phased to a particular phase center,
        marked by a different name than the original. Useful when you want to phase to
        one position, but want to differentiate different groups of data (e.g., marking
        every other integration to make jackknifing easier).

        Parameters
        ----------
        catalog_identifier : str or int
            Unique identifier of a phase center to be renamed. If supplied as a str,
            will be matched against the phase center name. Otherwise if an int, assumed
            to be the catalog ID number.
        new_name : str
            Name for the "split" portion of the phase center. Optional argument, default
            is to use the same name as the existing phase center.
        select_mask : array_like
            Selection mask for which data should be identified as belonging to the phase
            center labeled by `new_name`. Any array-like able to be used as an index
            is suitable -- the most typical is an array of bool with length `Nblts`,
            or an array of ints within the range (-Nblts, Nblts).
        new_id : int
            Catalog ID to assign to the new phase center. Optional argument, a unique
            value will be automatically assigned if not provided.
        downselect : bool
            If selecting data that is not marked as belonging to `cat_name`,
            normally an error is thrown. By setting this to True, `select_mask` will
            be modified to exclude data not marked as belonging to `cat_name`.

        Raises
        ------
        ValueError
            If  catalog_identifier is not an int or string or if it is not found as a
            cat_name in `phase_center_catalog` or if it is a string and is found
            multiple times. If new_id is not an int or already exists in the catalog.
            If new_name is not a string. If `select_mask` contains data that doesn't
            belong to catalog_identifier, unless `downselect` is True.
        IndexError
            If select_mask is not a valid indexing array.
        UserWarning
            If all data for `cat_name` was selected (in which case `rename_phase_center`
            is called instead), or if no valid data was selected.
        """
        # Check to make sure that everything lines up with
        if not isinstance(catalog_identifier, (str, int)):
            raise TypeError("catalog_identifier must be a string or an integer.")

        if isinstance(catalog_identifier, str):
            cat_id = None
            for pc_id, pc_dict in self.phase_center_catalog.items():
                if pc_dict["cat_name"] == catalog_identifier:
                    if cat_id is None:
                        cat_id = pc_id
                    else:
                        raise ValueError(
                            f"The cat_name {catalog_identifier} has multiple entries "
                            "in the catalog. Please specify a cat_id in order to "
                            "eliminate ambiguity (which you can see using the "
                            "`print_phase_center_info` method)."
                        )
            if cat_id is None:
                raise ValueError(
                    f"No catalog entries matching the name {catalog_identifier}."
                )
        else:
            cat_id = catalog_identifier
            if cat_id not in self.phase_center_catalog:
                raise ValueError(f"No entry with the ID {cat_id} found in the catalog.")

        if new_id is None:
            new_id = (
                set(range(self.Nphase + 1)).difference(self.phase_center_catalog).pop()
            )
        elif not isinstance(new_id, int):
            raise TypeError("Value provided to new_id must be an int.")

        if new_id in self.phase_center_catalog:
            raise ValueError(
                f"The ID {new_id} is already in the catalog, choose another value for "
                "new_id."
            )

        if not (isinstance(new_name, str) or new_name is None):
            raise TypeError("Value provided to new_name must be a string.")

        try:
            inv_mask = np.ones(self.Nblts, dtype=bool)
            inv_mask[select_mask] = False
        except IndexError as err:
            raise IndexError(
                "select_mask must be an array-like, either of ints with shape (Nblts), "
                "or  of ints within the range (-Nblts, Nblts)."
            ) from err

        # If we have selected any entries that don't correspond to the cat_id
        # in question, either downselect or raise an error.
        if np.any(
            np.isin(self.phase_center_id_array[select_mask], cat_id, invert=True)
        ):
            if downselect:
                inv_mask |= np.isin(self.phase_center_id_array, cat_id, invert=True)
                select_mask = ~inv_mask
            else:
                raise ValueError(
                    "Data selected with select_mask includes data which is not part of "
                    "the selected phase_center ({cat_id}).  You can fix this by either "
                    "revising select_mask or setting downselect=True."
                )

        # Now check for no(-ish) ops
        if np.all(inv_mask):
            # You didn't actually select anything we could change
            warnings.warn("No relevant data selected - check select_mask.")
        elif not np.any(np.isin(self.phase_center_id_array[inv_mask], cat_id)):
            # No matching catalog IDs found outside the range, so this is really a
            # replace more than a split.
            warnings.warn(
                "All data for the source selected - updating the cat_id instead."
            )
            self._update_phase_center_id(cat_id, new_id)
            if new_name is not None:
                self.rename_phase_center(new_id, new_name)
        else:
            temp_dict = self.phase_center_catalog[cat_id]
            cat_id = self._add_phase_center(
                temp_dict["cat_name"] if new_name is None else new_name,
                cat_type=temp_dict["cat_type"],
                cat_lon=temp_dict.get("cat_lon"),
                cat_lat=temp_dict.get("cat_lat"),
                cat_frame=temp_dict.get("cat_frame"),
                cat_epoch=temp_dict.get("cat_epoch"),
                cat_times=temp_dict.get("cat_times"),
                cat_pm_ra=temp_dict.get("cat_pm_ra"),
                cat_pm_dec=temp_dict.get("cat_pm_dec"),
                cat_dist=temp_dict.get("cat_dist"),
                cat_vrad=temp_dict.get("cat_vrad"),
                cat_id=new_id,
                force_update=True,
            )
            self.phase_center_id_array[select_mask] = cat_id

    def merge_phase_centers(
        self, catalog_identifier, force_merge=False, ignore_name=False
    ):
        """
        Merge two differently named objects into one within a multi-phase-ctr data set.

        Recombines two different objects into a single catalog entry -- useful if
        having previously used `split_phase_center` or when multiple objects with
        different names share the same source parameters.

        Parameters
        ----------
        catalog_identifier : str or int or list of str or int
            Unique identifier(s) of a phase center to be merged. If supplied as strings,
            will be matched against the phase center name. Otherwise if supplied as
            integers, assumed to be the catalog ID number(s).
        force_merge : bool
            Normally, the method will throw an error if the phase center properties
            differ for the catalogs listed in catalog_identifier. This can be overriden
            by setting this to True. Default is False.
        ignore_name : bool
            When comparing phase centers, all attributes are normally checked. However,
            if set to True, the catalog name ("cat_name") will be ignored when
            performing the comparison. Default is False.

        Raises
        ------
        ValueError
            If catname1 or catname2 are not found in the UVData object, of if their
            properties differ (and `force_merge` is not set to True).
        UserWarning
            If forcing the merge of two objects with different properties.
        """
        if isinstance(catalog_identifier, (str, int)):
            pass
        elif isinstance(catalog_identifier, list) and all(
            isinstance(cat, (str, int)) for cat in catalog_identifier
        ):
            pass
        else:
            raise TypeError(
                "catalog_identifier must be a string, an integer or a list of strings "
                "or integers."
            )

        if not isinstance(catalog_identifier, list):
            catalog_identifier = [catalog_identifier]

        cat_id_list = []
        for cat in catalog_identifier:
            if isinstance(cat, str):
                this_list = []
                for key, ps_dict in self.phase_center_catalog.items():
                    if ps_dict["cat_name"] == cat:
                        this_list.append(key)
                if len(this_list) == 0:
                    raise ValueError(f"No entry by the name {cat} in the catalog.")
                cat_id_list.extend(this_list)
            else:
                # Force cat_id to be a list to make downstream code simpler. If cat_id
                # is an int, it will throw a TypeError on casting to list, which we can
                # catch.
                if cat not in self.phase_center_catalog:
                    raise ValueError(f"No entry with the ID {cat} in the catalog.")
                cat_id_list.append(cat)

        # Check for the no-op
        if len(cat_id_list) < 2:
            warnings.warn(
                "Selection matches less than two phase centers, no need to merge."
            )
            return

        # First, let's check and see if the dict entries are identical
        for cat_id in cat_id_list[1:]:
            pc_id, pc_diffs = self._look_in_catalog(
                phase_dict=self.phase_center_catalog[cat_id],
                ignore_name=ignore_name,
                target_cat_id=cat_id_list[0],
            )
            if (pc_diffs != 0) or (pc_id is None):
                if force_merge:
                    warnings.warn(
                        "Forcing fields together, even though their attributes differ."
                    )
                else:
                    raise ValueError(
                        "Attributes of phase centers differ in phase_center_catalog. "
                        "You can ignore this error and force merge_phase_centers to "
                        "complete by setting force_merge=True, but this should be done "
                        "with substantial caution."
                    )

        # Set everything to the first cat ID in the list
        self.phase_center_id_array[
            np.isin(self.phase_center_id_array, cat_id_list)
        ] = cat_id_list[0]

        # Finally, remove the defunct cat IDs
        for cat_id in cat_id_list[1:]:
            self._remove_phase_center(cat_id)

    def print_phase_center_info(
        self,
        catalog_identifier=None,
        hms_format=None,
        return_str=False,
        print_table=True,
    ):
        """
        Print out the details of the phase centers.

        Prints out an ASCII table that contains the details of the
        `phase_center_catalog` attribute, which acts as the internal source catalog
        for UVData objects.

        Parameters
        ----------
        catalog_identifier : str or int or list of str or int
            Optional parameter which, if provided, will cause the method to only return
            information on the phase center(s) with the matching name(s) or catalog ID
            number(s). Default is to print out information on all catalog entries.
        hms_format : bool
            Optional parameter, which if selected, can be used to force coordinates to
            be printed out in Hours-Min-Sec (if set to True) or Deg-Min-Sec (if set to
            False) format. Default is to print out in HMS if all the objects have
            coordinate frames of icrs, gcrs, fk5, fk4, and top; otherwise, DMS format
            is used.
        return_str: bool
            If set to True, the method returns an ASCII string which contains all the
            table infrmation. Default is False.
        print_table : bool
            If set to True, prints the table to the terminal window. Default is True.

        Returns
        -------
        table_str : bool
            If return_str=True, an ASCII string containing the entire table text

        Raises
        ------
        ValueError
            If `cat_name` matches no keys in `phase_center_catalog`.
        """
        r2d = 180.0 / np.pi
        r2m = 60.0 * 180.0 / np.pi
        r2s = 3600.0 * 180.0 / np.pi
        ra_frames = ["icrs", "gcrs", "fk5", "fk4", "topo"]

        if catalog_identifier is not None:
            if isinstance(catalog_identifier, (str, int)):
                pass
            elif isinstance(catalog_identifier, list) and all(
                isinstance(cat, (str, int)) for cat in catalog_identifier
            ):
                pass
            else:
                raise TypeError(
                    "catalog_identifier must be a string, an integer or a list of "
                    "strings or integers."
                )

            if not isinstance(catalog_identifier, list):
                catalog_identifier = [catalog_identifier]

            cat_id_list = []
            for cat in catalog_identifier:
                if isinstance(cat, str):
                    this_list = []
                    for key, ps_dict in self.phase_center_catalog.items():
                        if ps_dict["cat_name"] == cat:
                            this_list.append(key)
                    if len(this_list) == 0:
                        raise ValueError(f"No entry by the name {cat} in the catalog.")
                    cat_id_list.extend(this_list)
                else:
                    # Force cat_id to be a list to make downstream code simpler.
                    # If cat_id is an int, it will throw a TypeError on casting to
                    # list, which we can catch.
                    if cat not in self.phase_center_catalog:
                        raise ValueError(f"No entry with the ID {cat} in the catalog.")
                    cat_id_list.append(cat)
        else:
            cat_id_list = list(self.phase_center_catalog)

        dict_list = [self.phase_center_catalog[cat_id] for cat_id in cat_id_list]

        # We want to check and actually see which fields we need to
        # print
        any_lon = any_lat = any_frame = any_epoch = any_times = False
        any_pm_ra = any_pm_dec = any_dist = any_vrad = False

        for indv_dict in dict_list:
            any_lon = any_lon or indv_dict.get("cat_lon") is not None
            any_lat = any_lat or indv_dict.get("cat_lat") is not None
            any_frame = any_frame or indv_dict.get("cat_frame") is not None
            any_epoch = any_epoch or indv_dict.get("cat_epoch") is not None
            any_times = any_times or indv_dict.get("cat_times") is not None
            any_pm_ra = any_pm_ra or indv_dict.get("cat_pm_ra") is not None
            any_pm_dec = any_pm_dec or indv_dict.get("cat_pm_dec") is not None
            any_dist = any_dist or indv_dict.get("cat_dist") is not None
            any_vrad = any_vrad or indv_dict.get("cat_vrad") is not None

            if any_lon and (hms_format is None):
                cat_frame = indv_dict.get("cat_frame")
                cat_type = indv_dict["cat_type"]
                if (cat_frame not in ra_frames) or (cat_type == "driftscan"):
                    hms_format = False

        if hms_format is None:
            hms_format = True

        col_list = []
        col_list.append(
            {"hdr": ("ID", "#"), "fmt": "% 4i", "field": " %4s ", "name": "cat_id"}
        )
        col_list.append(
            {
                "hdr": ("Cat Entry", "Name"),
                "fmt": "%12s",
                "field": " %12s ",
                "name": "cat_name",
            }
        )
        col_list.append(
            {"hdr": ("Type", ""), "fmt": "%12s", "field": " %12s ", "name": "cat_type"}
        )

        if any_lon:
            col_list.append(
                {
                    "hdr": ("Az/Lon/RA", "hours" if hms_format else "deg"),
                    "fmt": "% 3i:%02i:%05.2f",
                    "field": (" %12s " if hms_format else " %13s "),
                    "name": "cat_lon",
                }
            )
        if any_lat:
            col_list.append(
                {
                    "hdr": ("El/Lat/Dec", "deg"),
                    "fmt": "%1s%2i:%02i:%05.2f",
                    "field": " %12s ",
                    "name": "cat_lat",
                }
            )
        if any_frame:
            col_list.append(
                {
                    "hdr": ("Frame", ""),
                    "fmt": "%5s",
                    "field": " %5s ",
                    "name": "cat_frame",
                }
            )
        if any_epoch:
            col_list.append(
                {
                    "hdr": ("Epoch", ""),
                    "fmt": "%7s",
                    "field": " %7s ",
                    "name": "cat_epoch",
                }
            )
        if any_times:
            col_list.append(
                {
                    "hdr": ("   Ephem Range   ", "Start-MJD    End-MJD"),
                    "fmt": " %8.2f  % 8.2f",
                    "field": " %20s ",
                    "name": "cat_times",
                }
            )
        if any_pm_ra:
            col_list.append(
                {
                    "hdr": ("PM-Ra", "mas/yr"),
                    "fmt": "%.4g",
                    "field": " %6s ",
                    "name": "cat_pm_ra",
                }
            )
        if any_pm_dec:
            col_list.append(
                {
                    "hdr": ("PM-Dec", "mas/yr"),
                    "fmt": "%.4g",
                    "field": " %6s ",
                    "name": "cat_pm_dec",
                }
            )
        if any_dist:
            col_list.append(
                {
                    "hdr": ("Dist", "pc"),
                    "fmt": "%.1e",
                    "field": " %7s ",
                    "name": "cat_dist",
                }
            )
        if any_vrad:
            col_list.append(
                {
                    "hdr": ("V_rad", "km/s"),
                    "fmt": "%.4g",
                    "field": " %6s ",
                    "name": "cat_vrad",
                }
            )

        top_str = ""
        bot_str = ""
        for col in col_list:
            top_str += col["field"] % col["hdr"][0]
            bot_str += col["field"] % col["hdr"][1]

        info_str = ""

        info_str += top_str + "\n"
        info_str += bot_str + "\n"
        info_str += ("-" * len(bot_str)) + "\n"
        # We want to print in the order of cat_id
        for idx in np.argsort(cat_id_list):
            tbl_str = ""
            for col in col_list:
                # If we have a "special" field that needs extra handling,
                # take care of that up front
                if col["name"] == "cat_id":
                    temp_val = cat_id_list[idx]
                else:
                    temp_val = dict_list[idx][col["name"]]
                if temp_val is None:
                    temp_str = ""
                elif col["name"] == "cat_lon":
                    # Force the longitude component to be a positive value
                    temp_val = np.mod(np.median(temp_val), 2 * np.pi)
                    temp_val /= 15.0 if hms_format else 1.0
                    coord_tuple = (
                        np.mod(temp_val * r2d, 360.0),
                        np.mod(temp_val * r2m, 60.0),
                        np.mod(temp_val * r2s, 60.0),
                    )
                    temp_str = col["fmt"] % coord_tuple
                elif col["name"] == "cat_lat":
                    temp_val = np.median(temp_val)
                    coord_tuple = (
                        "-" if temp_val < 0.0 else "+",
                        np.mod(np.abs(temp_val) * r2d, 360.0),
                        np.mod(np.abs(temp_val) * r2m, 60.0),
                        np.mod(np.abs(temp_val) * r2s, 60.0),
                    )
                    temp_str = col["fmt"] % coord_tuple
                elif col["name"] == "cat_epoch":
                    use_byrs = dict_list[idx]["cat_frame"] in ["fk4", "fk4noeterms"]
                    temp_val = ("B%6.1f" if use_byrs else "J%6.1f") % temp_val
                    temp_str = col["fmt"] % temp_val
                elif col["name"] == "cat_times":
                    time_tuple = (
                        np.min(temp_val) - 2400000.5,
                        np.max(temp_val) - 2400000.5,
                    )
                    temp_str = col["fmt"] % time_tuple
                elif (col["name"] == "cat_dist") or (col["name"] == "cat_vrad"):
                    temp_val = np.median(temp_val)
                    temp_str = col["fmt"] % temp_val
                else:
                    temp_str = col["fmt"] % temp_val
                tbl_str += col["field"] % temp_str
            info_str += tbl_str + "\n"

        if print_table:
            # We need this extra bit of code to handle trailing whitespace, since
            # otherwise some checks (e.g., doc check on tutorials) will balk
            print(
                "\n".join([line.rstrip() for line in info_str.split("\n")]), end=""
            )  # pragma: nocover
        if return_str:
            return info_str

    def _update_phase_center_id(self, cat_id, new_id=None, reserved_ids=None):
        """
        Update a phase center with a new catalog ID number.

        Parameters
        ----------
        cat_id : int
            Current catalog ID of the phase center, which corresponds to a key in the
            attribute `phase_center_catalog`.
        new_id : int
            Optional argument. If supplied, then the method will attempt to use the
            provided value as the new catalog ID, provided that an existing catalog
            entry is not already using the same value. If not supplied, then the
            method will automatically assign a value.
        reserved_ids : array-like in int
            Optional argument. An array-like of ints that denotes which ID numbers
            are already reserved. Useful for when combining two separate catalogs.

        Raises
        ------
        ValueError
            If not using the method on a multi-phase-ctr data set, if there's no entry
            that matches `cat_name`, or of the value `new_id` is already taken.
        """
        if cat_id not in self.phase_center_catalog:
            raise ValueError(
                f"Cannot run _update_phase_center_id: no entry with id {cat_id}."
            )

        used_cat_ids = set(self.phase_center_catalog)
        used_cat_ids.remove(cat_id)
        if reserved_ids is not None:
            used_cat_ids = used_cat_ids.union(reserved_ids)

        if new_id is None:
            # If the old ID is in the reserved list, then we'll need to update it
            if cat_id not in used_cat_ids:
                # Don't need to actually update anything
                return
            new_id = set(range(len(used_cat_ids) + 1)).difference(used_cat_ids).pop()
        else:
            if new_id in used_cat_ids:
                raise ValueError("Catalog ID supplied already taken by another source.")

        self.phase_center_id_array[self.phase_center_id_array == cat_id] = new_id
        self.phase_center_catalog[new_id] = self.phase_center_catalog.pop(cat_id)

    def _consolidate_phase_center_catalogs(
        self, reference_catalog=None, other=None, ignore_name=False
    ):
        """
        Consolidate phase center catalogs with a reference or another object.

        This is a helper method which updates the phase_center_catalog and related
        parameters to make this object consistent with a reference catalog or with
        another object so the second object can be added or concatenated to this object.
        If both `reference_catalog` and `other` are provided, both this object and the
        one passed to `other` will have their catalogs updated.

        Parameters
        ----------
        reference_calalog : dict
            A reference catalog to make this object consistent with.
        other : UVData object
            A UVData object which self needs to be consistent with because it will be
            added to self. The phase_center_catalog from other is used as the reference
            catalog if the reference_catalog is None. If `reference_catalog` is also
            set, the phase_center_catalog on other will also be modified to be
            consistent with the `reference_catalog`.
        ignore_name : bool
            Option to ignore the name of the phase center (`cat_name` in
            `phase_center_catalog`) when identifying matching phase centers. If set to
            True, phase centers that are the same up to their name will be combined with
            the name set to the reference catalog name or the name found in the first
            UVData object. If set to False, phase centers that are the same up to the
            name will be kept as separate phase centers. Default is False.

        """
        if reference_catalog is None and other is None:
            raise ValueError(
                "Either the reference_catalog or the other parameter must be set."
            )

        if reference_catalog is None:
            reference_catalog = other.phase_center_catalog
        elif other is not None:
            # first update other to be consistent with the reference
            other._consolidate_phase_center_catalogs(
                reference_catalog=reference_catalog
            )
            # then use the updated other as the reference
            reference_catalog = other.phase_center_catalog

        reserved_ids = list(reference_catalog)
        # First loop, we want to update all the catalog IDs so that we know there
        # are no conflicts with the reference
        for cat_id in list(self.phase_center_catalog):
            self._update_phase_center_id(cat_id, reserved_ids=reserved_ids)

        # Next loop, we want to update the IDs of sources that are in the reference.
        for cat_id in list(reference_catalog):
            match_id, match_diffs = self._look_in_catalog(
                phase_dict=reference_catalog[cat_id], ignore_name=ignore_name
            )
            if match_id is not None and match_diffs == 0:
                # If match_diffs is 0 then all the keys in the phase center catalog
                # match, so this is functionally the same source
                self._update_phase_center_id(match_id, new_id=cat_id)
                if ignore_name:
                    # Make the names match if names were ignored in matching
                    self.phase_center_catalog[cat_id]["cat_name"] = reference_catalog[
                        cat_id
                    ]["cat_name"]

                # _look_in_catalog ignores the "info_source" field. Update it so
                # it matches the reference.
                self.phase_center_catalog[cat_id]["info_source"] = reference_catalog[
                    cat_id
                ]["info_source"]

        # Finally, add those other objects not found in self
        for cat_id in reference_catalog:
            if cat_id not in self.phase_center_catalog:
                self._add_phase_center(
                    reference_catalog[cat_id]["cat_name"],
                    cat_type=reference_catalog[cat_id]["cat_type"],
                    cat_lon=reference_catalog[cat_id]["cat_lon"],
                    cat_lat=reference_catalog[cat_id]["cat_lat"],
                    cat_frame=reference_catalog[cat_id]["cat_frame"],
                    cat_epoch=reference_catalog[cat_id]["cat_epoch"],
                    cat_times=reference_catalog[cat_id]["cat_times"],
                    cat_pm_ra=reference_catalog[cat_id]["cat_pm_ra"],
                    cat_pm_dec=reference_catalog[cat_id]["cat_pm_dec"],
                    cat_dist=reference_catalog[cat_id]["cat_dist"],
                    cat_vrad=reference_catalog[cat_id]["cat_vrad"],
                    info_source=reference_catalog[cat_id]["info_source"],
                    cat_id=cat_id,
                )

    @property
    def _data_params(self):
        """List of strings giving the data-like parameters."""
        return ["data_array", "nsample_array", "flag_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._data_params:
            getattr(self, "_" + param_name).required = not metadata_only

        return metadata_only

    def _old_phase_attributes_compatible(self):
        """
        Check if this object is compatible with the old phase attributes.

        Returns
        -------
        compatible : bool
            True if this object is compatible with the old phase attributes, False
            otherwise
        reason : str
            Reason it is not compatible. None if compatible is True.

        """
        if self.phase_center_catalog is None:
            return True, None

        if self.Nphase > 1:
            return False, "multiple phase centers"
        phase_dict = list(self.phase_center_catalog.values())[0]
        if phase_dict["cat_type"] not in ["sidereal", "unprojected"]:
            return False, f"{phase_dict['cat_type']} phase centers"
        if phase_dict["cat_type"] == "sidereal" and phase_dict["cat_frame"] not in [
            "icrs",
            "gcrs",
        ]:
            return False, f"{phase_dict['cat_frame']} phase frames"
        else:
            return True, None

    def __getattribute__(self, __name):
        """Issue deprecation warnings for old phasing attributes."""
        if __name in old_phase_attrs:
            compatible, reason = self._old_phase_attributes_compatible()
            if not compatible:
                raise ValueError(
                    "The older phase attributes, including "
                    + ", ".join(old_phase_attrs)
                    + " cannot be used with data phased with the new method. This was "
                    + "phased with the new method because it has "
                    + reason
                    + "."
                )
            _warn_old_phase_attr(__name)

            if self.phase_center_catalog is not None:
                phase_dict = list(self.phase_center_catalog.values())[0]
                if __name == "phase_type":
                    if phase_dict["cat_type"] == "unprojected":
                        ret_val = "drift"
                    else:
                        ret_val = "phased"
                elif __name == "phase_center_ra":
                    ret_val = phase_dict["cat_lon"]
                elif __name == "phase_center_dec":
                    ret_val = phase_dict["cat_lat"]
                elif __name == "phase_center_frame":
                    ret_val = phase_dict["cat_frame"]
                elif __name == "phase_center_epoch":
                    ret_val = phase_dict["cat_epoch"]
                elif __name == "object_name":
                    ret_val = phase_dict["cat_name"]
                return ret_val

        return super().__getattribute__(__name)

    def __setattr__(self, __name, __value) -> None:
        """Issue deprecation warnings for old phasing attributes."""
        if __name in old_phase_attrs:
            if (
                self.phase_center_catalog is not None
                and len(self.phase_center_catalog) > 0
            ):
                warnings.warn(
                    f"phase_center_catalog is already set, so {__name}, which is an "
                    "old phase attribute, cannot be set. This will become an error in "
                    "version 3.0",
                    DeprecationWarning,
                )
                return

            _warn_old_phase_attr(__name)

        return super().__setattr__(__name, __value)

    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",)
        for param_name in self._data_params:
            getattr(self, "_" + param_name).form = ("Nblts", "Nfreqs", "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.metadata_only:
            # remove the length-1 spw axis for all data-like parameters
            for param_name in self._data_params:
                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, :]

        if not self.flex_spw:
            # 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 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
        if not self.flex_spw:
            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]

        self.future_array_shapes = False
        for param_name in self._data_params:
            getattr(self, "_" + param_name).form = ("Nblts", 1, "Nfreqs", "Npols")
        if not self.metadata_only:
            for param_name in self._data_params:
                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 known_telescopes(self):
        """
        Get a list of telescopes known to pyuvdata.

        This is just a shortcut to uvdata.telescopes.known_telescopes()

        Returns
        -------
        list of str
            List of names of known telescopes
        """
        return uvtel.known_telescopes()

    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 = []
            for p in telescope_obj:
                telescope_param = getattr(telescope_obj, p)
                self_param = getattr(self, p)
                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:
                        # expected shapes aren't equal. This can happen
                        # e.g. with diameters,
                        # which is a single value on the telescope object but is
                        # an array of length Nants_telescope on the UVData object

                        # use an assert here because we want an error if this condition
                        # isn't true, but it's really an internal consistency check.
                        # This will error if there are changes to the Telescope
                        # object definition, but nothing that a normal user
                        # does will cause an error
                        assert telescope_shape == () and self_shape != "str", (
                            "There is a mismatch in one or more array sizes of the "
                            "UVData and telescope objects. This should not happen, "
                            "if you see this error please make an issue in the "
                            "pyuvdata issue log."
                        )
                        # this parameter is as of this comment most likely a float
                        # since only diameters and antenna positions will probably
                        # trigger this else statement
                        # assign float64 as the type of the array
                        array_val = (
                            np.zeros(self_shape, dtype=np.float64)
                            + telescope_param.value
                        )
                        params_set.append(self_param.name)
                        prop_name = self_param.name
                        setattr(self, prop_name, array_val)

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

    def _calc_single_integration_time(self):
        """
        Calculate a single integration time in seconds when not otherwise specified.

        This function computes the shortest time difference present in the
        time_array, and returns it to be used as the integration time for all
        samples.

        Returns
        -------
        int_time : int
            integration time in seconds to be assigned to all samples in the data.

        """
        # The time_array is in units of days, and integration_time has units of
        # seconds, so we need to convert.
        return np.diff(np.sort(list(set(self.time_array))))[0] * 86400

    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,
            frame=self._telescope_location.frame,
        )
        self.lst_array = unique_lst_array[inverse_inds]
        return

    def _set_app_coords_helper(self, pa_only=False):
        """
        Set values for the apparent coordinate arrays.

        This is an internal helper function, which is not designed to be called by
        users, but rather individual read/write functions for the UVData object.
        Users should use the phase() method for updating/adjusting coordinate values.

        Parameters
        ----------
        pa_only : bool, False
            Skip the calculation of the apparent RA/Dec, and only calculate the
            position angle between `cat_frame` and the apparent coordinate
            system. Useful for reading in data formats that do not calculate a PA.
        """
        if pa_only:
            app_ra = self.phase_center_app_ra
            app_dec = self.phase_center_app_dec
        else:
            app_ra = np.zeros(self.Nblts, dtype=float)
            app_dec = np.zeros(self.Nblts, dtype=float)
            for cat_id in self.phase_center_catalog.keys():
                temp_dict = self.phase_center_catalog[cat_id]
                select_mask = self.phase_center_id_array == cat_id
                cat_type = temp_dict["cat_type"]
                lon_val = temp_dict.get("cat_lon")
                lat_val = temp_dict.get("cat_lat")
                time_val = temp_dict.get("cat_times")
                epoch = temp_dict.get("cat_epoch")
                frame = temp_dict.get("cat_frame")
                pm_ra = temp_dict.get("cat_pm_ra")
                pm_dec = temp_dict.get("cat_pm_dec")
                vrad = temp_dict.get("vrad")
                dist = temp_dict.get("cat_dist")

                app_ra[select_mask], app_dec[select_mask] = uvutils.calc_app_coords(
                    lon_val,
                    lat_val,
                    frame,
                    coord_epoch=epoch,
                    coord_times=time_val,
                    pm_ra=pm_ra,
                    pm_dec=pm_dec,
                    vrad=vrad,
                    dist=dist,
                    time_array=self.time_array[select_mask],
                    lst_array=self.lst_array[select_mask],
                    telescope_loc=self.telescope_location_lat_lon_alt,
                    coord_type=cat_type,
                )

        # Now that we have the apparent coordinates sorted out, we can figure
        # out what it is we want to do with the position angle
        frame_pa = np.zeros(self.Nblts, dtype=float)
        for cat_id in self.phase_center_catalog.keys():
            temp_dict = self.phase_center_catalog[cat_id]
            select_mask = self.phase_center_id_array == cat_id
            if not np.any(select_mask):
                continue
            frame = temp_dict.get("cat_frame")
            epoch = temp_dict.get("cat_epoch")
            if not frame == "altaz":
                frame_pa[select_mask] = uvutils.calc_frame_pos_angle(
                    self.time_array[select_mask],
                    app_ra[select_mask],
                    app_dec[select_mask],
                    self.telescope_location_lat_lon_alt,
                    frame,
                    ref_epoch=epoch,
                )
        self.phase_center_app_ra = app_ra
        self.phase_center_app_dec = app_dec
        self.phase_center_frame_pa = frame_pa

    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 _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). In theory,
        UVH5 and UVData objects can handle this, but MIRIAD, MIR, UVFITS, and MS file
        formats cannot, so we just consider it forbidden.
        """
        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 uvfits & miriad 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.

        """
        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 remove_flex_pol(self, combine_spws=True):
        """
        Convert a flex-pol UVData object into one with a standard polarization axis.

        This will convert a flexible-polarization dataset into one with standard
        polarization handling, which is required for some operations or writing in
        certain filetypes. Note that depending on how it is used, this can inflate
        the size of data-like parameters by up to a factor of Nspws (the true value
        depends on the number of unique entries in `flex_spw_polarization_array`).
        """
        if self.flex_spw_polarization_array is None:
            # There isn't anything to do, so just move along
            return

        unique_pols = np.unique(self.flex_spw_polarization_array)
        n_pols = len(unique_pols)

        if self.Nspws == 1 or n_pols == 1:
            # Just remove the flex_spw_polarization_array and fix the polarization array
            self.polarization_array = unique_pols
            self.flex_spw_polarization_array = None
            return

        if combine_spws:
            # check to see if there are spectral windows that have matching freq_array
            # and channel_width (up to sorting). If so, they need to be combined.
            if self.future_array_shapes:
                freq_array_use = self.freq_array
            else:
                freq_array_use = self.freq_array[0, :]

            # Now find matching sets of spws
            # order spws by order of appearance in flex_spw_id_array
            # this tends to get back to the original order in a convert/remove loop
            spws_remaining = self.flex_spw_id_array[
                np.sort(np.unique(self.flex_spw_id_array, return_index=True)[1])
            ].tolist()
            # key is first spw in a set, value is another dict with keys:
            #  - "freqs" sorted array of frequencies
            #  - "widths" channel widths sorted to match frequencies (using argsort)
            #  - "spws" list of spws in this set
            #  - "pols" list of pols for the spws in this set
            spw_dict = {}
            # key is spw, value is first spw that matches (key into spw_dict)
            first_spw_dict = {}
            while len(spws_remaining) > 0:
                this_spw = spws_remaining[0]
                this_pol = self.flex_spw_polarization_array[self.spw_array == this_spw][
                    0
                ]
                spw_inds = np.nonzero(self.flex_spw_id_array == this_spw)[0]
                spw_inds = spw_inds[np.argsort(freq_array_use[spw_inds])]
                this_freqs = freq_array_use[spw_inds]
                this_widths = self.channel_width[spw_inds]
                match = False
                for spw1, fset1 in spw_dict.items():
                    if np.array_equal(fset1["freqs"], this_freqs) and np.array_equal(
                        fset1["widths"], this_widths
                    ):
                        if this_pol in spw_dict[spw1]["pols"]:
                            raise ValueError(
                                "Some spectral windows have identical frequencies, "
                                "channel widths and polarizations, so spws cannot be "
                                "combined. Set combine_spws=False to avoid this error."
                            )
                        # this spw matches an existing set with no overlapping pols
                        spw_dict[spw1]["spws"].append(this_spw)
                        spw_dict[spw1]["pols"].append(this_pol)
                        first_spw_dict[this_spw] = spw1
                        spws_remaining.remove(this_spw)
                        match = True
                        continue
                if not match:
                    # this spw does not match an existing set
                    spw_dict[this_spw] = {
                        "freqs": this_freqs,
                        "widths": this_widths,
                        "spws": [this_spw],
                        "pols": [this_pol],
                    }
                    first_spw_dict[this_spw] = this_spw
                    spws_remaining.remove(this_spw)

            n_sets = len(spw_dict)
            n_spws_per_set = []
            n_freqs = 0
            reorder_channels = False
            for spw1, spw_set in spw_dict.items():
                n_spws_per_set.append(len(spw_set["spws"]))
                n_freqs += spw_set["freqs"].size
                spw1_mask = self.flex_spw_id_array == spw1
                for spw2 in spw_set["spws"][1:]:
                    spw2_mask = self.flex_spw_id_array == spw2
                    if not (
                        np.array_equal(
                            freq_array_use[spw1_mask], freq_array_use[spw2_mask]
                        )
                        and np.array_equal(
                            self.channel_width[spw1_mask], self.channel_width[spw2_mask]
                        )
                    ):
                        reorder_channels = True

            n_spws_per_set = np.array(n_spws_per_set)

            if not np.all(n_spws_per_set == n_pols):
                # If all the pols are not present in all sets, we cannot combine spws
                warnings.warn(
                    "combine_spws is True but there are not matched spws for all "
                    "polarizations, so spws will not be combined."
                )
                combine_spws = False

        if combine_spws:
            # figure out whether reordering is required to use an inplace reshape
            # Criteria:
            #  1) polarization is the slowest changing axis
            #  2) freqs and spw sets are in the same order for each pol
            reorder_spws = False
            spw_inds = np.zeros_like(self.flex_spw_id_array)
            first_spw_array = np.zeros_like(self.flex_spw_id_array)
            for spw_ind, spw in enumerate(self.spw_array):
                these_freq_inds = np.nonzero(self.flex_spw_id_array == spw)[0]
                spw_inds[these_freq_inds] = spw_ind
                first_spw_array[these_freq_inds] = first_spw_dict[spw]
            pol_array_check = self.flex_spw_polarization_array[spw_inds]
            if not np.nonzero(np.diff(pol_array_check))[0].size == n_pols - 1:
                reorder_spws = True

            pol0_spw_order = first_spw_array[pol_array_check == unique_pols[0]]
            for pol in unique_pols[1:]:
                this_spw_order = first_spw_array[pol_array_check == pol]
                if not np.array_equal(this_spw_order, pol0_spw_order):
                    reorder_spws = True

            if reorder_channels or reorder_spws:
                if reorder_channels:
                    channel_order = "freq"
                else:
                    channel_order = None
                if reorder_spws:
                    # note: spw_order is an index array into spw_array
                    spw_order = np.zeros(self.Nspws, dtype=int)
                    if np.all(unique_pols < 0):
                        # use more standard ordering for polarizations
                        unique_pols = unique_pols[np.argsort(np.abs(unique_pols))]
                    for pol_ind, pol in enumerate(unique_pols):
                        for spw_ind, (_, spw_set) in enumerate(spw_dict.items()):
                            this_ind = pol_ind * n_sets + spw_ind
                            this_spw = np.array(spw_set["spws"])[spw_set["pols"] == pol]
                            spw_order[this_ind] = np.nonzero(
                                self.spw_array == this_spw
                            )[0]
                else:
                    spw_order = None

                self.reorder_freqs(channel_order=channel_order, spw_order=spw_order)

                # recalculate arrays used below
                if self.future_array_shapes:
                    freq_array_use = self.freq_array
                else:
                    freq_array_use = self.freq_array[0, :]
                first_spw_array = np.zeros_like(self.flex_spw_id_array)
                for spw_ind, spw in enumerate(self.spw_array):
                    these_freq_inds = np.nonzero(self.flex_spw_id_array == spw)[0]
                    spw_inds[these_freq_inds] = spw_ind
                    first_spw_array[these_freq_inds] = first_spw_dict[spw]
                pol_array_check = self.flex_spw_polarization_array[spw_inds]

            self.Npols = n_pols
            self.Nspws = n_sets
            self.Nfreqs = n_freqs

            # now things are in the correct order to do a simple reshape
            if self.future_array_shapes:
                self.freq_array = freq_array_use[: self.Nfreqs]
            else:
                self.freq_array = freq_array_use[np.newaxis, : self.Nfreqs]
            self.channel_width = self.channel_width[: self.Nfreqs]

            self.polarization_array = pol_array_check[
                np.sort(np.unique(pol_array_check, return_index=True)[1])
            ]
            self.flex_spw_polarization_array = None

            self.spw_array = first_spw_array[
                np.sort(np.unique(first_spw_array, return_index=True)[1])
            ]
            self.flex_spw_id_array = first_spw_array[: self.Nfreqs]
            if not self.metadata_only:
                # we use the order="F" parameter here to undo the reshape done in
                # `convert_to_flexp_pol` (which uses it to ensure that polarization is
                # the slowest changing axis)
                if self.future_array_shapes:
                    self.data_array = self.data_array.reshape(
                        self.Nblts, self.Nfreqs, self.Npols, order="F"
                    )
                    self.flag_array = self.flag_array.reshape(
                        self.Nblts, self.Nfreqs, self.Npols, order="F"
                    )
                    self.nsample_array = self.nsample_array.reshape(
                        self.Nblts, self.Nfreqs, self.Npols, order="F"
                    )
                else:
                    self.data_array = self.data_array.reshape(
                        self.Nblts, 1, self.Nfreqs, self.Npols, order="F"
                    )
                    self.flag_array = self.flag_array.reshape(
                        self.Nblts, 1, self.Nfreqs, self.Npols, order="F"
                    )
                    self.nsample_array = self.nsample_array.reshape(
                        self.Nblts, 1, self.Nfreqs, self.Npols, order="F"
                    )
            return

        self.Npols = n_pols
        # If we have metadata only, or there was only one pol we were working with,
        # then we do not need to do anything further aside from removing the array
        # associated with flex_spw_polarization_array
        if self.metadata_only:
            self.polarization_array = unique_pols
            self.flex_spw_polarization_array = None
            return

        # Otherwise, move through all of the data params
        self.polarization_array = unique_pols
        for name, param in zip(self._data_params, self.data_like_parameters):
            # We need to construct arrays with the appropriate shape
            new_shape = [self.Nblts, 1, self.Nfreqs, self.Npols]

            # Pop the spw-axis if using future array shapes
            if self.future_array_shapes:
                del new_shape[1]

            # Use full here, since we want zeros if we are working with nsample_array
            # or data_array, otherwise we want True if working w/ flag_array.
            new_param = np.full(new_shape, name == "flag_array", dtype=param.dtype)

            # Now we have to iterate through each spectral window
            for spw, pol in zip(self.spw_array, self.flex_spw_polarization_array):
                pol_idx = np.intersect1d(
                    pol, self.polarization_array, return_indices=True
                )[2][0]
                spw_screen = self.flex_spw_id_array == spw

                # Note that this works because pol_idx is an integer, ergo a simple
                # slice (wherease spw_screen is a complex slice, which we can only have
                # one of for an array).
                if self.future_array_shapes:
                    new_param[:, spw_screen, pol_idx] = param[:, spw_screen, 0]
                else:
                    new_param[:, :, spw_screen, pol_idx] = param[:, :, spw_screen, 0]

            # With the new array defined and filled, set the attribute equal to it
            setattr(self, name, new_param)

        # Finally, remove the flex-pol attribute
        self.flex_spw_polarization_array = None

    def _make_flex_pol(self, raise_error=False, raise_warning=True):
        """
        Convert a regular UVData object into one with flex-polarization enabled.

        This is an internal helper function, which is not designed to be called by
        users, but rather individual read/write functions for the UVData object.
        This will convert a regular UVData object into one that uses flexible
        polarization, which allows for each spectral window to have its own unique
        polarization code, useful for storing data more compactly when different
        windows have different polarizations recorded. Note that at this time,
        only one polarization code per-spw is allowed -- if more than one polarization
        is found to have unflagged data in a given spectral window, then the object
        will not be converted.

        Parameters
        ----------
        raise_error : bool
            If an object cannot be converted to flex-pol, then raise a ValueError.
            Default is False.
        raise_warning : bool
            If an object cannot be converted to flex-pol, and `raise_error=False`, then
            raise a warning that the conversion failed. Default is True.
        """
        if not self.flex_spw:
            msg = "Cannot make a flex-pol UVData object if flex_spw=False."
            if raise_error:
                raise ValueError(msg)
            if raise_warning:
                warnings.warn(msg)
            return

        if self.metadata_only:
            msg = (
                "Cannot make a metadata_only UVData object flex-pol because flagging "
                "info is required. Consider using `convert_to_flex_pol` instead, but "
                "be aware that the behavior is somewhat different"
            )
            if raise_error:
                raise ValueError(msg)
            if raise_warning:
                warnings.warn(msg)
            return

        if self.Npols == 1:
            # This is basically a no-op, fix the to pol-array attributes and exit
            if self.flex_spw_polarization_array is None:
                self.flex_spw_polarization_array = (
                    np.zeros_like(self.spw_array) + self.polarization_array[0]
                )
                self.polarization_array = np.array([0])
            return

        flex_pol_idx = np.zeros_like(self.spw_array)
        for idx, spw in enumerate(self.spw_array):
            spw_screen = self.flex_spw_id_array == spw

            # For each window, we want to check that there is only one polarization with
            # any unflagged data, which we can do by seeing if not all of the flags
            # are set across the non-polarization axes (hence the ~np.all()).
            if self.future_array_shapes:
                pol_check = ~np.all(self.flag_array[:, spw_screen], axis=(0, 1))
            else:
                pol_check = ~np.all(self.flag_array[:, 0, spw_screen], axis=(0, 1))

            if sum(pol_check) > 1:
                msg = (
                    "Cannot make a flex-pol UVData object, as some windows have "
                    "unflagged data in mutiple polarizations."
                )
                if raise_error:
                    raise ValueError(msg)
                if raise_warning:
                    warnings.warn(msg)
                return
            elif not np.any(pol_check):
                flex_pol_idx[idx] = -1
            else:
                flex_pol_idx[idx] = np.where(pol_check)[0][0]

        # If one window was all flagged out, but the others all belong to the same pol,
        # assume we just want that polarization.
        if len(np.unique(flex_pol_idx[flex_pol_idx >= 0])) == 1:
            flex_pol_idx[:] = np.unique(flex_pol_idx[flex_pol_idx >= 0])

        # Now that we have polarizations sorted out, update metadata attributes
        self.flex_spw_polarization_array = self.polarization_array[flex_pol_idx]
        self.polarization_array = np.array([0])
        self.Npols = 1

        # Finally, prep for working w/ data-like attibutes. Start by determining the
        # right shape for the new values, modulo future_array_shapes
        new_shape = [self.Nblts, 1, self.Nfreqs, 1]

        # Pop the spw-axis if using future array shapes
        if self.future_array_shapes:
            del new_shape[1]

        # Now go through one-by-one with data-like parameters and update
        for name, param in zip(self._data_params, self.data_like_parameters):
            # We can use empty here, since we know that we will be filling all
            # values of his array (and empty allows us to forgo the extra overhead
            # of setting all the elements to a particular value).
            new_param = np.empty(new_shape, dtype=param.dtype)

            # Now we have to iterate through each spectral window
            for spw, pol_idx in zip(self.spw_array, flex_pol_idx):
                spw_screen = self.flex_spw_id_array == spw

                # Note that this works because pol_idx is an integer, ergo a simple
                # slice (wherease spw_screen is a complex slice, which we can only have
                # one of for an array).
                if self.future_array_shapes:
                    new_param[:, spw_screen, 0] = param[:, spw_screen, pol_idx]
                else:
                    new_param[:, :, spw_screen, 0] = param[:, :, spw_screen, pol_idx]

            # With the new array defined and filled, set the attribute equal to it
            setattr(self, name, new_param)

    def convert_to_flex_pol(self):
        """
        Convert a regular UVData object into a flex-polarization object.

        This effectively combines the frequency and polarization axis with polarization
        changing slowest. Saving data to uvh5 files this way can speed up some kinds
        of data access.

        """
        if self.flex_spw_polarization_array is not None:
            raise ValueError("This is already a flex-pol object")

        if not self.flex_spw:
            self._set_flex_spw()
            if not self.future_array_shapes:
                self.channel_width = np.full(self.Nfreqs, self.channel_width)
            self.flex_spw_id_array = np.zeros(self.Nfreqs, dtype=int)

        new_spw_array = self.spw_array
        new_flex_pol_array = np.full(self.Nspws, self.polarization_array[0])
        new_spw_id_array = np.zeros((self.Nfreqs, self.Npols), dtype=int)
        for pol_ind, pol in enumerate(self.polarization_array):
            if pol_ind == 0:
                new_spw_id_array[:, 0] = self.flex_spw_id_array
            else:
                for spw in self.spw_array:
                    new_spw = (
                        set(range(self.Nspws * self.Npols + 1))
                        .difference(new_spw_array)
                        .pop()
                    )
                    new_spw_array = np.concatenate((new_spw_array, np.array([new_spw])))
                    new_flex_pol_array = np.concatenate(
                        (new_flex_pol_array, np.array([pol]))
                    )
                    spw_inds = np.nonzero(self.flex_spw_id_array == spw)[0]
                    new_spw_id_array[spw_inds, pol_ind] = new_spw

        spw_sort = np.argsort(new_spw_array)
        self.spw_array = new_spw_array[spw_sort]
        self.flex_spw_polarization_array = new_flex_pol_array[spw_sort]
        # we use the order="F" parameter here to ensure that polarization is the slowest
        # changing axis
        self.flex_spw_id_array = new_spw_id_array.reshape(
            self.Nfreqs * self.Npols, order="F"
        )
        self.Nspws = self.spw_array.size
        self.polarization_array = np.array([0])
        if self.future_array_shapes:
            freq_array_use = self.freq_array
        else:
            freq_array_use = self.freq_array[0, :]
        self.freq_array = np.tile(freq_array_use, self.Npols)
        if not self.future_array_shapes:
            self.freq_array = self.freq_array[np.newaxis, :]
        self.channel_width = np.tile(self.channel_width, self.Npols)
        if not self.metadata_only:
            # we use the order="F" parameter here to ensure that polarization is the
            # slowest changing axis
            if self.future_array_shapes:
                self.data_array = self.data_array.reshape(
                    self.Nblts, self.Nfreqs * self.Npols, 1, order="F"
                )
                self.flag_array = self.flag_array.reshape(
                    self.Nblts, self.Nfreqs * self.Npols, 1, order="F"
                )
                self.nsample_array = self.nsample_array.reshape(
                    self.Nblts, self.Nfreqs * self.Npols, 1, order="F"
                )
            else:
                self.data_array = self.data_array.reshape(
                    self.Nblts, 1, self.Nfreqs * self.Npols, 1, order="F"
                )
                self.flag_array = self.flag_array.reshape(
                    self.Nblts, 1, self.Nfreqs * self.Npols, 1, order="F"
                )
                self.nsample_array = self.nsample_array.reshape(
                    self.Nblts, 1, self.Nfreqs * self.Npols, 1, order="F"
                )

        self.Nfreqs = self.Nfreqs * self.Npols
        self.Npols = 1

        return

    def _calc_nants_data(self):
        """Calculate the number of antennas from ant_1_array and ant_2_array arrays."""
        return int(np.union1d(self.ant_1_array, self.ant_2_array).size)

    def _fix_autos(self):
        """Remove imaginary component of auto-correlations."""
        if self.polarization_array is None or (
            self.ant_1_array is None or self.ant_2_array is None
        ):
            warnings.warn(
                "Cannot use _fix_autos if ant_1_array, ant_2_array, or "
                "polarization_array are None. Leaving data_array untouched."
            )
            return

        # Select out the autos
        auto_screen = self.ant_1_array == self.ant_2_array

        # Only these pols have "true" auto-correlations, that we'd expect
        # to be real only. Select on only them
        auto_pol_list = ["xx", "yy", "rr", "ll", "pI", "pQ", "pU", "pV"]
        pol_screen = np.array(
            [
                uvutils.POL_NUM2STR_DICT[pol] in auto_pol_list
                for pol in self.polarization_array
            ]
        )

        # Make sure we actually have work to do here, otherwise skip all of this
        if (np.any(pol_screen) and np.any(auto_screen)) and not (
            pol_screen is None or auto_screen is None
        ):
            # Select out the relevant data. Need to do this because we have two
            # complex slices we need to do
            auto_data = self.data_array[auto_screen]

            # Set the autos to be real-only by taking the absolute value
            if self.future_array_shapes:
                auto_data[:, :, pol_screen] = np.abs(auto_data[:, :, pol_screen])
            else:
                auto_data[:, :, :, pol_screen] = np.abs(auto_data[:, :, :, pol_screen])

            # Finally, plug the modified values back into data_array
            self.data_array[auto_screen] = auto_data

    def _convert_old_phase_attributes(self):
        """
        Convert old phase attributes set on object to the phase_center_catalog.

        This method can be removed in version 3.0.
        """
        if self.phase_center_catalog is None or len(self.phase_center_catalog) == 0:
            # first handle any old phase parameters that have been added to the object
            old_phase_attrs = [
                "phase_type",
                "phase_center_ra",
                "phase_center_dec",
                "phase_center_frame",
                "phase_center_epoch",
                "object_name",
            ]
            old_phase_info = {}
            for attr in old_phase_attrs:
                with warnings.catch_warnings():
                    warnings.filterwarnings(
                        "ignore", "The older phase attributes, including"
                    )
                    if hasattr(self, attr):
                        old_phase_info[attr] = getattr(self, attr)
            if len(old_phase_info) > 0:
                converted = False
                if "phase_type" in old_phase_info.keys():
                    if old_phase_info["phase_type"] != "phased":
                        if "object_name" in old_phase_info.keys():
                            cat_name = old_phase_info["object_name"]
                        else:
                            cat_name = "unprojected"
                        cat_id = self._add_phase_center(
                            cat_name=cat_name, cat_type="unprojected"
                        )
                        converted = True
                    else:
                        if (
                            "phase_center_ra" in old_phase_info.keys()
                            and "phase_center_dec" in old_phase_info.keys()
                            and "phase_center_frame" in old_phase_info.keys()
                            and "phase_center_epoch" in old_phase_info.keys()
                            and "object_name" in old_phase_info.keys()
                        ):
                            cat_id = self._add_phase_center(
                                cat_name=old_phase_info["object_name"],
                                cat_type="sidereal",
                                cat_lon=old_phase_info["phase_center_ra"],
                                cat_lat=old_phase_info["phase_center_dec"],
                                cat_frame=old_phase_info["phase_center_frame"],
                                cat_epoch=old_phase_info["phase_center_epoch"],
                            )
                            converted = True

                if converted:
                    warnings.warn(
                        "The phase_center_catalog was not set and a complete set of "
                        f"old phase attributes ({', '.join(old_phase_info.keys())}) "
                        "were detected so the old phase attributes were converted into "
                        "the phase_center_catalog. "
                        f"The older attributes including {', '.join(old_phase_attrs)} "
                        "will be removed in version 3.0",
                        DeprecationWarning,
                    )
                    self.phase_center_id_array = np.full(self.Nblts, cat_id, dtype=int)
                    self._set_app_coords_helper()
                    for attr in old_phase_attrs:
                        if attr in old_phase_info.keys():
                            delattr(self, attr)
                else:
                    raise ValueError(
                        "The phase_center_catalog was not set and some old phase "
                        f"attributes ({', '.join(old_phase_info.keys())}) were "
                        "detected, but not enough to define the phase status of the "
                        "object, so they could not be converted into the "
                        "phase_center_catalog. You can use `_add_phase_center_catalog` "
                        "to set the phase_center_catalog."
                    )

    def check(
        self,
        check_extra=True,
        run_check_acceptability=True,
        check_freq_spacing=False,
        strict_uvw_antpos_check=False,
        allow_flip_conj=False,
        check_autos=False,
        fix_autos=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 UVData
            objects in general but is required to write to uvfits and miriad files.
        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.
        allow_flip_conj : bool
            If set to True, and the UVW coordinates do not match antenna positions,
            check and see if flipping the conjugation of the baselines (i.e, multiplying
            the UVWs by -1) resolves  the apparent discrepancy -- and if it does, fix
            the apparent conjugation error in `uvw_array` and `data_array`. Default is
            False.
        check_autos : bool
            Check whether any auto-correlations have non-zero imaginary values in
            data_array (which should not mathematically exist). Default is False.
        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
        -------
        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)

        """
        # first check for any old phase attributes set on the object and move the info
        # into the phase_center_catalog.
        self._convert_old_phase_attributes()

        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."
                )

        # first run the basic check from UVBase

        logger.debug("Doing UVBase check...")
        super(UVData, self).check(
            check_extra=check_extra, run_check_acceptability=run_check_acceptability
        )
        logger.debug("... Done UVBase Check")

        # Check blt axis rectangularity arguments
        if self.time_axis_faster_than_bls and not self.blts_are_rectangular:
            raise ValueError(
                "time_axis_faster_than_bls is True but blts_are_rectangular is False. "
                "This is not allowed."
            )
        if self.time_axis_faster_than_bls:
            if self.Nbls != 1 and self.Ntimes == 1:
                raise ValueError("time_axis_faster_than_bls is True but Ntimes is 1. ")
            if self.Ntimes > 1 and self.time_array[1] == self.time_array[0]:
                raise ValueError(
                    "time_axis_faster_than_bls is True but time_array does not "
                    "move first"
                )

        # Check internal consistency of numbers which don't explicitly correspond
        # to the shape of another array.
        if self.Nants_data != self._calc_nants_data():
            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 "
                f"baselines in the data_array. Got {self.Nbls}, not"
                f"{len(np.unique(self.baseline_array))}"
            )

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

        for val in np.unique(self.phase_center_id_array):
            if val not in self.phase_center_catalog.keys():
                raise ValueError(
                    f"Phase center id {val} is does not have an entry in "
                    "`phase_center_catalog`, which has keys "
                    f"{self.phase_center_catalog.keys()}. All values in "
                    "`phase_center_id_array` must be keys in `phase_center_catalog`. "
                )

        if self.flex_spw_polarization_array is not None:
            # Check that usage of flex_spw_polarization_array follows the rule that
            # each window only has a single polarization per spectral window.
            if self.Npols != 1:
                raise ValueError(
                    "Npols must be equal to 1 if flex_spw_polarization_array is set."
                    f"Got {self.Npols}"
                )
            if np.any(self.polarization_array != 0):
                raise ValueError(
                    "polarization_array must all be equal to 0 if "
                    "flex_spw_polarization_array is set."
                )
        elif np.any(self.polarization_array == 0):
            # If flex_spw_polarization_array is not set, then make sure that
            # no entries in polarization_array are equal to zero.
            raise ValueError(
                "polarization_array may not be equal to 0 if "
                "flex_spw_polarization_array is not set."
            )

        # require that all entries in ant_1_array and ant_2_array exist in
        # antenna_numbers
        logger.debug("Doing Antenna Uniqueness Check...")
        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.")
        logger.debug("... Done Antenna Uniqueness Check")

        # issue warning if extra_keywords keys are longer than 8 characters
        for key in self.extra_keywords.keys():
            if len(key) > 8:
                warnings.warn(
                    f"key {key} in extra_keywords is longer than 8 "
                    "characters. It will be truncated to 8 if written "
                    "to uvfits or miriad file formats."
                )

        # 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(
                    f"{key} in extra_keywords is a list, array or dict, "
                    "which will raise an error when writing uvfits or "
                    "miriad file types"
                )

        if run_check_acceptability:
            # check that the uvws make sense given the antenna positions
            # make a metadata only copy of this object to properly calculate uvws
            temp_obj = self.copy(metadata_only=True)

            with warnings.catch_warnings():
                warnings.simplefilter("ignore")
                logger.debug("Setting UVWs from antenna positions...")
                temp_obj.set_uvws_from_antenna_positions()
                logger.debug("... Done Setting UVWs")

            if not np.allclose(temp_obj.uvw_array, self.uvw_array, atol=1):
                max_diff = np.max(np.abs(temp_obj.uvw_array - self.uvw_array))
                if allow_flip_conj and np.allclose(
                    -temp_obj.uvw_array, self.uvw_array, atol=1
                ):
                    warnings.warn(
                        "UVW orientation appears to be flipped, attempting to "
                        "fix by changing conjugation of baselines."
                    )
                    self.uvw_array *= -1
                    self.data_array = np.conj(self.data_array)
                    logger.info("Flipped Array")
                elif not strict_uvw_antpos_check:
                    warnings.warn(
                        "The uvw_array does not match the expected values given "
                        "the antenna positions. The largest discrepancy is "
                        f"{max_diff} meters. This is a fairly common situation "
                        "but might indicate an error in the antenna positions, "
                        "the uvws or the phasing."
                    )
                else:
                    raise ValueError(
                        "The uvw_array does not match the expected values given "
                        "the antenna positions. The largest discrepancy is "
                        f"{max_diff} meters."
                    )

            # check auto and cross-corrs have sensible uvws
            logger.debug("Checking autos...")
            autos = np.isclose(self.ant_1_array - self.ant_2_array, 0.0)
            if not np.all(
                np.isclose(
                    self.uvw_array[autos],
                    0.0,
                    rtol=self._uvw_array.tols[0],
                    atol=self._uvw_array.tols[1],
                )
            ):
                raise ValueError(
                    "Some auto-correlations have non-zero uvw_array coordinates."
                )
            logger.debug("... Done Checking Autos")
            if (self.data_array is not None and np.any(autos)) and check_autos:
                # Verify here that the autos do not have any imaginary components
                # Only these pols have "true" auto-correlations, that we'd expect
                # to be real only. Select on only them
                auto_pol_list = ["xx", "yy", "rr", "ll", "pI", "pQ", "pU", "pV"]
                if self.flex_spw_polarization_array is not None:
                    pol_screen = np.array(
                        [
                            uvutils.POL_NUM2STR_DICT[pol] in auto_pol_list
                            for pol in self.flex_spw_polarization_array
                        ]
                    )
                    # There should be a better way...
                    spw_inds = np.zeros_like(self.flex_spw_id_array)
                    for spw_ind, spw in enumerate(self.spw_array):
                        these_freq_inds = np.nonzero(self.flex_spw_id_array == spw)[0]
                        spw_inds[these_freq_inds] = spw_ind
                    freq_screen = pol_screen[spw_inds]
                else:
                    pol_screen = np.array(
                        [
                            uvutils.POL_NUM2STR_DICT[pol] in auto_pol_list
                            for pol in self.polarization_array
                        ]
                    )

                # Check autos if they have imag component -- doing iscomplex first and
                # then pol select was faster in every case checked in test files.
                if not np.any(pol_screen):
                    # There's no relevant pols to check, just skip the rest of this
                    auto_imag = False
                else:
                    auto_imag = np.iscomplex(self.data_array[autos])
                    if np.all(pol_screen):
                        auto_imag = np.any(auto_imag)
                    elif self.future_array_shapes:
                        if self.flex_spw_polarization_array is not None:
                            auto_imag = np.any(auto_imag[:, freq_screen])
                        else:
                            auto_imag = np.any(auto_imag[:, :, pol_screen])
                    else:
                        if self.flex_spw_polarization_array is not None:
                            auto_imag = np.any(auto_imag[:, :, freq_screen])
                        else:
                            auto_imag = np.any(auto_imag[:, :, :, pol_screen])
                if auto_imag:
                    if np.all(pol_screen):
                        temp_data = self.data_array[autos]
                    else:
                        auto_data = self.data_array[autos]
                        if self.future_array_shapes:
                            if self.flex_spw_polarization_array is not None:
                                temp_data = auto_data[:, freq_screen]
                            else:
                                temp_data = auto_data[:, :, pol_screen]
                        else:
                            if self.flex_spw_polarization_array is not None:
                                temp_data = auto_data[:, :, freq_screen]
                            else:
                                temp_data = auto_data[:, :, :, pol_screen]
                    temp_data = temp_data[temp_data.imag != 0]
                    max_imag = np.max(np.abs(temp_data.imag))
                    max_imag_ratio = np.max(np.abs(temp_data.imag / temp_data.real))
                    if fix_autos:
                        warnings.warn(
                            "Fixing auto-correlations to be be real-only, after some "
                            "imaginary values were detected in data_array. "
                            f"Largest imaginary component was {max_imag}, largest "
                            f"imaginary/real ratio was {max_imag_ratio}."
                        )
                        self._fix_autos()
                    else:
                        raise ValueError(
                            "Some auto-correlations have non-real values in data_array."
                            f" Largest imaginary component was {max_imag}, largest "
                            f"imaginary/real ratio was {max_imag_ratio}."
                            " You can attempt to fix this by setting fix_autos=True."
                        )

            if np.any(
                np.isclose(
                    # this line used to use np.linalg.norm but it turns out
                    # squaring and sqrt is slightly more efficient unless the array
                    # is "very large".
                    np.sqrt(
                        self.uvw_array[~autos, 0] ** 2
                        + self.uvw_array[~autos, 1] ** 2
                        + self.uvw_array[~autos, 2] ** 2
                    ),
                    0.0,
                    rtol=self._uvw_array.tols[0],
                    atol=self._uvw_array.tols[1],
                )
            ):
                raise ValueError(
                    "Some cross-correlations have near-zero uvw_array magnitudes."
                )

        if check_freq_spacing:
            self._check_freq_spacing()

        return True

    def copy(self, metadata_only=False):
        """
        Make and return a copy of the UVData object.

        Parameters
        ----------
        metadata_only : bool
            If True, only copy the metadata of the object.

        Returns
        -------
        UVData
            Copy of self.
        """
        if not metadata_only:
            return super(UVData, self).copy()
        else:
            uv = UVData()
            # 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)))

        if uv.future_array_shapes:
            for param_name in uv._data_params:
                getattr(uv, "_" + param_name).form = ("Nblts", "Nfreqs", "Npols")

        return uv

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

        Parameters
        ----------
        baseline : int or array_like of int
            baseline number

        Returns
        -------
        int or array_like of int
            first antenna number(s)
        int or array_like of int
            second antenna number(s)
        """
        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.
        """
        return uvutils.antnums_to_baseline(
            ant1, ant2, self.Nants_telescope, attempt256=attempt256
        )

    def antpair2ind(self, ant1, ant2=None, ordered=True):
        """
        Get indices along the baseline-time axis for a given antenna pair.

        This will search for either the key as specified, or the key and its
        conjugate.

        Parameters
        ----------
        ant1, ant2 : int
            Either an antenna-pair key, or key expanded as arguments,
            e.g. antpair2ind( (10, 20) ) or antpair2ind(10, 20)
        ordered : bool
            If True, search for antpair as provided, else search for it and
            its conjugate.

        Returns
        -------
        inds : ndarray of int-64
            indices of the antpair along the baseline-time axis.
        """
        # check for expanded antpair or key
        if ant2 is None:
            if not isinstance(ant1, tuple):
                raise ValueError(
                    "antpair2ind must be fed an antpair tuple "
                    "or expand it as arguments"
                )
            ant2 = ant1[1]
            ant1 = ant1[0]
        else:
            if not isinstance(ant1, (int, np.integer)):
                raise ValueError(
                    "antpair2ind must be fed an antpair tuple or "
                    "expand it as arguments"
                )
        if not isinstance(ordered, (bool, np.bool_)):
            raise ValueError("ordered must be a boolean")

        # if getting auto-corr, ordered must be True
        if ant1 == ant2:
            ordered = True

        # get indices
        inds = np.where((self.ant_1_array == ant1) & (self.ant_2_array == ant2))[0]
        if ordered:
            return inds
        else:
            ind2 = np.where((self.ant_1_array == ant2) & (self.ant_2_array == ant1))[0]
            inds = np.asarray(np.append(inds, ind2), dtype=np.int64)
            return inds

    def _key2inds(self, key):
        """
        Interpret user specified key as antenna pair and/or polarization.

        Parameters
        ----------
        key : tuple of int
            Identifier of data. Key can be length 1, 2, or 3:

            if len(key) == 1:
                if (key < 5) or (type(key) is str):  interpreted as a
                             polarization number/name, return all blts for that pol.
                else: interpreted as a baseline number. Return all times and
                      polarizations for that baseline.

            if len(key) == 2: interpreted as an antenna pair. Return all
                times and pols for that baseline.

            if len(key) == 3: interpreted as antenna pair and pol (ant1, ant2, pol).
                Return all times for that baseline, pol. pol may be a string.

        Returns
        -------
        blt_ind1 : ndarray of int
            blt indices for antenna pair.
        blt_ind2 : ndarray of int
            blt indices for conjugate antenna pair.
            Note if a cross-pol baseline is requested, the polarization will
            also be reversed so the appropriate correlations are returned.
            e.g. asking for (1, 2, 'xy') may return conj(2, 1, 'yx'), which
            is equivalent to the requesting baseline. See utils.conj_pol() for
            complete conjugation mapping.
        pol_ind : tuple of ndarray of int
            polarization indices for blt_ind1 and blt_ind2

        """
        key = uvutils._get_iterable(key)
        if type(key) is str:
            # Single string given, assume it is polarization
            pol_ind1 = np.where(
                self.polarization_array
                == uvutils.polstr2num(key, x_orientation=self.x_orientation)
            )[0]
            if len(pol_ind1) > 0:
                blt_ind1 = np.arange(self.Nblts, dtype=np.int64)
                blt_ind2 = np.array([], dtype=np.int64)
                pol_ind2 = np.array([], dtype=np.int64)
                pol_ind = (pol_ind1, pol_ind2)
            else:
                raise KeyError("Polarization {pol} not found in data.".format(pol=key))
        elif len(key) == 1:
            key = key[0]  # For simplicity
            if isinstance(key, Iterable):
                # Nested tuple. Call function again.
                blt_ind1, blt_ind2, pol_ind = self._key2inds(key)
            elif key < 5:
                # Small number, assume it is a polarization number a la AIPS memo
                pol_ind1 = np.where(self.polarization_array == key)[0]
                if len(pol_ind1) > 0:
                    blt_ind1 = np.arange(self.Nblts)
                    blt_ind2 = np.array([], dtype=np.int64)
                    pol_ind2 = np.array([], dtype=np.int64)
                    pol_ind = (pol_ind1, pol_ind2)
                else:
                    raise KeyError(
                        "Polarization {pol} not found in data.".format(pol=key)
                    )
            else:
                # Larger number, assume it is a baseline number
                inv_bl = self.antnums_to_baseline(
                    self.baseline_to_antnums(key)[1], self.baseline_to_antnums(key)[0]
                )
                blt_ind1 = np.where(self.baseline_array == key)[0]
                blt_ind2 = np.where(self.baseline_array == inv_bl)[0]
                if len(blt_ind1) + len(blt_ind2) == 0:
                    raise KeyError("Baseline {bl} not found in data.".format(bl=key))
                if len(blt_ind1) > 0:
                    pol_ind1 = np.arange(self.Npols)
                else:
                    pol_ind1 = np.array([], dtype=np.int64)
                if len(blt_ind2) > 0:
                    try:
                        pol_ind2 = uvutils.reorder_conj_pols(self.polarization_array)
                    except ValueError as err:
                        if len(blt_ind1) == 0:
                            raise KeyError(
                                f"Baseline {key} not found for polarization "
                                "array in data."
                            ) from err
                        else:
                            pol_ind2 = np.array([], dtype=np.int64)
                            blt_ind2 = np.array([], dtype=np.int64)
                else:
                    pol_ind2 = np.array([], dtype=np.int64)
                pol_ind = (pol_ind1, pol_ind2)
        elif len(key) == 2:
            # Key is an antenna pair
            blt_ind1 = self.antpair2ind(key[0], key[1])
            blt_ind2 = self.antpair2ind(key[1], key[0])
            if len(blt_ind1) + len(blt_ind2) == 0:
                raise KeyError("Antenna pair {pair} not found in data".format(pair=key))
            if len(blt_ind1) > 0:
                pol_ind1 = np.arange(self.Npols)
            else:
                pol_ind1 = np.array([], dtype=np.int64)
            if len(blt_ind2) > 0:
                try:
                    pol_ind2 = uvutils.reorder_conj_pols(self.polarization_array)
                except ValueError as err:
                    if len(blt_ind1) == 0:
                        raise KeyError(
                            f"Baseline {key} not found for polarization array in data."
                        ) from err
                    else:
                        pol_ind2 = np.array([], dtype=np.int64)
                        blt_ind2 = np.array([], dtype=np.int64)
            else:
                pol_ind2 = np.array([], dtype=np.int64)
            pol_ind = (pol_ind1, pol_ind2)
        elif len(key) == 3:
            # Key is an antenna pair + pol
            blt_ind1 = self.antpair2ind(key[0], key[1])
            blt_ind2 = self.antpair2ind(key[1], key[0])
            if len(blt_ind1) + len(blt_ind2) == 0:
                raise KeyError(
                    "Antenna pair {pair} not found in "
                    "data".format(pair=(key[0], key[1]))
                )
            if type(key[2]) is str:
                # pol is str
                if len(blt_ind1) > 0:
                    pol_ind1 = np.where(
                        self.polarization_array
                        == uvutils.polstr2num(key[2], x_orientation=self.x_orientation)
                    )[0]
                else:
                    pol_ind1 = np.array([], dtype=np.int64)
                if len(blt_ind2) > 0:
                    pol_ind2 = np.where(
                        self.polarization_array
                        == uvutils.polstr2num(
                            uvutils.conj_pol(key[2]), x_orientation=self.x_orientation
                        )
                    )[0]
                else:
                    pol_ind2 = np.array([], dtype=np.int64)
            else:
                # polarization number a la AIPS memo
                if len(blt_ind1) > 0:
                    pol_ind1 = np.where(self.polarization_array == key[2])[0]
                else:
                    pol_ind1 = np.array([], dtype=np.int64)
                if len(blt_ind2) > 0:
                    pol_ind2 = np.where(
                        self.polarization_array == uvutils.conj_pol(key[2])
                    )[0]
                else:
                    pol_ind2 = np.array([], dtype=np.int64)
            pol_ind = (pol_ind1, pol_ind2)
            if len(blt_ind1) * len(pol_ind[0]) + len(blt_ind2) * len(pol_ind[1]) == 0:
                raise KeyError(
                    "Polarization {pol} not found in data.".format(pol=key[2])
                )
        # Catch autos
        if np.array_equal(blt_ind1, blt_ind2):
            blt_ind2 = np.array([], dtype=np.int64)
        return (blt_ind1, blt_ind2, pol_ind)

    def _smart_slicing(
        self, data, ind1, ind2, indp, squeeze="default", force_copy=False
    ):
        """
        Quickly get the relevant section of a data-like array.

        Used in get_data, get_flags and get_nsamples.

        Parameters
        ----------
        data : ndarray
            4-dimensional array shaped like self.data_array
        ind1 : array_like of int
            blt indices for antenna pair (e.g. from self._key2inds)
        ind2 : array_like of int
            blt indices for conjugate antenna pair. (e.g. from self._key2inds)
        indp : tuple array_like of int
            polarization indices for ind1 and ind2 (e.g. from self._key2inds)
        squeeze : str
            string specifying how to squeeze the returned array. Options are:
            'default': squeeze pol and spw dimensions if possible;
            'none': no squeezing of resulting numpy array;
            'full': squeeze all length 1 dimensions.
        force_copy : bool
            Option to explicitly make a copy of the data.

        Returns
        -------
        ndarray
            copy (or if possible, a read-only view) of relevant section of data
        """
        p_reg_spaced = [False, False]
        p_start = [0, 0]
        p_stop = [0, 0]
        dp = [1, 1]
        for i, pi in enumerate(indp):
            if len(pi) == 0:
                continue
            if len(set(np.ediff1d(pi))) <= 1:
                p_reg_spaced[i] = True
                p_start[i] = pi[0]
                p_stop[i] = pi[-1] + 1
                if len(pi) != 1:
                    dp[i] = pi[1] - pi[0]

        if len(ind2) == 0:
            # only unconjugated baselines
            if len(set(np.ediff1d(ind1))) <= 1:
                blt_start = ind1[0]
                blt_stop = ind1[-1] + 1
                if len(ind1) == 1:
                    dblt = 1
                else:
                    dblt = ind1[1] - ind1[0]
                if p_reg_spaced[0]:
                    if self.future_array_shapes:
                        out = data[
                            blt_start:blt_stop:dblt, :, p_start[0] : p_stop[0] : dp[0]
                        ]
                    else:
                        out = data[
                            blt_start:blt_stop:dblt,
                            :,
                            :,
                            p_start[0] : p_stop[0] : dp[0],
                        ]
                else:
                    if self.future_array_shapes:
                        out = data[blt_start:blt_stop:dblt, :, indp[0]]
                    else:
                        out = data[blt_start:blt_stop:dblt, :, :, indp[0]]
            else:
                out = data[ind1]
                if p_reg_spaced[0]:
                    if self.future_array_shapes:
                        out = out[:, :, p_start[0] : p_stop[0] : dp[0]]
                    else:
                        out = out[:, :, :, p_start[0] : p_stop[0] : dp[0]]
                else:
                    if self.future_array_shapes:
                        out = out[:, :, indp[0]]
                    else:
                        out = out[:, :, :, indp[0]]
        elif len(ind1) == 0:
            # only conjugated baselines
            if len(set(np.ediff1d(ind2))) <= 1:
                blt_start = ind2[0]
                blt_stop = ind2[-1] + 1
                if len(ind2) == 1:
                    dblt = 1
                else:
                    dblt = ind2[1] - ind2[0]
                if p_reg_spaced[1]:
                    if self.future_array_shapes:
                        out = np.conj(
                            data[
                                blt_start:blt_stop:dblt,
                                :,
                                p_start[1] : p_stop[1] : dp[1],
                            ]
                        )
                    else:
                        out = np.conj(
                            data[
                                blt_start:blt_stop:dblt,
                                :,
                                :,
                                p_start[1] : p_stop[1] : dp[1],
                            ]
                        )
                else:
                    if self.future_array_shapes:
                        out = np.conj(data[blt_start:blt_stop:dblt, :, indp[1]])
                    else:
                        out = np.conj(data[blt_start:blt_stop:dblt, :, :, indp[1]])
            else:
                out = data[ind2]
                if p_reg_spaced[1]:
                    if self.future_array_shapes:
                        out = np.conj(out[:, :, p_start[1] : p_stop[1] : dp[1]])
                    else:
                        out = np.conj(out[:, :, :, p_start[1] : p_stop[1] : dp[1]])
                else:
                    if self.future_array_shapes:
                        out = np.conj(out[:, :, indp[1]])
                    else:
                        out = np.conj(out[:, :, :, indp[1]])
        else:
            # both conjugated and unconjugated baselines
            out = (data[ind1], np.conj(data[ind2]))
            if p_reg_spaced[0] and p_reg_spaced[1]:
                if self.future_array_shapes:
                    out = np.append(
                        out[0][:, :, p_start[0] : p_stop[0] : dp[0]],
                        out[1][:, :, p_start[1] : p_stop[1] : dp[1]],
                        axis=0,
                    )
                else:
                    out = np.append(
                        out[0][:, :, :, p_start[0] : p_stop[0] : dp[0]],
                        out[1][:, :, :, p_start[1] : p_stop[1] : dp[1]],
                        axis=0,
                    )
            else:
                if self.future_array_shapes:
                    out = np.append(
                        out[0][:, :, indp[0]], out[1][:, :, indp[1]], axis=0
                    )
                else:
                    out = np.append(
                        out[0][:, :, :, indp[0]], out[1][:, :, :, indp[1]], axis=0
                    )

        if squeeze == "full":
            out = np.squeeze(out)
        elif squeeze == "default":
            if self.future_array_shapes:
                if out.shape[2] == 1:
                    # one polarization dimension
                    out = np.squeeze(out, axis=2)
            else:
                if out.shape[3] == 1:
                    # one polarization dimension
                    out = np.squeeze(out, axis=3)
                if out.shape[1] == 1:
                    # one spw dimension
                    out = np.squeeze(out, axis=1)
        elif squeeze != "none":
            raise ValueError(
                '"' + str(squeeze) + '" is not a valid option for squeeze.'
                'Only "default", "none", or "full" are allowed.'
            )

        if force_copy:
            out = np.array(out)
        elif out.base is not None:
            # if out is a view rather than a copy, make it read-only
            out.flags.writeable = False

        return out

    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.
        """
        return np.unique(np.append(self.ant_1_array, self.ant_2_array))

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

        Returns
        -------
        ndarray of int
            Array of unique baselines with data associated with them.
        """
        return np.unique(self.baseline_array)

    def get_antpairs(self):
        """
        Get the unique antpair tuples that have data associated with them.

        Returns
        -------
        list of tuples of int
            list of unique antpair tuples (ant1, ant2) with data associated with them.
        """
        return list(zip(*self.baseline_to_antnums(self.get_baseline_nums())))

    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 get_antpairpols(self):
        """
        Get the unique antpair + pol tuples that have data associated with them.

        Returns
        -------
        list of tuples of int
            list of unique antpair + pol tuples (ant1, ant2, pol) with data
            associated with them.
        """
        pols = self.get_pols()
        bls = self.get_antpairs()
        return [(bl) + (pol,) for bl in bls for pol in pols]

    def get_feedpols(self):
        """
        Get the unique antenna feed polarizations in the data.

        Returns
        -------
        list of str
            list of antenna feed polarizations (e.g. ['X', 'Y']) in the data.

        Raises
        ------
        ValueError
            If any pseudo-Stokes visibilities are present
        """
        if np.any(self.polarization_array > 0):
            raise ValueError(
                "Pseudo-Stokes visibilities cannot be interpreted as feed polarizations"
            )
        else:
            return list(set("".join(self.get_pols())))

    def get_data(self, key1, key2=None, key3=None, squeeze="default", force_copy=False):
        """
        Get the data corresonding to a baseline and/or polarization.

        Parameters
        ----------
        key1, key2, key3 : int or tuple of ints
            Identifier of which data to get, can be passed as 1, 2, or 3 arguments
            or as a single tuple of length 1, 2, or 3. These are collectively
            called the key.

            If key is length 1:
                if (key < 5) or (type(key) is str):
                    interpreted as a polarization number/name, get all data for
                    that pol.
                else:
                    interpreted as a baseline number, get all data for that baseline.

            if key is length 2: interpreted as an antenna pair, get all data
                for that baseline.

            if key is length 3: interpreted as antenna pair and pol (ant1, ant2, pol),
                get all data for that baseline, pol. pol may be a string or int.
        squeeze : str
            string specifying how to squeeze the returned array. Options are:
            'default': squeeze pol and spw dimensions if possible;
            'none': no squeezing of resulting numpy array;
            'full': squeeze all length 1 dimensions.
        force_copy : bool
            Option to explicitly make a copy of the data.

        Returns
        -------
        ndarray
            copy (or if possible, a read-only view) of relevant section of data.
            If data exists conjugate to requested antenna pair, it will be conjugated
            before returning.
        """
        key = []
        for val in [key1, key2, key3]:
            if isinstance(val, str):
                key.append(val)
            elif val is not None:
                key += list(uvutils._get_iterable(val))
        if len(key) > 3:
            raise ValueError("no more than 3 key values can be passed")
        ind1, ind2, indp = self._key2inds(key)
        out = self._smart_slicing(
            self.data_array, ind1, ind2, indp, squeeze=squeeze, force_copy=force_copy
        )
        return out

    def get_flags(
        self, key1, key2=None, key3=None, squeeze="default", force_copy=False
    ):
        """
        Get the flags corresonding to a baseline and/or polarization.

        Parameters
        ----------
        key1, key2, key3 : int or tuple of ints
            Identifier of which data to get, can be passed as 1, 2, or 3 arguments
            or as a single tuple of length 1, 2, or 3. These are collectively
            called the key.

            If key is length 1:
                if (key < 5) or (type(key) is str):
                    interpreted as a polarization number/name, get all flags for
                    that pol.
                else:
                    interpreted as a baseline number, get all flags for that baseline.

            if key is length 2: interpreted as an antenna pair, get all flags
                for that baseline.

            if key is length 3: interpreted as antenna pair and pol (ant1, ant2, pol),
                get all flags for that baseline, pol. pol may be a string or int.
        squeeze : str
            string specifying how to squeeze the returned array. Options are:
            'default': squeeze pol and spw dimensions if possible;
            'none': no squeezing of resulting numpy array;
            'full': squeeze all length 1 dimensions.
        force_copy : bool
            Option to explicitly make a copy of the data.

        Returns
        -------
        ndarray
            copy (or if possible, a read-only view) of relevant section of flags.
        """
        key = []
        for val in [key1, key2, key3]:
            if isinstance(val, str):
                key.append(val)
            elif val is not None:
                key += list(uvutils._get_iterable(val))
        if len(key) > 3:
            raise ValueError("no more than 3 key values can be passed")
        ind1, ind2, indp = self._key2inds(key)
        # When we select conjugated baselines, there is a call to np.conj()
        # inside of _smart_slicing to correct the data array. This has the
        # unintended consequence of promoting the dtype of an array of np.bool_
        # to np.int8. Rather than having a bunch of special handling for this
        # ~corner case, we instead explicitly cast back to np.bool_ before we
        # hand back to the user.
        out = self._smart_slicing(
            self.flag_array, ind1, ind2, indp, squeeze=squeeze, force_copy=force_copy
        ).astype(np.bool_)
        return out

    def get_nsamples(
        self, key1, key2=None, key3=None, squeeze="default", force_copy=False
    ):
        """
        Get the nsamples corresonding to a baseline and/or polarization.

        Parameters
        ----------
        key1, key2, key3 : int or tuple of ints
            Identifier of which data to get, can be passed as 1, 2, or 3 arguments
            or as a single tuple of length 1, 2, or 3. These are collectively
            called the key.

            If key is length 1:
                if (key < 5) or (type(key) is str):
                    interpreted as a polarization number/name, get all nsamples for
                    that pol.
                else:
                    interpreted as a baseline number, get all nsamples for that
                    baseline.

            if key is length 2: interpreted as an antenna pair, get all nsamples
                for that baseline.

            if key is length 3: interpreted as antenna pair and pol (ant1, ant2, pol),
                get all nsamples for that baseline, pol. pol may be a string or int.
        squeeze : str
            string specifying how to squeeze the returned array. Options are:
            'default': squeeze pol and spw dimensions if possible;
            'none': no squeezing of resulting numpy array;
            'full': squeeze all length 1 dimensions.
        force_copy : bool
            Option to explicitly make a copy of the data.

        Returns
        -------
        ndarray
            copy (or if possible, a read-only view) of relevant section of
            nsample_array.
        """
        key = []
        for val in [key1, key2, key3]:
            if isinstance(val, str):
                key.append(val)
            elif val is not None:
                key += list(uvutils._get_iterable(val))
        if len(key) > 3:
            raise ValueError("no more than 3 key values can be passed")
        ind1, ind2, indp = self._key2inds(key)
        out = self._smart_slicing(
            self.nsample_array, ind1, ind2, indp, squeeze=squeeze, force_copy=force_copy
        )
        return out

    def get_times(self, key1, key2=None, key3=None):
        """
        Get the times for a given antpair or baseline number.

        Meant to be used in conjunction with get_data function.

        Parameters
        ----------
        key1, key2, key3 : int or tuple of ints
            Identifier of which data to get, can be passed as 1, 2, or 3 arguments
            or as a single tuple of length 1, 2, or 3. These are collectively
            called the key.

            If key is length 1:
                if (key < 5) or (type(key) is str):
                    interpreted as a polarization number/name, get all times.
                else:
                    interpreted as a baseline number, get all times for that baseline.

            if key is length 2: interpreted as an antenna pair, get all times
                for that baseline.

            if key is length 3: interpreted as antenna pair and pol (ant1, ant2, pol),
                get all times for that baseline.

        Returns
        -------
        ndarray
            times from the time_array for the given antpair or baseline.
        """
        key = []
        for val in [key1, key2, key3]:
            if isinstance(val, str):
                key.append(val)
            elif val is not None:
                key += list(uvutils._get_iterable(val))
        if len(key) > 3:
            raise ValueError("no more than 3 key values can be passed")
        inds1, inds2, indp = self._key2inds(key)
        return self.time_array[np.append(inds1, inds2)]

    def get_lsts(self, key1, key2=None, key3=None):
        """
        Get the LSTs for a given antpair or baseline number.

        Meant to be used in conjunction with get_data function.

        Parameters
        ----------
        key1, key2, key3 : int or tuple of ints
            Identifier of which data to get, can be passed as 1, 2, or 3 arguments
            or as a single tuple of length 1, 2, or 3. These are collectively
            called the key.

            If key is length 1:
                if (key < 5) or (type(key) is str):
                    interpreted as a polarization number/name, get all times.
                else:
                    interpreted as a baseline number, get all times for that baseline.

            if key is length 2: interpreted as an antenna pair, get all times
                for that baseline.

            if key is length 3: interpreted as antenna pair and pol (ant1, ant2, pol),
                get all times for that baseline.

        Returns
        -------
        ndarray
            LSTs from the lst_array for the given antpair or baseline.
        """
        key = []
        for val in [key1, key2, key3]:
            if isinstance(val, str):
                key.append(val)
            elif val is not None:
                key += list(uvutils._get_iterable(val))
        if len(key) > 3:
            raise ValueError("no more than 3 key values can be passed")
        inds1, inds2, indp = self._key2inds(key)
        return self.lst_array[np.append(inds1, inds2)]

    def get_ENU_antpos(self, center=False, pick_data_ants=False):
        """
        Get antenna positions in ENU (topocentric) coordinates in units of meters.

        Parameters
        ----------
        center : bool
            If True, subtract median of array position from antpos
        pick_data_ants : bool
            If True, return only antennas found in data

        Returns
        -------
        antpos : ndarray
            Antenna positions in ENU (topocentric) coordinates in units of
            meters, shape=(Nants, 3)
        ants : ndarray
            Antenna numbers matching ordering of antpos, shape=(Nants,)

        """
        antpos = uvutils.ENU_from_ECEF(
            (self.antenna_positions + self.telescope_location),
            *self.telescope_location_lat_lon_alt,
            frame=self._telescope_location.frame,
        )
        ants = self.antenna_numbers

        if pick_data_ants:
            data_ants = np.unique(np.concatenate([self.ant_1_array, self.ant_2_array]))
            telescope_ants = self.antenna_numbers
            select = np.in1d(telescope_ants, data_ants)
            antpos = antpos[select, :]
            ants = telescope_ants[select]

        if center:
            antpos -= np.median(antpos, axis=0)

        return antpos, ants

    def _set_method_helper(self, dshape, key1, key2=None, key3=None):
        """
        Extract the indices for setting data, flags, or nsample arrays.

        This is a helper method designed to work with set_data, set_flags, and
        set_nsamples. Given the shape of the data-like array and the keys
        corresponding to where the data should end up, it finds the indices
        that are needed for the `_index_dset` method.

        Parameters
        ----------
        dshape : tuple of int
            The shape of the data-like array. This is used to ensure the array
            is compatible with the indices selected.
        key1, key2, key3 : int or tuple of ints
            Identifier of which flags to set, can be passed as 1, 2, or 3 arguments
            or as a single tuple of length 1, 2, or 3. These are collectively
            called the key.

            If key is length 1:
                if (key < 5) or (type(key) is str):
                    interpreted as a polarization number/name, set all flags for
                    that pol.
                else:
                    interpreted as a baseline number, set all flags for that baseline.

            if key is length 2: interpreted as an antenna pair, set all flags
                for that baseline.

            if key is length 3: interpreted as antenna pair and pol (ant1, ant2, pol),
                set all flags for that baseline, pol. pol may be a string or int.

        Returns
        -------
        inds : tuple of int
            The indices in the data-like array to slice into.

        Raises
        ------
        ValueError:
            If more than 3 keys are passed, if the requested indices are
            conjugated in the data, if the data array shape is not compatible
            with the indices.

        """
        key = []
        for val in [key1, key2, key3]:
            if isinstance(val, str):
                key.append(val)
            elif val is not None:
                key += list(uvutils._get_iterable(val))
        if len(key) > 3:
            raise ValueError("no more than 3 key values can be passed")
        ind1, ind2, indp = self._key2inds(key)
        if len(ind2) != 0:
            raise ValueError(
                "the requested key is present on the object, but conjugated. Please "
                "conjugate data and keys appropriately and try again"
            )

        if self.future_array_shapes:
            expected_shape = (len(ind1), self.Nfreqs, len(indp[0]))
        else:
            expected_shape = (len(ind1), 1, self.Nfreqs, len(indp[0]))
        if dshape != expected_shape:
            raise ValueError(
                "the input array is not compatible with the shape of the destination. "
                f"Input array shape is {dshape}, expected shape is {expected_shape}."
            )

        blt_slices, blt_sliceable = uvutils._convert_to_slices(
            ind1, max_nslice_frac=0.1
        )
        pol_slices, pol_sliceable = uvutils._convert_to_slices(
            indp[0], max_nslice_frac=0.5
        )

        if self.future_array_shapes:
            inds = [ind1, np.s_[:], indp[0]]
        else:
            inds = [ind1, np.s_[:], np.s_[:], indp[0]]
        if blt_sliceable:
            inds[0] = blt_slices
        if pol_sliceable:
            inds[-1] = pol_slices

        return tuple(inds)

    def set_data(self, data, key1, key2=None, key3=None):
        """
        Set the data array to some values provided by the user.

        Parameters
        ----------
        data : ndarray of complex
            The data to overwrite into the data_array. Must be the same shape as
            the target indices.
        key1, key2, key3 : int or tuple of ints
            Identifier of which data to set, can be passed as 1, 2, or 3 arguments
            or as a single tuple of length 1, 2, or 3. These are collectively
            called the key.

            If key is length 1:
                if (key < 5) or (type(key) is str):
                    interpreted as a polarization number/name, get all data for
                    that pol.
                else:
                    interpreted as a baseline number, get all data for that baseline.

            if key is length 2: interpreted as an antenna pair, get all data
                for that baseline.

            if key is length 3: interpreted as antenna pair and pol (ant1, ant2, pol),
                get all data for that baseline, pol. pol may be a string or int.

        Returns
        -------
        None

        Raises
        ------
        ValueError:
            If more than 3 keys are passed, if the requested indices are
            conjugated in the data, if the data array shape is not compatible
            with the indices.

        """
        dshape = data.shape
        inds = self._set_method_helper(dshape, key1, key2, key3)
        uvutils._index_dset(self.data_array, inds, data)

        return

    def set_flags(self, flags, key1, key2=None, key3=None):
        """
        Set the flag array to some values provided by the user.

        Parameters
        ----------
        flag : ndarray of boolean
            The flags to overwrite into the fkag_array. Must be the same shape
            as the target indices.
        key1, key2, key3 : int or tuple of ints
            Identifier of which flags to set, can be passed as 1, 2, or 3 arguments
            or as a single tuple of length 1, 2, or 3. These are collectively
            called the key.

            If key is length 1:
                if (key < 5) or (type(key) is str):
                    interpreted as a polarization number/name, set all flags for
                    that pol.
                else:
                    interpreted as a baseline number, set all flags for that baseline.

            if key is length 2: interpreted as an antenna pair, set all flags
                for that baseline.

            if key is length 3: interpreted as antenna pair and pol (ant1, ant2, pol),
                set all flags for that baseline, pol. pol may be a string or int.

        Returns
        -------
        None

        Raises
        ------
        ValueError:
            If more than 3 keys are passed, if the requested indices are
            conjugated in the data, if the data array shape is not compatible
            with the indices.

        """
        dshape = flags.shape
        inds = self._set_method_helper(dshape, key1, key2, key3)
        uvutils._index_dset(self.flag_array, inds, flags)

        return

    def set_nsamples(self, nsamples, key1, key2=None, key3=None):
        """
        Set the nsamples array to some values provided by the user.

        Parameters
        ----------
        nsamples : ndarray of float
            The nsamples to overwrite into the nsample_array. Must be the same
            shape as the target indices.
        key1, key2, key3 : int or tuple of ints
            Identifier of which nsamples to set, can be passed as 1, 2, or 3
            arguments or as a single tuple of length 1, 2, or 3. These are
            collectively called the key.

            If key is length 1:
                if (key < 5) or (type(key) is str):
                    interpreted as a polarization number/name, set all data for
                    that pol.
                else:
                    interpreted as a baseline number, set all nsamples for that
                    baseline.

            if key is length 2: interpreted as an antenna pair, set all nsamples
                for that baseline.

            if key is length 3: interpreted as antenna pair and pol (ant1, ant2,
                pol), set all nsamples for that baseline, pol. pol may be a
                string or int.

        Returns
        -------
        None

        Raises
        ------
        ValueError:
            If more than 3 keys are passed, if the requested indices are
            conjugated in the data, if the data array shape is not compatible
            with the indices.

        """
        dshape = nsamples.shape
        inds = self._set_method_helper(dshape, key1, key2, key3)
        uvutils._index_dset(self.nsample_array, inds, nsamples)

        return

    def antpairpol_iter(self, squeeze="default"):
        """
        Iterate the data for each antpair, polarization combination.

        Parameters
        ----------
        squeeze : str
            string specifying how to squeeze the returned array. Options are:
            'default': squeeze pol and spw dimensions if possible;
            'none': no squeezing of resulting numpy array;
            'full': squeeze all length 1 dimensions.

        Yields
        ------
        key : tuple
            antenna1, antenna2, and polarization string
        data : ndarray of complex
            data for the ant pair and polarization specified in key
        """
        antpairpols = self.get_antpairpols()
        for key in antpairpols:
            yield (key, self.get_data(key, squeeze=squeeze))

    def conjugate_bls(self, convention="ant1<ant2", use_enu=True, uvw_tol=0.0):
        """
        Conjugate baselines according to one of the supported conventions.

        This will fail if only one of the cross pols is present (because
        conjugation requires changing the polarization number for cross pols).

        Parameters
        ----------
        convention : str or array_like of int
            A convention for the directions of the baselines, options are:
            'ant1<ant2', 'ant2<ant1', 'u<0', 'u>0', 'v<0', 'v>0' or an
            index array of blt indices to conjugate.
        use_enu : bool
            Use true antenna positions to determine uv location (as opposed to
            uvw array). Only applies if `convention` is 'u<0', 'u>0', 'v<0', 'v>0'.
            Set to False to use uvw array values.
        uvw_tol : float
            Defines a tolerance on uvw coordinates for setting the
            u>0, u<0, v>0, or v<0 conventions. Defaults to 0m.

        Raises
        ------
        ValueError
            If convention is not an allowed value or if not all conjugate pols exist.

        """
        if isinstance(convention, (np.ndarray, list, tuple)):
            convention = np.array(convention)
            if (
                np.max(convention) >= self.Nblts
                or np.min(convention) < 0
                or convention.dtype not in [int, np.int_, np.int32, np.int64]
            ):
                raise ValueError(
                    "If convention is an index array, it must "
                    "contain integers and have values greater "
                    "than zero and less than NBlts"
                )
        else:
            if convention not in ["ant1<ant2", "ant2<ant1", "u<0", "u>0", "v<0", "v>0"]:
                raise ValueError(
                    "convention must be one of 'ant1<ant2', "
                    "'ant2<ant1', 'u<0', 'u>0', 'v<0', 'v>0' or "
                    "an index array with values less than NBlts"
                )

        if isinstance(convention, str):
            if convention in ["u<0", "u>0", "v<0", "v>0"]:
                if use_enu is True:
                    enu, anum = self.get_ENU_antpos()
                    anum = anum.tolist()
                    uvw_array_use = np.zeros_like(self.uvw_array)
                    for i in range(self.baseline_array.size):
                        a1, a2 = self.ant_1_array[i], self.ant_2_array[i]
                        i1, i2 = anum.index(a1), anum.index(a2)
                        uvw_array_use[i, :] = enu[i2] - enu[i1]
                else:
                    uvw_array_use = copy.copy(self.uvw_array)

            if convention == "ant1<ant2":
                index_array = np.asarray(self.ant_1_array > self.ant_2_array).nonzero()
            elif convention == "ant2<ant1":
                index_array = np.asarray(self.ant_2_array > self.ant_1_array).nonzero()
            elif convention == "u<0":
                index_array = np.asarray(
                    (uvw_array_use[:, 0] > uvw_tol)
                    | (uvw_array_use[:, 1] > uvw_tol)
                    & np.isclose(uvw_array_use[:, 0], 0, atol=uvw_tol)
                    | (uvw_array_use[:, 2] > uvw_tol)
                    & np.isclose(uvw_array_use[:, 0], 0, atol=uvw_tol)
                    & np.isclose(uvw_array_use[:, 1], 0, atol=uvw_tol)
                ).nonzero()
            elif convention == "u>0":
                index_array = np.asarray(
                    (uvw_array_use[:, 0] < -uvw_tol)
                    | (
                        (uvw_array_use[:, 1] < -uvw_tol)
                        & np.isclose(uvw_array_use[:, 0], 0, atol=uvw_tol)
                    )
                    | (
                        (uvw_array_use[:, 2] < -uvw_tol)
                        & np.isclose(uvw_array_use[:, 0], 0, atol=uvw_tol)
                        & np.isclose(uvw_array_use[:, 1], 0, atol=uvw_tol)
                    )
                ).nonzero()
            elif convention == "v<0":
                index_array = np.asarray(
                    (uvw_array_use[:, 1] > uvw_tol)
                    | (uvw_array_use[:, 0] > uvw_tol)
                    & np.isclose(uvw_array_use[:, 1], 0, atol=uvw_tol)
                    | (uvw_array_use[:, 2] > uvw_tol)
                    & np.isclose(uvw_array_use[:, 0], 0, atol=uvw_tol)
                    & np.isclose(uvw_array_use[:, 1], 0, atol=uvw_tol)
                ).nonzero()
            elif convention == "v>0":
                index_array = np.asarray(
                    (uvw_array_use[:, 1] < -uvw_tol)
                    | (uvw_array_use[:, 0] < -uvw_tol)
                    & np.isclose(uvw_array_use[:, 1], 0, atol=uvw_tol)
                    | (uvw_array_use[:, 2] < -uvw_tol)
                    & np.isclose(uvw_array_use[:, 0], 0, atol=uvw_tol)
                    & np.isclose(uvw_array_use[:, 1], 0, atol=uvw_tol)
                ).nonzero()
        else:
            index_array = convention

        if index_array[0].size > 0:
            new_pol_inds = uvutils.reorder_conj_pols(self.polarization_array)

            self.uvw_array[index_array] *= -1

            if not self.metadata_only:
                orig_data_array = copy.copy(self.data_array)
                for pol_ind in np.arange(self.Npols):
                    if self.future_array_shapes:
                        self.data_array[
                            index_array, :, new_pol_inds[pol_ind]
                        ] = np.conj(orig_data_array[index_array, :, pol_ind])
                    else:
                        self.data_array[
                            index_array, :, :, new_pol_inds[pol_ind]
                        ] = np.conj(orig_data_array[index_array, :, :, pol_ind])

            ant_1_vals = self.ant_1_array[index_array]
            ant_2_vals = self.ant_2_array[index_array]
            self.ant_1_array[index_array] = ant_2_vals
            self.ant_2_array[index_array] = ant_1_vals
            self.baseline_array[index_array] = self.antnums_to_baseline(
                self.ant_1_array[index_array], self.ant_2_array[index_array]
            )
            self.Nbls = np.unique(self.baseline_array).size

    def reorder_pols(
        self,
        order="AIPS",
        run_check=True,
        check_extra=True,
        run_check_acceptability=True,
        strict_uvw_antpos_check=False,
    ):
        """
        Arrange polarization axis according to desired order.

        Parameters
        ----------
        order : str
            Either a string specifying a canonical ordering ('AIPS' or 'CASA')
            or an index array of length Npols that specifies how to shuffle the
            data (this is not the desired final pol order).
            CASA ordering has cross-pols in between (e.g. XX,XY,YX,YY)
            AIPS ordering has auto-pols followed by cross-pols (e.g. XX,YY,XY,YX)
            Default ('AIPS') will sort by absolute value of pol values.
        run_check : bool
            Option to check for the existence and proper shapes of parameters
            after reordering.
        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
            reordering.
        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.

        Raises
        ------
        ValueError
            If the order is not one of the allowed values.

        """
        if isinstance(order, (np.ndarray, list, tuple)):
            order = np.array(order)
            if (
                order.size != self.Npols
                or order.dtype not in [int, np.int_, np.int32, np.int64]
                or np.min(order) < 0
                or np.max(order) >= self.Npols
            ):
                raise ValueError(
                    "If order is an index array, it must "
                    "contain integers and be length Npols."
                )
            index_array = order
        elif order == "AIPS":
            index_array = np.argsort(np.abs(self.polarization_array))
        elif order == "CASA":
            casa_order = np.array([1, 2, 3, 4, -1, -3, -4, -2, -5, -7, -8, -6])
            pol_inds = []
            for pol in self.polarization_array:
                pol_inds.append(np.where(casa_order == pol)[0][0])
            index_array = np.argsort(pol_inds)
        else:
            raise ValueError(
                "order must be one of: 'AIPS', 'CASA', or an "
                "index array of length Npols"
            )

        self.polarization_array = self.polarization_array[index_array]
        if not self.metadata_only:
            # data array is special and large, take is faster here
            if self.future_array_shapes:
                self.data_array = np.take(self.data_array, index_array, axis=2)
                self.nsample_array = self.nsample_array[:, :, index_array]
                self.flag_array = self.flag_array[:, :, index_array]
            else:
                self.data_array = np.take(self.data_array, index_array, axis=3)
                self.nsample_array = self.nsample_array[:, :, :, index_array]
                self.flag_array = self.flag_array[:, :, :, index_array]

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

    def set_rectangularity(self, force: bool = False) -> None:
        """
        Set the rectangularity attributes of the object.

        Parameters
        ----------
        force : bool, optional
            Whether to foce setting the rectangularity attributes, even if they are
            unset. Default is to leave them unset if they are not already set, but
            otherwise just ensure correctness.

        Returns
        -------
        None
        """
        if not force and self.blts_are_rectangular is None:
            self.time_axis_faster_than_bls = None
            return

        rect, time = uvutils.determine_rectangularity(
            self.time_array,
            self.baseline_array,
            self.Nbls,
            self.Ntimes,
            blt_order=self.blt_order,
        )
        self.blts_are_rectangular = rect
        self.time_axis_faster_than_bls = time

    def reorder_blts(
        self,
        order="time",
        minor_order=None,
        autos_first=False,
        conj_convention=None,
        uvw_tol=0.0,
        conj_convention_use_enu=True,
        run_check=True,
        check_extra=True,
        run_check_acceptability=True,
        strict_uvw_antpos_check=False,
    ):
        """
        Arrange baseline-times axis according to desired order.

        If the specified order is an index type, it will be the slowest changing index
        along the baseline-time axis (the minor order will be the next slowest).
        If the `conj_convention` is set, this method can also conjugate some baselines
        using the `conjugate_bls` method.

        Parameters
        ----------
        order : str or array_like of int
            A string describing the desired order along the blt axis or an
            index array of length Nblts that specifies the new order.
            If a string, the options are: `time`, `baseline`, `ant1`, `ant2`, `bda`.
            If this is `time`, `baseline`, `ant1`, `ant2` then this will be the
            slowest changing index.
        minor_order : str
            Optionally specify a secondary ordering.
            Default depends on how order is set:
            if order is 'time', this defaults to `baseline`,
            if order is `ant1`, or `ant2` this defaults to the other antenna,
            if order is `baseline` the only allowed value is `time`.
            Ignored if order is `bda` or an integer array.
            If this is the same as order, it is reset to the default.
            This will be the next slowest changing index.
        autos_first : bool
            If True, sort the autos before all the crosses. The autos and crosses will
            each be sorted according to the order and minor order keywords. Ignored if
            order is an integer array.
        conj_convention : str or array_like of int
            Optionally conjugate baselines to make the baselines have the
            desired orientation. See conjugate_bls for allowed values and details.
        uvw_tol : float
            If conjugating baselines, sets a tolerance for determining the signs
            of u,v, and w, and whether or not they are zero.
            See conjugate_bls for details.
        conj_convention_use_enu: bool
            If `conj_convention` is set, this is passed to conjugate_bls, see that
            method for details.
        run_check : bool
            Option to check for the existence and proper shapes of parameters
            after reordering.
        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
            reordering.
        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.

        Raises
        ------
        ValueError
            If parameter values are inappropriate

        """
        if isinstance(order, (np.ndarray, list, tuple)):
            order = np.array(order)
            if order.size != self.Nblts or order.dtype not in [
                int,
                np.int_,
                np.int32,
                np.int64,
            ]:
                raise ValueError(
                    "If order is an index array, it must "
                    "contain integers and be length Nblts."
                )
            if minor_order is not None:
                raise ValueError(
                    "Minor order cannot be set if order is an index array."
                )
        else:
            if order not in ["time", "baseline", "ant1", "ant2", "bda"]:
                raise ValueError(
                    "order must be one of 'time', 'baseline', "
                    "'ant1', 'ant2', 'bda' or an index array of "
                    "length Nblts"
                )

            if minor_order == order:
                minor_order = None

            if minor_order is not None:
                if minor_order not in ["time", "baseline", "ant1", "ant2"]:
                    raise ValueError(
                        "minor_order can only be one of 'time', "
                        "'baseline', 'ant1', 'ant2'"
                    )
                if isinstance(order, np.ndarray) or order == "bda":
                    raise ValueError(
                        "minor_order cannot be specified if order is "
                        "'bda' or an index array."
                    )
                if order == "baseline":
                    if minor_order in ["ant1", "ant2"]:
                        raise ValueError("minor_order conflicts with order")
            else:
                if order == "time":
                    minor_order = "baseline"
                elif order == "ant1":
                    minor_order = "ant2"
                elif order == "ant2":
                    minor_order = "ant1"
                elif order == "baseline":
                    minor_order = "time"

        if conj_convention is not None:
            self.conjugate_bls(
                convention=conj_convention,
                use_enu=conj_convention_use_enu,
                uvw_tol=uvw_tol,
            )

        if isinstance(order, str):
            if minor_order is None:
                self.blt_order = (order,)
                self._blt_order.form = (1,)
            else:
                self.blt_order = (order, minor_order)
                # set it back to the right shape in case it was set differently before
                self._blt_order.form = (2,)
        else:
            self.blt_order = None

        if autos_first:
            # find the auto indices
            auto_inds = np.nonzero(self.ant_1_array == self.ant_2_array)[0]
            cross_inds = np.nonzero(self.ant_1_array != self.ant_2_array)[0]
            inds_use_list = [auto_inds, cross_inds]
        else:
            inds_use_list = [np.arange(self.Nblts)]

        if not isinstance(order, np.ndarray):
            index_array = []
            for inds_use in inds_use_list:
                if inds_use.size == 0:
                    continue
                # Use lexsort to sort along different arrays in defined order.
                if order == "time":
                    arr1 = self.time_array[inds_use]
                    if minor_order == "ant1":
                        arr2 = self.ant_1_array[inds_use]
                        arr3 = self.ant_2_array[inds_use]
                    elif minor_order == "ant2":
                        arr2 = self.ant_2_array[inds_use]
                        arr3 = self.ant_1_array[inds_use]
                    else:
                        # minor_order is baseline
                        arr2 = self.baseline_array[inds_use]
                        arr3 = self.baseline_array[inds_use]
                elif order == "ant1":
                    arr1 = self.ant_1_array[inds_use]
                    if minor_order == "time":
                        arr2 = self.time_array[inds_use]
                        arr3 = self.ant_2_array[inds_use]
                    elif minor_order == "ant2":
                        arr2 = self.ant_2_array[inds_use]
                        arr3 = self.time_array[inds_use]
                    else:  # minor_order is baseline
                        arr2 = self.baseline_array[inds_use]
                        arr3 = self.time_array[inds_use]
                elif order == "ant2":
                    arr1 = self.ant_2_array[inds_use]
                    if minor_order == "time":
                        arr2 = self.time_array[inds_use]
                        arr3 = self.ant_1_array[inds_use]
                    elif minor_order == "ant1":
                        arr2 = self.ant_1_array[inds_use]
                        arr3 = self.time_array[inds_use]
                    else:
                        # minor_order is baseline
                        arr2 = self.baseline_array[inds_use]
                        arr3 = self.time_array[inds_use]
                elif order == "baseline":
                    arr1 = self.baseline_array[inds_use]
                    # only allowed minor order is time
                    arr2 = self.time_array[inds_use]
                    arr3 = self.time_array[inds_use]
                elif order == "bda":
                    arr1 = self.integration_time[inds_use]
                    # only allowed minor order is time
                    arr2 = self.baseline_array[inds_use]
                    arr3 = self.time_array[inds_use]

                # lexsort uses the listed arrays from last to first
                # (so the primary sort is on the last one)
                index_array.extend(inds_use[np.lexsort((arr3, arr2, arr1))].tolist())
        else:
            index_array = order

        index_array = np.asarray(index_array, dtype=int)

        # actually do the reordering
        self.ant_1_array = self.ant_1_array[index_array]
        self.ant_2_array = self.ant_2_array[index_array]
        self.baseline_array = self.baseline_array[index_array]
        self.uvw_array = self.uvw_array[index_array, :]
        self.time_array = self.time_array[index_array]
        self.lst_array = self.lst_array[index_array]
        self.integration_time = self.integration_time[index_array]
        self.phase_center_app_ra = self.phase_center_app_ra[index_array]
        self.phase_center_app_dec = self.phase_center_app_dec[index_array]
        self.phase_center_frame_pa = self.phase_center_frame_pa[index_array]
        self.phase_center_id_array = self.phase_center_id_array[index_array]

        if not self.metadata_only:
            self.data_array = self.data_array[index_array]
            self.flag_array = self.flag_array[index_array]
            self.nsample_array = self.nsample_array[index_array]

        self.set_rectangularity(force=True)

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

    def reorder_freqs(
        self,
        spw_order=None,
        channel_order=None,
        select_spw=None,
        run_check=True,
        check_extra=True,
        run_check_acceptability=True,
        strict_uvw_antpos_check=False,
    ):
        """
        Arrange frequency axis according to desired order.

        Can be applied across the entire frequency axis, or just a subset.

        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.
        run_check : bool
            Option to check for the existence and proper shapes of parameters
            after reordering.
        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
            reordering.
        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.

        Returns
        -------
        None

        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.

        """
        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

        # Now update all of the arrays.
        if self.future_array_shapes:
            self.freq_array = self.freq_array[index_array]
            if not self.metadata_only:
                self.data_array = self.data_array[:, index_array, :]
                self.flag_array = self.flag_array[:, index_array, :]
                self.nsample_array = self.nsample_array[:, index_array, :]
        else:
            self.freq_array = self.freq_array[:, index_array]
            if not self.metadata_only:
                self.data_array = self.data_array[:, :, index_array, :]
                self.flag_array = self.flag_array[:, :, index_array, :]
                self.nsample_array = self.nsample_array[:, :, index_array, :]
        if self.flex_spw_id_array is not None:
            self.flex_spw_id_array = self.flex_spw_id_array[index_array]

            # 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]
            )
            new_spw_array = self.flex_spw_id_array[unique_index]
            if self.flex_spw_polarization_array is not None:
                spw_sort_inds = np.zeros_like(self.spw_array)
                for idx, spw in enumerate(new_spw_array):
                    spw_sort_inds[idx] = np.nonzero(self.spw_array == spw)[0]
                self.flex_spw_polarization_array = self.flex_spw_polarization_array[
                    spw_sort_inds
                ]
            self.spw_array = new_spw_array

        if self.flex_spw or self.future_array_shapes:
            self.channel_width = self.channel_width[index_array]

        if self.eq_coeffs is not None:
            self.eq_coeffs = self.eq_coeffs[:, index_array]
        # check if object is self-consistent
        if run_check:
            self.check(
                check_extra=check_extra,
                run_check_acceptability=run_check_acceptability,
                strict_uvw_antpos_check=strict_uvw_antpos_check,
            )

    def remove_eq_coeffs(self):
        """
        Remove equalization coefficients from the data.

        Some telescopes, e.g. HERA, apply per-antenna, per-frequency gain
        coefficients as part of the signal chain. These are stored in the
        `eq_coeffs` attribute of the object. This method will remove them, so
        that the data are in "unnormalized" raw units.

        Parameters
        ----------
        None

        Returns
        -------
        None

        Raises
        ------
        ValueError
            Raised if eq_coeffs or eq_coeffs_convention are not defined on the
            object, or if eq_coeffs_convention is not one of "multiply" or "divide".
        """
        if self.eq_coeffs is None:
            raise ValueError(
                "The eq_coeffs attribute must be defined on the object to apply them."
            )
        if self.eq_coeffs_convention is None:
            raise ValueError(
                "The eq_coeffs_convention attribute must be defined on the object "
                "to apply them."
            )
        if self.eq_coeffs_convention not in ("multiply", "divide"):
            raise ValueError(
                "Got unknown convention {}. Must be one of: "
                '"multiply", "divide"'.format(self.eq_coeffs_convention)
            )

        # apply coefficients for each baseline
        for key in self.get_antpairs():
            # get indices for this key
            blt_inds = self.antpair2ind(key)

            ant1_index = np.asarray(self.antenna_numbers == key[0]).nonzero()[0][0]
            ant2_index = np.asarray(self.antenna_numbers == key[1]).nonzero()[0][0]

            eq_coeff1 = self.eq_coeffs[ant1_index, :]
            eq_coeff2 = self.eq_coeffs[ant2_index, :]

            # make sure coefficients are the right size to broadcast
            eq_coeff1 = np.repeat(eq_coeff1[:, np.newaxis], self.Npols, axis=1)
            eq_coeff2 = np.repeat(eq_coeff2[:, np.newaxis], self.Npols, axis=1)

            if self.eq_coeffs_convention == "multiply":
                self.data_array[blt_inds] *= eq_coeff1 * eq_coeff2
            else:
                self.data_array[blt_inds] /= eq_coeff1 * eq_coeff2

        return

    def _apply_w_proj(self, new_w_vals, old_w_vals, select_mask=None):
        """
        Apply corrections based on changes to w-coord.

        Adjusts the data to account for a change along the w-axis of a baseline.

        Parameters
        ----------
        new_w_vals: float or ndarray of float
            New w-coordinates for the baselines, in units of meters. Can either be a
            solitary float (helpful for unphasing data, where new_w_vals can be set to
            0.0) or an array of shape (Nselect,) (which is Nblts if select_mask=None).
        old_w_vals: float or ndarray of float
            Old w-coordinates for the baselines, in units of meters. Can either be a
            solitary float (helpful for unphasing data, where new_w_vals can be set to
            0.0) or an array of shape (Nselect,) (which is Nblts if select_mask=None).
        select_mask: ndarray of bool
            Array is of shape (Nblts,), where the sum of all enties marked True is
            equal to Nselect (mentioned above).

        Raises
        ------
        IndexError
            If the length of new_w_vals or old_w_vals isn't compatible with
            select_mask, or if select mask isn't the right length.
        """
        # If we only have metadata, then we have no work to do. W00t!
        if self.metadata_only or (self.data_array is None):
            return

        if select_mask is None:
            select_len = self.Nblts
        else:
            try:
                inv_mask = np.ones(self.Nblts, dtype=bool)
                inv_mask[select_mask] = False
                select_mask = ~inv_mask
                select_len = np.sum(select_mask)
            except IndexError as err:
                raise IndexError(
                    "select_mask must be an array-like, either of ints with shape "
                    "(Nblts), or of ints within the range (-Nblts, Nblts)."
                ) from err

        # Promote everything to float64 ndarrays if they aren't already
        old_w_vals = np.array(old_w_vals, dtype=np.float64)
        old_w_vals.shape += (1,) if (old_w_vals.ndim == 0) else ()
        new_w_vals = np.array(new_w_vals, dtype=np.float64)
        new_w_vals.shape += (1,) if (new_w_vals.ndim == 0) else ()

        # Make sure the lengths of everything make sense
        new_val_len = len(new_w_vals)
        old_val_len = len(old_w_vals)

        if new_val_len not in [1, select_len]:
            raise IndexError(
                f"The length of new_w_vals is wrong (expected 1 or {select_len}, "
                f"got {new_val_len})!"
            )
        if old_val_len not in [1, select_len]:
            raise IndexError(
                f"The length of old_w_vals is wrong (expected 1 or {select_len}, "
                f"got {old_val_len})!"
            )

        # Calculate the difference in w terms as a function of freq. Note that the
        # 1/c is there to speed of processing (faster to multiply than divide)
        delta_w_lambda = (
            (new_w_vals - old_w_vals).reshape(-1, 1)
            * (1.0 / const.c.to("m/s").value)
            * self.freq_array.reshape(1, self.Nfreqs)
        )
        if select_mask is None or np.all(select_mask):
            # If all the w values are changing, it turns out to be twice as fast
            # to ditch any sort of selection mask and just do the full multiply.
            if self.future_array_shapes:
                self.data_array *= np.exp(
                    (-1j * 2 * np.pi) * delta_w_lambda[:, :, None]
                )
            else:
                self.data_array *= np.exp(
                    (-1j * 2 * np.pi) * delta_w_lambda[:, None, :, None]
                )
        elif np.any(select_mask):
            # In the case we are _not_ doing all baselines, use a selection mask to
            # only update the values we need. In the worse case, it slows down the
            # processing by ~2x, but it can save a lot on time and memory if only
            # needing to update a select number of baselines.
            if self.future_array_shapes:
                self.data_array[select_mask] *= np.exp(
                    (-1j * 2 * np.pi) * delta_w_lambda[:, :, None]
                )
            else:
                self.data_array[select_mask] *= np.exp(
                    (-1j * 2 * np.pi) * delta_w_lambda[:, None, :, None]
                )

    def unphase_to_drift(self, phase_frame=None, use_ant_pos=True, use_old_proj=False):
        """
        Undo phasing to get back to an `unprojected` state.

        Deprecated. Use "unproject_phase" instead.
        See the phasing memo under docs/references for more documentation.

        Parameters
        ----------
        phase_frame : str
            Deprecated, will be removed in version 2.4. Only used if use_old_proj is
            True. The astropy frame to phase from Either 'icrs' or 'gcrs'. 'gcrs'
            accounts for precession & nutation, 'icrs' also includes abberation.
            Defaults to using the "cat_frame" value in the `phase_center_catalog`.
        use_ant_pos : bool
            If True, calculate the uvws directly from the antenna positions
            rather than from the existing uvws. Default is True.
        use_old_proj : bool
            Deprecates, will be removed in version 2.4. If True, uses the 'old' way of
            calculating baseline projections. In the future, if a dataset is known to
            be phased using the old way, use the `fix_phase` method to fix the phasing
            before calling this method.

        Raises
        ------
        ValueError
            If the object is alread unprojected.
        """
        warnings.warn(
            "This method is deprecated in favor of `unproject_phase`, which better "
            "describes what is being done. This method will be removed in version 2.4.",
            DeprecationWarning,
        )
        self.unproject_phase(
            phase_frame=phase_frame, use_ant_pos=use_ant_pos, use_old_proj=use_old_proj
        )

    def unproject_phase(
        self,
        phase_frame=None,
        use_ant_pos=True,
        use_old_proj=False,
        select_mask=None,
        cat_name="unprojected",
    ):
        """
        Undo phasing to get back to an `unprojected` state.

        See the phasing memo under docs/references for more documentation.

        Parameters
        ----------
        phase_frame : str
            Deprecated, will be removed in version 2.4. Only used if use_old_proj is
            True. The astropy frame to phase from, either 'icrs' or 'gcrs'. 'gcrs'
            accounts for precession & nutation, 'icrs' also includes abberation.
            Defaults to using the "cat_frame" value in the `phase_center_catalog`.
        use_ant_pos : bool
            If True, calculate the uvws directly from the antenna positions
            rather than from the existing uvws. Default is True.
        use_old_proj : bool
            Deprecated, will be removed in version 2.4. If True, uses the 'old' way of
            calculating baseline projections. If a dataset is known to be phased using
            the old way, use the `fix_phase` method to fix the phasing before calling
            this method.
        select_mask : ndarray of bool
            Optional mask for selecting which data to operate on along the blt-axis.
            Shape is (Nblts,). Ignored if `use_old_proj` is True.
        cat_name : str
            Name for the newly unprojected entry in the phase_center_catalog.

        Raises
        ------
        ValueError
            If the object is alread unprojected.
        """
        if not use_old_proj:
            # select_mask_use is length Nblts, True means should be unprojected
            # only select blts that are actually phased.
            if select_mask is not None:
                if len(select_mask) != self.Nblts:
                    raise IndexError("Selection mask must be of length Nblts.")
                if not isinstance(select_mask[0], (bool, np.bool_)):
                    raise ValueError("Selection mask must be a boolean array")
                select_mask_use = ~self._check_for_cat_type("unprojected") & select_mask
            else:
                select_mask_use = ~self._check_for_cat_type("unprojected")

            if np.all(~select_mask_use):
                warnings.warn("No selected baselines are projected, doing nothing")

            new_uvw = uvutils.calc_uvw(
                lst_array=self.lst_array,
                use_ant_pos=use_ant_pos,
                uvw_array=self.uvw_array,
                antenna_positions=self.antenna_positions,
                antenna_numbers=self.antenna_numbers,
                ant_1_array=self.ant_1_array,
                ant_2_array=self.ant_2_array,
                old_app_ra=self.phase_center_app_ra,
                old_app_dec=self.phase_center_app_dec,
                old_frame_pa=self.phase_center_frame_pa,
                telescope_lat=self.telescope_location_lat_lon_alt[0],
                telescope_lon=self.telescope_location_lat_lon_alt[1],
                to_enu=True,
            )

            self._apply_w_proj(0.0, self.uvw_array[select_mask_use, 2], select_mask_use)
            self.uvw_array = new_uvw

            # remove/update phase center
            match_id, match_diffs = self._look_in_catalog(
                cat_name=cat_name, cat_type="unprojected"
            )
            if match_diffs == 0:
                self.phase_center_id_array[select_mask_use] = match_id
            else:
                self.phase_center_id_array[select_mask_use] = self._add_phase_center(
                    cat_name, cat_type="unprojected"
                )
            self._clear_unused_phase_centers()
            self.phase_center_app_ra[select_mask_use] = self.lst_array[
                select_mask_use
            ].copy()
            self.phase_center_app_dec[
                select_mask_use
            ] = self.telescope_location_lat_lon_alt[0]
            self.phase_center_frame_pa[select_mask_use] = 0

            return

        warnings.warn(
            "The `use_old_proj` option is deprecated and will be removed in "
            "version 2.4. In the future, if a dataset is known to be phased using the "
            "old way, use the `fix_phase` method to fix the phasing before calling "
            "this method.",
            DeprecationWarning,
        )

        if phase_frame is not None:
            warnings.warn(
                "The `phase_frame` parameter is deprecated and will be removed in "
                "version 2.4 because it only applies if `use_old_proj` is True.",
                DeprecationWarning,
            )

        # If you are a new phase style data set, there's no valid reason to be going
        # back to the old phase method. Time to bail!
        compatible, reason = self._old_phase_attributes_compatible()
        if not compatible:
            raise ValueError(
                "Objects with " + reason + " are not compatible with the old phasing "
                "method, please set use_old_proj=False."
            )

        # TODO the following code needs to be moved to `fix_phase` in version 2.4
        # when the `use_old_proj` parameter is removed
        phase_dict = list(self.phase_center_catalog.values())[0]

        if phase_dict["cat_type"] == "unprojected":
            raise ValueError("Data are already unprojected.")

        if phase_frame is None:
            phase_frame = phase_dict["cat_frame"]

        icrs_coord = SkyCoord(
            ra=phase_dict["cat_lon"],
            dec=phase_dict["cat_lat"],
            unit="radian",
            frame="icrs",
        )
        if phase_frame == "icrs":
            frame_phase_center = icrs_coord
        else:
            # use center of observation for obstime for gcrs
            center_time = np.mean([np.max(self.time_array), np.min(self.time_array)])
            icrs_coord.obstime = Time(center_time, format="jd")
            frame_phase_center = icrs_coord.transform_to("gcrs")

        # This promotion is REQUIRED to get the right answer when we
        # add in the telescope location for ICRS
        # In some cases, the uvws are already float64, but sometimes they're not
        self.uvw_array = np.float64(self.uvw_array)

        # apply -w phasor
        if not self.metadata_only:
            w_lambda = (
                self.uvw_array[:, 2].reshape(self.Nblts, 1)
                / const.c.to("m/s").value
                * self.freq_array.reshape(1, self.Nfreqs)
            )
            if self.future_array_shapes:
                phs = np.exp(-1j * 2 * np.pi * (-1) * w_lambda[:, :, None])
            else:
                phs = np.exp(-1j * 2 * np.pi * (-1) * w_lambda[:, None, :, None])
            self.data_array *= phs

        unique_times, _ = np.unique(self.time_array, return_index=True)

        telescope_location = EarthLocation.from_geocentric(
            *self.telescope_location, unit=units.m
        )
        obs_times = Time(unique_times, format="jd")
        itrs_telescope_locations = telescope_location.get_itrs(obstime=obs_times)
        itrs_telescope_locations = SkyCoord(itrs_telescope_locations)
        # just calling transform_to(coord.GCRS) will delete the obstime information
        # need to re-add obstimes for a GCRS transformation

        if phase_frame == "gcrs":
            frame_telescope_locations = itrs_telescope_locations.transform_to(
                getattr(coord, f"{phase_frame}".upper())(obstime=obs_times)
            )
        else:
            frame_telescope_locations = itrs_telescope_locations.transform_to(
                getattr(coord, f"{phase_frame}".upper())
            )

        frame_telescope_locations.representation_type = "cartesian"

        for ind, jd in enumerate(unique_times):
            inds = np.where(self.time_array == jd)[0]

            obs_time = obs_times[ind]

            if use_ant_pos:
                with warnings.catch_warnings():
                    warnings.filterwarnings(
                        "ignore", "This function supports the old phasing method"
                    )
                    ant_uvw = uvutils.phase_uvw(
                        self.telescope_location_lat_lon_alt[1],
                        self.telescope_location_lat_lon_alt[0],
                        self.antenna_positions,
                    )
                # instead of looping through every ind, find the spot in antenna number
                # array where ant_num <= ant1 < ant_number and similarly for ant2
                # for all baselines in inds
                # then find the uvw coordinate for all at the same time

                # antenna_numbers does not necessarily need to be in order on the object
                # but needs to be in order for the searchsorted to work.
                # ant1_index and ant2_index arrays will preserve the order of blts
                ant_sort = np.argsort(self.antenna_numbers)
                ant1_index = np.searchsorted(
                    self.antenna_numbers[ant_sort], self.ant_1_array[inds]
                )
                ant2_index = np.searchsorted(
                    self.antenna_numbers[ant_sort], self.ant_2_array[inds]
                )
                self.uvw_array[inds] = (
                    ant_uvw[ant_sort][ant2_index, :] - ant_uvw[ant_sort][ant1_index, :]
                )

            else:
                frame_telescope_location = frame_telescope_locations[ind]
                itrs_lat_lon_alt = self.telescope_location_lat_lon_alt

                uvws_use = self.uvw_array[inds, :]

                with warnings.catch_warnings():
                    warnings.filterwarnings(
                        "ignore", "This function supports the old phasing method"
                    )
                    uvw_rel_positions = uvutils.unphase_uvw(
                        frame_phase_center.ra.rad, frame_phase_center.dec.rad, uvws_use
                    )

                frame_uvw_coord = SkyCoord(
                    x=uvw_rel_positions[:, 0] * units.m + frame_telescope_location.x,
                    y=uvw_rel_positions[:, 1] * units.m + frame_telescope_location.y,
                    z=uvw_rel_positions[:, 2] * units.m + frame_telescope_location.z,
                    frame=phase_frame,
                    obstime=obs_time,
                    representation_type="cartesian",
                )

                itrs_uvw_coord = frame_uvw_coord.transform_to("itrs")

                # now convert them to ENU, which is the space uvws are in
                self.uvw_array[inds, :] = uvutils.ENU_from_ECEF(
                    itrs_uvw_coord.cartesian.get_xyz().value.T, *itrs_lat_lon_alt
                )

        # remove/add phase center
        self.phase_center_id_array[:] = self._add_phase_center(
            cat_name, cat_type="unprojected"
        )
        self._clear_unused_phase_centers()

        self.phase_center_app_ra = self.lst_array.copy()
        self.phase_center_app_dec[:] = (
            np.zeros(self.Nblts) + self.telescope_location_lat_lon_alt[0]
        )
        self.phase_center_frame_pa = np.zeros(self.Nblts)

    def _phase_dict_helper(
        self,
        lon,
        lat,
        epoch,
        phase_frame,
        ephem_times,
        cat_type,
        pm_ra,
        pm_dec,
        dist,
        vrad,
        cat_name,
        lookup_name,
        time_array,
    ):
        """
        Supplies a dictionary with parameters for the phase method to use.

        This method should not be called directly by users; it is instead a function
        called by the `phase` method, which packages up phase center information
        into a single dictionary to allow for consistent behavior between different
        instantiations of `UVData` objects.
        """
        cat_id = None
        info_source = "user"

        name_dict = {
            pc_dict["cat_name"]: pc_id
            for pc_id, pc_dict in self.phase_center_catalog.items()
        }

        if lookup_name:
            if len(self._look_for_name(cat_name)) > 1:
                raise ValueError(
                    "Name of object has multiple matches in phase center catalog. "
                    "Set lookup_name=False in order to continue."
                )

        # TODO I don't understand this comment:
        # We only want to use the JPL-Horizons service if using a non-multi-phase-ctr
        # instance of a UVData object.
        if lookup_name and (cat_name not in name_dict):
            if (cat_type is None) or (cat_type == "ephem"):
                [
                    cat_times,
                    cat_lon,
                    cat_lat,
                    cat_dist,
                    cat_vrad,
                ] = uvutils.lookup_jplhorizons(
                    cat_name,
                    time_array,
                    telescope_loc=self.telescope_location_lat_lon_alt,
                )
                cat_type = "ephem"
                cat_pm_ra = cat_pm_dec = None
                cat_epoch = 2000.0
                cat_frame = "icrs"
                info_source = "jplh"
            else:
                raise ValueError(
                    f"Unable to find {cat_name} in among the existing sources "
                    "recorded in the catalog. Please supply source "
                    "information (e.g., RA and Dec coordinates) and "
                    "set lookup_name=False."
                )
        elif cat_name in name_dict:
            # If the name of the source matches, then verify that all of its
            # properties are the same as what is stored in phase_center_catalog.
            if lookup_name:
                cat_id = name_dict[cat_name]
                cat_diffs = 0
            else:
                cat_id, cat_diffs = self._look_in_catalog(
                    cat_name,
                    cat_type=cat_type,
                    cat_lon=lon,
                    cat_lat=lat,
                    cat_frame=phase_frame,
                    cat_epoch=epoch,
                    cat_times=ephem_times,
                    cat_pm_ra=pm_ra,
                    cat_pm_dec=pm_dec,
                    cat_dist=dist,
                    cat_vrad=vrad,
                )
            # If cat_diffs > 0, it means that the catalog entries dont match
            if cat_diffs != 0:
                warnings.warn(
                    f"The entry name {cat_name} is not unique inside the phase "
                    "center catalog, adding anyways."
                )
                cat_type = "sidereal" if cat_type is None else cat_type
                cat_lon = lon
                cat_lat = lat
                cat_frame = phase_frame
                cat_epoch = epoch
                cat_times = ephem_times
                cat_pm_ra = pm_ra
                cat_pm_dec = pm_dec
                cat_dist = dist
                cat_vrad = vrad
            else:
                temp_dict = self.phase_center_catalog[cat_id]
                cat_type = temp_dict["cat_type"]
                info_source = temp_dict["info_source"]
                # Get here will return None if no key found, which we want
                cat_lon = temp_dict.get("cat_lon")
                cat_lat = temp_dict.get("cat_lat")
                cat_frame = temp_dict.get("cat_frame")
                cat_epoch = temp_dict.get("cat_epoch")
                cat_times = temp_dict.get("cat_times")
                cat_pm_ra = temp_dict.get("cat_pm_ra")
                cat_pm_dec = temp_dict.get("cat_pm_dec")
                cat_dist = temp_dict.get("cat_dist")
                cat_vrad = temp_dict.get("cat_vrad")
        else:
            # The name of the source is unique!
            cat_type = "sidereal" if cat_type is None else cat_type
            cat_lon = lon
            cat_lat = lat
            cat_frame = phase_frame
            cat_epoch = epoch
            cat_times = ephem_times
            cat_pm_ra = pm_ra
            cat_pm_dec = pm_dec
            cat_dist = dist
            cat_vrad = vrad

        if cat_epoch is None:
            cat_epoch = 1950.0 if (cat_frame in ["fk4", "fk4noeterms"]) else 2000.0
        if isinstance(cat_epoch, str) or isinstance(cat_epoch, Time):
            cat_epoch = Time(cat_epoch).to_value(
                "byear" if cat_frame in ["fk4", "fk4noeterms"] else "jyear"
            )

        # One last check - if we have an ephem phase center, lets make sure that the
        # time range of the ephemeris encapsulates the entire range of time_array
        check_ephem = False
        if cat_type == "ephem":
            # Take advantage of this to make sure that lat, lon, and times are all
            # ndarray types
            cat_lon = np.array(cat_lon, dtype=float)
            cat_lat = np.array(cat_lat, dtype=float)
            cat_times = np.array(cat_times, dtype=float)
            cat_lon.shape += (1,) if (cat_lon.ndim == 0) else ()
            cat_lat.shape += (1,) if (cat_lat.ndim == 0) else ()
            cat_times.shape += (1,) if (cat_times.ndim == 0) else ()
            check_ephem = np.min(time_array) < np.min(cat_times)
            check_ephem = check_ephem or (np.max(time_array) > np.max(cat_times))
            # If the ephem was supplied by JPL-Horizons, then we can easily expand
            # it to cover the requested range.
            if check_ephem and (info_source == "jplh"):
                # Concat the two time ranges to make sure that we cover both the
                # requested time range _and_ the original time range.
                [
                    cat_times,
                    cat_lon,
                    cat_lat,
                    cat_dist,
                    cat_vrad,
                ] = uvutils.lookup_jplhorizons(
                    cat_name,
                    np.concatenate((np.reshape(time_array, -1), cat_times)),
                    telescope_loc=self.telescope_location_lat_lon_alt,
                )
            elif check_ephem:
                # The ephem was user-supplied during the call to the phase method,
                # raise an error to ask for more ephem data.
                raise ValueError(
                    "Ephemeris data does not cover the entirety of the time range "
                    "attempted to be phased. Please supply additional ephem data "
                    "(and if used, set lookup_name=False)."
                )
        # Time to repackage everything into a dict
        phase_dict = {
            "cat_name": cat_name,
            "cat_type": cat_type,
            "cat_lon": cat_lon,
            "cat_lat": cat_lat,
            "cat_frame": cat_frame,
            "cat_epoch": cat_epoch,
            "cat_times": cat_times,
            "cat_pm_ra": cat_pm_ra,
            "cat_pm_dec": cat_pm_dec,
            "cat_dist": cat_dist,
            "cat_vrad": cat_vrad,
            "info_source": info_source,
            "cat_id": cat_id,
        }

        # Finally, make sure everything is a float or an ndarray of floats
        for key in phase_dict.keys():
            if isinstance(phase_dict[key], np.ndarray):
                phase_dict[key] = phase_dict[key].astype(float)
            elif (key == "cat_id") and (phase_dict[key] is not None):
                # If this is the cat_id, make it an int
                phase_dict[key] = int(phase_dict[key])
            elif not ((phase_dict[key] is None) or isinstance(phase_dict[key], str)):
                phase_dict[key] = float(phase_dict[key])

        return phase_dict

    def phase(
        self,
        lon=None,
        lat=None,
        epoch="J2000",
        phase_frame="icrs",
        ra=None,
        dec=None,
        cat_type=None,
        ephem_times=None,
        pm_ra=None,
        pm_dec=None,
        dist=None,
        vrad=None,
        cat_name=None,
        lookup_name=False,
        use_ant_pos=True,
        allow_rephase=None,
        orig_phase_frame=None,
        select_mask=None,
        cleanup_old_sources=True,
        use_old_proj=False,
        fix_old_proj=None,
    ):
        """
        Phase data to a new direction, supports sidereal, ephemeris and driftscan types.

        Can be used to phase all or a subset of the baseline-times. Types of phase
        centers (`cat_type`) that are supported include:

            - sidereal (fixed RA/Dec)
            - ephem (RA/Dec that moves with time)
            - driftscan (fixed az/el position)

        See the phasing memo under docs/references for more documentation.

        Tested against MWA_Tools/CONV2UVFITS/convutils.

        Parameters
        ----------
        lon : float
            The longitude coordinate (e.g. RA or Azimuth) to phase to in radians.
        lat : float
            The latitude coordinate (e.g. Dec or Altitude) to phase to in radians.
        epoch : astropy.time.Time object or str
            The epoch to use for phasing. Either an astropy Time object or the
            string "J2000" (which is the default).
            Note that the epoch is only used to evaluate the ra & dec values,
            if the epoch is not J2000, the ra & dec values are interpreted
            as FK5 ra/dec values and transformed to J2000, the data are then
            phased to the J2000 ra/dec values.
        phase_frame : str
            The astropy frame to phase to, any astropy-supported frame is allowed unless
            use_old_proj is True, in which case it can only be 'icrs' or 'gcrs'.
            'gcrs' accounts for precession & nutation,
            'icrs' accounts for precession, nutation & abberation.
        ra : float
            An alias for `lon`.
        dec : float
            An alias for `lat`.
        cat_type : str
            Type of phase center to be added. Must be one of:
            "sidereal" (fixed RA/Dec), "ephem" (RA/Dec that moves with time),
            "driftscan" (fixed az/el position). Default is "sidereal".
        ephem_times : ndarray of float
            Only used when `cat_type="ephem"`. Describes the time for which the values
            of `cat_lon` and `cat_lat` are caclulated, in units of JD. Shape is (Npts,).
        pm_ra : float
            Proper motion in RA, in units of mas/year. Only used for sidereal phase
            centers.
        pm_dec : float
            Proper motion in Dec, in units of mas/year. Only used for sidereal phase
            centers.
        dist : float or ndarray of float
            Distance of the source, in units of pc. Only used for sidereal and ephem
            phase centers. Expected to be a float for sidereal phase
            centers, and an ndarray of floats of shape (Npts,) for ephem phase centers.
        vrad : float or ndarray of float
            Radial velocity of the source, in units of km/s. Only used for sidereal and
            ephem phase centers. Expected to be a float for sidereal phase
            centers, and an ndarray of floats of shape (Npts,) for ephem phase centers.
        cat_name : str
            Name of the phase center being phased to, required.
        lookup_name : bool
            Allows the user to lookup phase center infomation in `phase_center_catalog`
            (for the entry matching `cat_name`). Setting this to `True` will ignore the
            values supplied to the `ra`, `dec`, `epoch`, `phase_frame`, `pm_ra`,
            `pm_dec`, `dist`, `vrad`.
        use_ant_pos : bool
            If True, calculate the uvws directly from the antenna positions
            rather than from the existing uvws.
        allow_rephase : bool
            Deprecated, will be removed in version 2.4. Only used if  `use_old_proj` is
            True. If this parameter is True, allow unphasing and rephasing if this
            object is already phased.
        orig_phase_frame : str
            Deprecated, will be removed in version 2.4. The original phase frame of
            this object (to use in unphasing). Only used if `use_old_proj` is True,
            object is already phased, `allow_rephase` is True and the
            phase_center_ra/dec of the object does not match `ra` and `dec`.
            Defaults to using the "cat_frame" value in the `phase_center_catalog`.
        select_mask : ndarray of bool
            Optional mask for selecting which data to operate on along the blt-axis.
            Shape is (Nblts,). Ignored if `use_old_proj` is True.
        use_old_proj : bool
            Deprecated, will be removed in version 2.4. If True, use the "old" method
            for calculating baseline uvw-coordinates, which involved using astropy to
            move antenna positions (in ITRF) into the requested reference frame
            (either GCRS or ICRS). Default is False.
        fix_old_proj : bool
            Deprecated and has no effect. Detection of old phasing is really only
            possible for uvh5 files, and those are now fixed on read by default. If the
            object is known to have been phased with the old phasing, use the
            `fix_phase` method to correct the phasing.

        Raises
        ------
        ValueError
            If the `use_old_proj` is True, the object is already phased, and
            allow_rephase is False.
        """
        if fix_old_proj is not None:
            warnings.warn(
                "The `fix_old_proj` parameter is deprecated and will be removed in "
                "version 2.4. If the object is known to have been phased with the old "
                "phasing, use the `fix_phase` method to correct the phasing.",
                DeprecationWarning,
            )

        if cat_name is None:
            warnings.warn(
                "No cat_name provided, this will become an error in pyuvdata v2.4. "
                "Defaulting to naming based on lat/lon.",
                DeprecationWarning,
            )

        # If you are a new phase style data set, there's no valid reason to be going
        # back to the old phase method. Time to bail!
        compatible, reason = self._old_phase_attributes_compatible()
        if use_old_proj and not compatible:
            raise ValueError(
                "Objects with " + reason + " are not compatible with the old phasing "
                "method, please set use_old_proj=False."
            )

        if cat_type != "unprojected":
            if lon is None:
                if ra is None:
                    raise ValueError(
                        "lon parameter must be set if cat_type is not 'unprojected'"
                    )
                else:
                    lon = ra
            if lat is None:
                if dec is None:
                    raise ValueError(
                        "lat parameter must be set if cat_type is not 'unprojected'"
                    )
                else:
                    lat = dec
            if cat_name is None:
                cat_name = (
                    "lon_"
                    + str(np.rad2deg(lon))
                    + "deg_lat_"
                    + str(np.rad2deg(lat))
                    + "_deg"
                )

        # Before moving forward with the heavy calculations, we need to do some
        # basic housekeeping to make sure that we've got the coordinate data that
        # we need in order to proceed.
        phase_dict = self._phase_dict_helper(
            lon,
            lat,
            epoch,
            phase_frame,
            ephem_times,
            cat_type,
            pm_ra,
            pm_dec,
            dist,
            vrad,
            cat_name,
            lookup_name,
            self.time_array,
        )

        if phase_dict["cat_type"] not in ["ephem", "unprojected"]:
            if np.array(lon).size > 1:
                raise ValueError(
                    "lon parameter must be a single value for cat_type "
                    f"{phase_dict['cat_type']}"
                )

            if np.array(lat).size > 1:
                raise ValueError(
                    "lat parameter must be a single value for cat_type "
                    f"{phase_dict['cat_type']}"
                )

        # Right up front, we're gonna split off the piece of the code that
        # does the phasing using the "new" method, since its a lot more flexible
        # and because I think at some point, everything outside of this loop
        # can be deprecated
        if not use_old_proj:
            # Grab all the meta-data we need for the rotations
            time_array = self.time_array
            lst_array = self.lst_array
            uvw_array = self.uvw_array
            ant_1_array = self.ant_1_array
            ant_2_array = self.ant_2_array
            old_w_vals = self.uvw_array[:, 2].copy()
            old_w_vals[self._check_for_cat_type("unprojected")] = 0.0
            old_app_ra = self.phase_center_app_ra
            old_app_dec = self.phase_center_app_dec
            old_frame_pa = self.phase_center_frame_pa
            # Check and see if we have any unprojected objects, in which case
            # their w-values should be zeroed out.

            if select_mask is not None:
                if len(select_mask) != self.Nblts:
                    raise IndexError("Selection mask must be of length Nblts.")
                if not isinstance(select_mask[0], (bool, np.bool_)):
                    raise ValueError("Selection mask must be a boolean array")
                time_array = time_array[select_mask]
                lst_array = lst_array[select_mask]
                uvw_array = uvw_array[select_mask, :]
                ant_1_array = ant_1_array[select_mask]
                ant_2_array = ant_2_array[select_mask]
                if isinstance(old_w_vals, np.ndarray):
                    old_w_vals = old_w_vals[select_mask]

            # We got the meta-data, now handle calculating the apparent coordinates.
            # First, check if we need to look up the phase center in question
            new_app_ra, new_app_dec = uvutils.calc_app_coords(
                phase_dict["cat_lon"],
                phase_dict["cat_lat"],
                coord_frame=phase_dict["cat_frame"],
                coord_epoch=phase_dict["cat_epoch"],
                coord_times=phase_dict["cat_times"],
                coord_type=phase_dict["cat_type"],
                time_array=time_array,
                lst_array=lst_array,
                pm_ra=phase_dict["cat_pm_ra"],
                pm_dec=phase_dict["cat_pm_dec"],
                vrad=phase_dict["cat_vrad"],
                dist=phase_dict["cat_dist"],
                telescope_loc=self.telescope_location_lat_lon_alt,
            )

            # Now calculate position angles.
            if not phase_frame == "altaz":
                new_frame_pa = uvutils.calc_frame_pos_angle(
                    time_array,
                    new_app_ra,
                    new_app_dec,
                    self.telescope_location_lat_lon_alt,
                    phase_frame,
                    ref_epoch=epoch,
                )
            else:
                new_frame_pa = np.zeros(time_array.shape, dtype=float)

            # Now its time to do some rotations and calculate the new coordinates
            new_uvw = uvutils.calc_uvw(
                app_ra=new_app_ra,
                app_dec=new_app_dec,
                frame_pa=new_frame_pa,
                lst_array=lst_array,
                use_ant_pos=use_ant_pos,
                uvw_array=uvw_array,
                antenna_positions=self.antenna_positions,
                antenna_numbers=self.antenna_numbers,
                ant_1_array=ant_1_array,
                ant_2_array=ant_2_array,
                old_app_ra=old_app_ra,
                old_app_dec=old_app_dec,
                old_frame_pa=old_frame_pa,
                telescope_lat=self.telescope_location_lat_lon_alt[0],
                telescope_lon=self.telescope_location_lat_lon_alt[1],
            )

            # With all operations complete, we now start manipulating the UVData object
            cat_id = self._add_phase_center(
                phase_dict["cat_name"],
                phase_dict["cat_type"],
                cat_lon=phase_dict["cat_lon"],
                cat_lat=phase_dict["cat_lat"],
                cat_frame=phase_dict["cat_frame"],
                cat_epoch=phase_dict["cat_epoch"],
                cat_times=phase_dict["cat_times"],
                cat_pm_ra=phase_dict["cat_pm_ra"],
                cat_pm_dec=phase_dict["cat_pm_dec"],
                cat_dist=phase_dict["cat_dist"],
                cat_vrad=phase_dict["cat_vrad"],
                info_source=phase_dict["info_source"],
                cat_id=phase_dict["cat_id"],
                force_update=True,
            )

            # Now its time to update the raw data. This will return empty if
            # metadata_only is set to True.
            new_w_vals = 0.0 if (cat_type == "unprojected") else new_uvw[:, 2]
            self._apply_w_proj(new_w_vals, old_w_vals, select_mask=select_mask)

            # Finally, we now take it upon ourselves to update some metadata. What we
            # do here will depend a little bit on whether or not we have a selection
            # mask active, since most everything is affected by that.
            if select_mask is not None:
                self.uvw_array[select_mask] = new_uvw
                self.phase_center_app_ra[select_mask] = new_app_ra
                self.phase_center_app_dec[select_mask] = new_app_dec
                self.phase_center_frame_pa[select_mask] = new_frame_pa
                self.phase_center_id_array[select_mask] = cat_id
            else:
                self.uvw_array = new_uvw
                self.phase_center_app_ra = new_app_ra
                self.phase_center_app_dec = new_app_dec
                self.phase_center_frame_pa = new_frame_pa
                self.phase_center_id_array[:] = cat_id

            # If not multi phase center, make sure to update the ra/dec values, since
            # otherwise we'll have no record of source properties.
            if cleanup_old_sources:
                self._clear_unused_phase_centers()
            # All done w/ the new phase method
            return

        # use the old method
        warnings.warn(
            "The `use_old_proj` option is deprecated and will be removed in "
            "version 2.4. Note that the old and new phase methods are NOT compatible "
            "with one another, so if you have phased using the old method, you should "
            "call the `fix_phase` method to undo the old corrections before using the "
            "new version of the phase method.",
            DeprecationWarning,
        )

        if allow_rephase is not None:
            warnings.warn(
                "The `allow_rephase` option is deprecated, and will be removed in "
                "version 2.4. It is only used if `use_old_proj` is True.",
                DeprecationWarning,
            )
        else:
            # default it to True if it is not set
            allow_rephase = True

        if orig_phase_frame is not None:
            warnings.warn(
                "The `allow_rephase` option is deprecated, and will be removed in "
                "version 2.4. It is only used if `use_old_proj` is True.",
                DeprecationWarning,
            )

        if phase_dict["cat_type"] != "sidereal":
            raise ValueError(
                "Old phasing only supports sidereal catalog types, "
                "please change the cat_type or use_old_proj=False."
            )

        if phase_dict["cat_frame"] not in ["icrs", "gcrs"]:
            raise ValueError(
                "Old phasing only supports 'icrs' and 'gcrs' phase_frames. Either use "
                "one of those frames or set use_old_proj=False."
            )

        ra = lon
        dec = lat

        # To get here there can only be one phase center
        old_phase_dict = list(self.phase_center_catalog.values())[0]

        if old_phase_dict["cat_type"] != "unprojected":
            if not allow_rephase:
                raise ValueError(
                    "Visibilities are already phased; set allow_rephase to True to "
                    "unphase and rephase."
                )

            # To get to this point, allow_rephase has to be true
            if old_phase_dict["cat_lon"] != lon or old_phase_dict["cat_lat"] != lat:
                self.unproject_phase(
                    phase_frame=orig_phase_frame,
                    use_ant_pos=use_ant_pos,
                    use_old_proj=True,
                )

        if epoch == "J2000" or epoch == 2000:
            icrs_coord = SkyCoord(ra=ra, dec=dec, unit="radian", frame="icrs")
        else:
            assert isinstance(epoch, Time)
            phase_center_coord = SkyCoord(
                ra=ra, dec=dec, unit="radian", equinox=epoch, frame=FK5
            )
            # convert to icrs (i.e. J2000) to write to object
            icrs_coord = phase_center_coord.transform_to("icrs")

        # This feels yucky, but it is what was done in old phasing because we defined
        # ra/dec as being in ICRS
        phase_dict["cat_lon"] = icrs_coord.ra.radian
        phase_dict["cat_lat"] = icrs_coord.dec.radian
        phase_dict["cat_epoch"] = 2000.0

        cat_id = self._add_phase_center(
            cat_name,
            cat_type="sidereal",
            cat_lon=phase_dict["cat_lon"],
            cat_lat=phase_dict["cat_lat"],
            cat_frame=phase_dict["cat_frame"],
            cat_epoch=phase_dict["cat_epoch"],
        )
        self.phase_center_id_array = np.zeros(self.Nblts, dtype=int) + cat_id
        self._clear_unused_phase_centers()

        self.phase_center_app_ra = None
        self.phase_center_app_dec = None
        self.phase_center_frame_pa = None

        if phase_dict["cat_frame"] == "icrs":
            frame_phase_center = icrs_coord
        else:
            # use center of observation for obstime for gcrs
            center_time = np.mean([np.max(self.time_array), np.min(self.time_array)])
            icrs_coord.obstime = Time(center_time, format="jd")
            frame_phase_center = icrs_coord.transform_to("gcrs")

        # This promotion is REQUIRED to get the right answer when we
        # add in the telescope location for ICRS
        self.uvw_array = np.float64(self.uvw_array)

        unique_times, unique_inds = np.unique(self.time_array, return_index=True)

        telescope_location = EarthLocation.from_geocentric(
            *self.telescope_location, unit=units.m
        )
        obs_times = Time(unique_times, format="jd")

        itrs_telescope_locations = telescope_location.get_itrs(obstime=obs_times)
        itrs_telescope_locations = SkyCoord(itrs_telescope_locations)
        # just calling transform_to(coord.GCRS) will delete the obstime information
        # need to re-add obstimes for a GCRS transformation
        if phase_dict["cat_frame"] == "gcrs":
            frame_telescope_locations = itrs_telescope_locations.transform_to(
                getattr(coord, f"{phase_dict['cat_frame']}".upper())(obstime=obs_times)
            )
        else:
            frame_telescope_locations = itrs_telescope_locations.transform_to(
                getattr(coord, f"{phase_dict['cat_frame']}".upper())
            )
        # set the representation_type to cartensian to get xyz later
        frame_telescope_locations.representation_type = "cartesian"

        for ind, jd in enumerate(unique_times):
            inds = np.where(self.time_array == jd)[0]

            obs_time = obs_times[ind]

            itrs_lat_lon_alt = self.telescope_location_lat_lon_alt

            frame_telescope_location = frame_telescope_locations[ind]

            if use_ant_pos:
                # This promotion is REQUIRED to get the right answer when we
                # add in the telescope location for ICRS
                ecef_ant_pos = (
                    np.float64(self.antenna_positions) + self.telescope_location
                )

                itrs_ant_coord = SkyCoord(
                    x=ecef_ant_pos[:, 0] * units.m,
                    y=ecef_ant_pos[:, 1] * units.m,
                    z=ecef_ant_pos[:, 2] * units.m,
                    frame="itrs",
                    obstime=obs_time,
                )

                frame_ant_coord = itrs_ant_coord.transform_to(phase_dict["cat_frame"])

                frame_ant_rel = (
                    (frame_ant_coord.cartesian - frame_telescope_location.cartesian)
                    .get_xyz()
                    .T.value
                )

                with warnings.catch_warnings():
                    warnings.filterwarnings(
                        "ignore", "This function supports the old phasing method"
                    )
                    frame_ant_uvw = uvutils.phase_uvw(
                        frame_phase_center.ra.rad,
                        frame_phase_center.dec.rad,
                        frame_ant_rel,
                    )
                # instead of looping through every ind, find the spot in antenna number
                # array where ant_num <= ant1 < ant_number and similarly for ant2
                # for all baselines in inds
                # then find the uvw coordinate for all at the same time

                # antenna_numbers does not necessarily need to be in order on the object
                # but needs to be in order for the searchsorted to work.
                # ant1_index and ant2_index arrays will preserve the order of blts
                ant_sort = np.argsort(self.antenna_numbers)
                ant1_index = np.searchsorted(
                    self.antenna_numbers[ant_sort], self.ant_1_array[inds]
                )
                ant2_index = np.searchsorted(
                    self.antenna_numbers[ant_sort], self.ant_2_array[inds]
                )
                self.uvw_array[inds] = (
                    frame_ant_uvw[ant_sort][ant2_index, :]
                    - frame_ant_uvw[ant_sort][ant1_index, :]
                )
            else:
                # Also, uvws should be thought of like ENU, not ECEF (or rotated ECEF)
                # convert them to ECEF to transform between frames
                uvws_use = self.uvw_array[inds, :]

                uvw_ecef = uvutils.ECEF_from_ENU(uvws_use, *itrs_lat_lon_alt)

                itrs_uvw_coord = SkyCoord(
                    x=uvw_ecef[:, 0] * units.m,
                    y=uvw_ecef[:, 1] * units.m,
                    z=uvw_ecef[:, 2] * units.m,
                    frame="itrs",
                    obstime=obs_time,
                )
                frame_uvw_coord = itrs_uvw_coord.transform_to(phase_dict["cat_frame"])

                # this takes out the telescope location in the new frame,
                # so these are vectors again
                frame_rel_uvw = (
                    frame_uvw_coord.cartesian.get_xyz().value.T
                    - frame_telescope_location.cartesian.get_xyz().value
                )

                with warnings.catch_warnings():
                    warnings.filterwarnings(
                        "ignore", "This function supports the old phasing method"
                    )
                    self.uvw_array[inds, :] = uvutils.phase_uvw(
                        frame_phase_center.ra.rad,
                        frame_phase_center.dec.rad,
                        frame_rel_uvw,
                    )

        # calculate data and apply phasor
        if not self.metadata_only:
            w_lambda = (
                self.uvw_array[:, 2].reshape(self.Nblts, 1)
                / const.c.to("m/s").value
                * self.freq_array.reshape(1, self.Nfreqs)
            )
            if self.future_array_shapes:
                phs = np.exp(-1j * 2 * np.pi * w_lambda[:, :, None])
            else:
                phs = np.exp(-1j * 2 * np.pi * w_lambda[:, None, :, None])
            self.data_array *= phs

        # make sure apparent coords are properly set.
        self._set_app_coords_helper()

    def phase_to_time(
        self,
        time,
        phase_frame="icrs",
        use_ant_pos=True,
        use_old_proj=False,
        allow_rephase=True,
        orig_phase_frame=None,
        select_mask=None,
    ):
        """
        Phase to the ra/dec of zenith at a particular time.

        See the phasing memo under docs/references for more documentation.

        Parameters
        ----------
        time : astropy.time.Time object or float
            The time to phase to, an astropy Time object or a float Julian Date
        phase_frame : str
            The astropy frame to phase to, any astropy-supported frame is allowed unless
            use_old_proj is True, in which case it can only be 'icrs' or 'gcrs'.
            'gcrs' accounts for precession & nutation,
            'icrs' accounts for precession, nutation & abberation.
        use_ant_pos : bool
            If True, calculate the uvws directly from the antenna positions
            rather than from the existing uvws.
        use_old_proj : bool
            Deprecated, will be removed in version 2.4. If True, use the "old" method
            for calculating baseline uvw-coordinates, which involved using astropy to
            move antenna positions (in ITRF) into the requested reference frame
            (either GCRS or ICRS). Default is False.
        allow_rephase : bool
            Deprecated, will be removed in version 2.4. Only used if  `use_old_proj` is
            True. If this parameter is True, allow unphasing and rephasing if this
            object is already phased.
        orig_phase_frame : str
            Deprecated, will be removed in version 2.4. The original phase frame of
            this object (to use in unphasing). Only used if `use_old_proj` is True,
            object is already phased, `allow_rephase` is True and the
            phase_center_ra/dec of the object does not match `ra` and `dec`.
            Defaults to using the "cat_frame" value in the `phase_center_catalog`.
        select_mask : array_like
            Selection mask for which data should be rephased. Any array-like able to be
            used as an index is suitable -- the most typical is an array of bool with
            length `Nblts`, or an array of ints within the range (-Nblts, Nblts).

        Raises
        ------
        ValueError
            If the `use_old_proj` is True, the object is already phased, and
            allow_rephase is False.
        TypeError
            If time is not an astropy.time.Time object or Julian Date as a float

        """
        if isinstance(time, (float, np.floating)):
            time = Time(time, format="jd")

        if not isinstance(time, Time):
            raise TypeError("time must be an astropy.time.Time object or a float")

        # Generate ra/dec of zenith at time in the phase_frame coordinate
        # system to use for phasing
        telescope_location = EarthLocation.from_geocentric(
            *self.telescope_location, unit="m"
        )

        zenith_coord = SkyCoord(
            alt=Angle(90 * units.deg),
            az=Angle(0 * units.deg),
            obstime=time,
            frame="altaz",
            location=telescope_location,
        )

        obs_zenith_coord = zenith_coord.transform_to(phase_frame)
        zenith_ra = obs_zenith_coord.ra.rad
        zenith_dec = obs_zenith_coord.dec.rad

        self.phase(
            zenith_ra,
            zenith_dec,
            epoch="J2000",
            phase_frame=phase_frame,
            use_ant_pos=use_ant_pos,
            use_old_proj=use_old_proj,
            allow_rephase=allow_rephase,
            orig_phase_frame=orig_phase_frame,
            select_mask=select_mask,
            cat_name=("zenith_at_jd%f" % time.jd),
        )

    def set_uvws_from_antenna_positions(
        self,
        update_vis=True,
        allow_phasing=False,
        require_phasing=None,
        orig_phase_frame=None,
        output_phase_frame="icrs",
        use_old_proj=False,
    ):
        """
        Calculate UVWs based on antenna_positions.

        Parameters
        ----------
        update_vis : bool
            Option to update visibilities when recalculating uvws to correct for
            phasing errors from using the `old` method. This should only be set to False
            in limited circumstances (e.g., when certain metadata like exact times are
            not trusted), as misuse can significantly corrupt data. Ignored if
            `use_old_proj` is True.
        allow_phasing : bool
            Option for phased data, only applies if `use_old_proj` is True. If data is
            phased and allow_phasing=True, UVWs will be calculated and the visibilities
            will be rephased. Default is False. This option is deprecated and will be
            removed in version 2.4 with `use_old_proj`.
        require_phasing : bool
            Deprecated. Use `update_vis` instead. This parameter will be removed in
            version 2.4
        orig_phase_frame : str
            The astropy frame to phase from. Either 'icrs' or 'gcrs'.
            Defaults to using the phase_center_catalog["cat_frame"]. Only used if
            allow_phasing is True and use_old_proj is True. This option is deprecated
            and will be removed in version 2.4 with `use_old_proj`.
        output_phase_frame : str
            The astropy frame to phase to. Either 'icrs' or 'gcrs'. Only used if
            allow_phasing is True, and use_old_proj is True. This option is deprecated
            and will be removed in version 2.4 with `use_old_proj`.
        use_old_proj : bool
            If set to True, uses the 'old' method of calculating baseline vectors.
            Default is False, which will instead use the 'new' method. This option is
            deprecated and will be removed in version 2.4 after which only the 'new'
            method will be supported (because it is more accurate).

        Raises
        ------
        ValueError
            If use_old_proj is True, all data is phased and allow_phasing is False.

        """
        if require_phasing is not None:
            warnings.warn(
                "The `require_phasing` option is deprecated, and will be removed in "
                "pyuvdata v2.4. Use `update_vis` instead",
                DeprecationWarning,
            )
            update_vis = require_phasing

        if not use_old_proj:
            telescope_location = self.telescope_location_lat_lon_alt
            unprojected_blts = self._check_for_cat_type("unprojected")

            new_uvw = uvutils.calc_uvw(
                app_ra=self.phase_center_app_ra,
                app_dec=self.phase_center_app_dec,
                frame_pa=self.phase_center_frame_pa,
                lst_array=self.lst_array,
                use_ant_pos=True,
                antenna_positions=self.antenna_positions,
                antenna_numbers=self.antenna_numbers,
                ant_1_array=self.ant_1_array,
                ant_2_array=self.ant_2_array,
                telescope_lat=telescope_location[0],
                telescope_lon=telescope_location[1],
            )
            if np.any(~unprojected_blts):
                # At least some are phased
                if update_vis:
                    old_w_vals = self.uvw_array[:, 2].copy()
                    old_w_vals[unprojected_blts] = 0.0
                    self._apply_w_proj(new_uvw[:, 2], old_w_vals)
                else:
                    warnings.warn(
                        "Recalculating uvw_array without adjusting visibility "
                        "phases -- this can introduce significant errors if used "
                        "incorrectly."
                    )

            # If the data are phased, we've already adjusted the phases. Now we just
            # need to update the uvw's and we are home free.
            self.uvw_array = new_uvw
            return

        # datasets phased with new phasing should not use the "old" method
        compatible, reason = self._old_phase_attributes_compatible()
        if use_old_proj and not compatible:
            raise ValueError(
                "Objects with " + reason + " are not compatible with the old phasing "
                "method, please set use_old_proj=False."
            )

        # if we got here, there's only one phase center
        phase_dict = list(self.phase_center_catalog.values())[0]

        # use the old method
        warnings.warn(
            "The `use_old_proj` option is deprecated, and will be removed in "
            "pyuvdata 2.4. Note that the old and new phase methods are NOT compatible "
            "with one another, so if you have phased using the old method, you should "
            "call the `fix_phase` method or use the unproject_phase method with "
            "use_old_proj=True to undo the old corrections before using the new "
            "version of the phase method.",
            DeprecationWarning,
        )

        if phase_dict["cat_type"] != "unprojected":
            if allow_phasing:
                if not self.metadata_only:
                    warnings.warn(
                        "Data will be unphased and rephased "
                        "to calculate UVWs, which might introduce small "
                        "inaccuracies to the data."
                    )
                if orig_phase_frame not in [None, "icrs", "gcrs"]:
                    raise ValueError(
                        "Invalid parameter orig_phase_frame. "
                        'Options are "icrs", "gcrs", or None.'
                    )
                if output_phase_frame not in ["icrs", "gcrs"]:
                    raise ValueError(
                        "Invalid parameter output_phase_frame. "
                        'Options are "icrs" or "gcrs".'
                    )
                with warnings.catch_warnings():
                    warnings.filterwarnings(
                        "ignore", "The `use_old_proj` option is deprecated"
                    )
                    self.unproject_phase(
                        phase_frame=orig_phase_frame, use_old_proj=True
                    )
            else:
                raise ValueError(
                    "UVW calculation with `use_old_proj` requires unprojected data. "
                    "Use `unproject_phase` or set allow_phasing=True."
                )
        antenna_locs_ENU, _ = self.get_ENU_antpos(center=False)
        # this code used to loop through every bl in the unique,
        # find the index into self.antenna_array of ant1 and ant2
        # and fill out the self.uvw_array for all matching bls.

        # instead, find the indices and reverse inds from the unique,
        # create the unique ant1 and ant2 arrays
        # use searchsorted to find the index of the antenna numbers into ant1 and ant2
        # create the unique uvw array then broadcast to self.uvw_array
        bls, unique_inds, reverse_inds = np.unique(
            self.baseline_array, return_index=True, return_inverse=True
        )

        # antenna_numbers does not necessarily need to be in order on the object
        # but needs to be in order for the searchsorted to work.
        # ant1_index and ant2_index arrays will preserve the order of blts
        ant_sort = np.argsort(self.antenna_numbers)
        ant1_index = np.searchsorted(
            self.antenna_numbers[ant_sort], self.ant_1_array[unique_inds]
        )
        ant2_index = np.searchsorted(
            self.antenna_numbers[ant_sort], self.ant_2_array[unique_inds]
        )
        _uvw_array = np.zeros((bls.size, 3))
        _uvw_array = (
            antenna_locs_ENU[ant_sort][ant2_index, :]
            - antenna_locs_ENU[ant_sort][ant1_index, :]
        )
        self.uvw_array = _uvw_array[reverse_inds]
        if phase_dict["cat_type"] != "unprojected":
            with warnings.catch_warnings():
                warnings.filterwarnings(
                    "ignore", "The `use_old_proj` option is deprecated"
                )
                self.phase(
                    lon=phase_dict["cat_lon"],
                    lat=phase_dict["cat_lat"],
                    epoch=phase_dict["cat_epoch"],
                    phase_frame=phase_dict["cat_frame"],
                    cat_name=phase_dict["cat_name"],
                    use_old_proj=True,
                )

    def fix_phase(self, use_ant_pos=True):
        """
        Fix the data to be consistent with the new phasing method.

        This is a simple utility function for updating UVW coordinates calculated using
        the 'old' phasing algorithm with those calculated by the 'new' algorithm. Note
        that this step is required for using the new methods with data phased using the
        `phase` methiod prior to pyuvdata v2.2.

        Parameters
        ----------
        use_ant_pos : bool
            Use the antenna positions for determining UVW coordinates. Default is True.
        """
        # datasets phased with new phasing should not use the this method
        compatible, reason = self._old_phase_attributes_compatible()
        if not compatible:
            raise ValueError(
                "Objects with " + reason + " were not phased with the old "
                "method, so no fixing is required."
            )

        # If we are just using the antenna positions, we don't actually need to do
        # anything, since the new baseline vectors will be unaffected by the prior
        # phasing method, and the delta_w values get correctly corrected for in
        # set_uvws_from_antenna_positions.
        if use_ant_pos:
            warnings.warn("Fixing phases using antenna positions.")

            self.set_uvws_from_antenna_positions()
        else:
            warnings.warn(
                "Attempting to fix residual phasing errors from the old `phase` method "
                "using the `unproject_phase` with use_old_proj set to True. This "
                "can result in closure errors if the data were not actually phased "
                "using the old method -- caution is advised."
            )
            # Record the old values
            # if we get here, we can only have one phase center
            phase_dict = list(self.phase_center_catalog.values())[0]

            # Bring the UVWs back to ENU/unprojected
            with warnings.catch_warnings():
                warnings.filterwarnings(
                    "ignore", "The `use_old_proj` option is deprecated"
                )
                warnings.filterwarnings(
                    "ignore", "The `phase_frame` parameter is deprecated"
                )
                self.unproject_phase(
                    phase_frame=phase_dict["cat_frame"],
                    use_ant_pos=False,
                    use_old_proj=True,
                )

            # Check for any autos, since their uvws get potentially corrupted
            # by the above operation
            auto_mask = self.ant_1_array == self.ant_2_array
            if any(auto_mask):
                self.uvw_array[auto_mask, :] = 0.0

            # And rephase the data using the new algorithm
            self.phase(
                phase_dict["cat_lon"],
                phase_dict["cat_lat"],
                phase_frame=phase_dict["cat_frame"],
                epoch=phase_dict["cat_epoch"],
                cat_name=phase_dict["cat_name"],
                use_ant_pos=False,
            )

    def _deprecated_phase_helper(
        self,
        other,
        phase_center_radec,
        unphase_to_drift,
        phase_frame,
        orig_phase_frame,
        use_ant_pos,
        make_multi_phase,
    ):
        """
        Handle deprecation warnings and phasing for deprecated parameters.

        This is a helper method for `__add__`, `fast_concat` and `read` that should be
        removed in version 2.4 when all the phase-related deprecated parameters are
        removed.

        Parameters
        ----------
        other : UVData object or list of UVData objects
            UVData object or list of UVData objects. Can be an empty list.
        phase_center_radec : array_like of float
            The phase center to phase the objects to in radians.
        unphase_to_drift : bool
            Unproject the objects before combining them.
        phase_frame : str
            The astropy frame to phase to. Only used if `phase_center_radec` is set.
        orig_phase_frame : str
            Has no effect.
        use_ant_pos : bool
            If True, calculate the uvws directly from the antenna positions rather than
            from the existing uvws.
        make_multi_phase : bool
            Has no effect.

        """
        if unphase_to_drift is not None:
            warnings.warn(
                "The `unphase_to_drift` option is deprecated, and will be removed in "
                "pyuvdata v2.4. instead call `unproject_phase` on the combined object "
                "after this method.",
                DeprecationWarning,
            )
        if unphase_to_drift is None:
            unphase_to_drift = False

        if phase_frame is not None:
            warnings.warn(
                "The `phase_frame` parameter is deprecated, and will be removed in "
                "pyuvdata v2.4 along with the `phase_center_radec` parameter.",
                DeprecationWarning,
            )

        if use_ant_pos is not None:
            warnings.warn(
                "The `use_ant_pos` option is deprecated, and will be removed in "
                "pyuvdata v2.4 along with the `phase_center_radec` parameter.",
                DeprecationWarning,
            )
        else:
            use_ant_pos = True

        if phase_center_radec is not None:
            warnings.warn(
                "The `phase_center_radec` parameter is deprecated, and will be removed "
                "in pyuvdata v2.4. instead call `phase` on the combined object "
                "after this method.",
                DeprecationWarning,
            )
            if phase_frame is None:
                phase_frame = "icrs"

        if orig_phase_frame is not None:
            warnings.warn(
                "The `orig_phase_frame` option is deprecated and has no effect. It "
                "will be removed in pyuvdata v2.4.",
                DeprecationWarning,
            )

        if make_multi_phase is not None:
            warnings.warn(
                "The `make_multi_phase` option is deprecated and has no effect. It "
                "will be removed in pyuvdata v2.4.",
                DeprecationWarning,
            )

        if phase_center_radec is not None and unphase_to_drift:
            raise ValueError(
                "phase_center_radec cannot be set if unphase_to_drift is True."
            )

        if unphase_to_drift:
            if not isinstance(other, (list, tuple, np.ndarray)):
                # if this is a UVData object already, stick it in a list
                other = [other]
            if np.any(~self._check_for_cat_type("unprojected")):
                warnings.warn("Unprojecting this UVData object.")
                self.unproject_phase(use_ant_pos=use_ant_pos)
            for obj in other:
                if np.any(~obj._check_for_cat_type("unprojected")):
                    warnings.warn("Unprojecting other UVData object.")
                    obj.unproject_phase(use_ant_pos=use_ant_pos)

        if phase_center_radec is not None:
            if np.array(phase_center_radec).size != 2:
                raise ValueError("phase_center_radec should have length 2.")

            if not isinstance(other, (list, tuple, np.ndarray)):
                # if this is a UVData object already, stick it in a list
                other = [other]

            # If this object is not phased or has multiple phase centers or one that
            # is not phased close to phase_center_radec, (re)phase it.
            # Close is defined using the phase_center_ra/dec tolerances.
            obj_names = ["this"] + ["other"] * len(other)
            for obj_ind, obj in enumerate([self] + other):
                rephase = False
                if obj.Nphase > 1 or np.any(obj._check_for_cat_type("unprojected")):
                    rephase = True
                else:
                    # this only has one phase center and is projected
                    phase_dict = list(obj.phase_center_catalog.values())[0]
                    if phase_dict["cat_type"] != "sidereal":
                        rephase = True
                    elif phase_dict["cat_frame"] not in ["icrs", "gcrs"]:
                        rephase = True
                    elif phase_dict["cat_lon"] != phase_center_radec[0]:
                        rephase = True
                    elif phase_dict["cat_lat"] != phase_center_radec[1]:
                        rephase = True

                obj_name = obj_names[obj_ind]
                if rephase:
                    warnings.warn(
                        f"Phasing {obj_name} UVData object to {phase_center_radec}"
                    )
                    with warnings.catch_warnings():
                        warnings.filterwarnings(
                            "ignore", "No cat_name provided, this will become an error"
                        )
                        obj.phase(
                            phase_center_radec[0],
                            phase_center_radec[1],
                            phase_frame=phase_frame,
                            use_ant_pos=use_ant_pos,
                            allow_rephase=True,
                        )

    def __add__(
        self,
        other,
        inplace=False,
        phase_center_radec=None,
        unphase_to_drift=None,
        phase_frame=None,
        orig_phase_frame=None,
        use_ant_pos=None,
        verbose_history=False,
        run_check=True,
        check_extra=True,
        run_check_acceptability=True,
        strict_uvw_antpos_check=False,
        make_multi_phase=None,
        ignore_name=False,
    ):
        """
        Combine two UVData objects along frequency, polarization and/or baseline-time.

        Parameters
        ----------
        other : UVData object
            Another UVData object which will be added to self.
        inplace : bool
            If True, overwrite self as we go, otherwise create a third object
            as the sum of the two.
        phase_center_radec : array_like of float
            Deprecated and will be removed in version 2.4, instead call `phase` on the
            combined object after this method. The phase center to phase the files to
            before adding the objects in radians.
        unphase_to_drift : bool
            Deprecated, instead call `unproject_phase` on the combined object after
            this method. Default is False.
        phase_frame : str
            The astropy frame to phase to. Only used if `phase_center_radec` is set.
            Default is "icrs".
            Deprecated and will be removed in version 2.4 with `phase_center_radec`.
        orig_phase_frame : str
            Deprecated, has no effect. Will be removed in version 2.4.
        use_ant_pos : bool
            If True, calculate the uvws directly from the antenna positions rather than
            from the existing uvws. Only used if `unphase_to_drift` or
            `phase_center_radec` are set. Deprecated and will be removed in version 2.4
            with `unphase_to_drift` and `phase_center_radec`. Default is True.
        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.
        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.
        make_multi_phase : bool
            Deprecated and has no effect. Will be removed in version 2.4.
        ignore_name : bool
            Option to ignore the name of the phase center (`cat_name` in
            `phase_center_catalog`) when combining two UVData objects. If set to True,
            phase centers that are the same up to their name will be combined with the
            name set to the name found in the first UVData object in the sum. If set to
            False, phase centers that are the same up to the name will be kept as
            separate phase centers. Default is False.

        Raises
        ------
        ValueError
            If other is not a UVData object, self and other are not compatible
            or if data in self and other overlap.
            If `phase_center_radec` is not None and is not length 2.

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

        # Check that both objects are UVData and valid
        this.check(
            check_extra=check_extra,
            run_check_acceptability=run_check_acceptability,
            strict_uvw_antpos_check=strict_uvw_antpos_check,
        )
        if not issubclass(other.__class__, this.__class__):
            if not issubclass(this.__class__, other.__class__):
                raise ValueError(
                    "Only UVData (or subclass) objects can be "
                    "added to a UVData (or subclass) object"
                )
        other.check(
            check_extra=check_extra,
            run_check_acceptability=run_check_acceptability,
            strict_uvw_antpos_check=strict_uvw_antpos_check,
        )

        # 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."
            )

        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 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._deprecated_phase_helper(
            other,
            phase_center_radec,
            unphase_to_drift,
            phase_frame,
            orig_phase_frame,
            use_ant_pos,
            make_multi_phase,
        )

        # Define parameters that must be the same to add objects
        compatibility_params = [
            "_vis_units",
            "_telescope_name",
            "_instrument",
            "_telescope_location",
            "_Nants_telescope",
            "_antenna_names",
            "_antenna_numbers",
            "_antenna_positions",
        ]
        if not this.future_array_shapes and not this.flex_spw:
            compatibility_params.append("_channel_width")

        # Build up history string
        history_update_string = " Combined data along "
        n_axes = 0

        # Create blt arrays for convenience
        prec_t = -2 * np.floor(np.log10(this._time_array.tols[-1])).astype(int)
        prec_b = 8
        this_blts = np.array(
            [
                "_".join(
                    ["{1:.{0}f}".format(prec_t, blt[0]), str(blt[1]).zfill(prec_b)]
                )
                for blt in zip(this.time_array, this.baseline_array)
            ]
        )
        other_blts = np.array(
            [
                "_".join(
                    ["{1:.{0}f}".format(prec_t, blt[0]), str(blt[1]).zfill(prec_b)]
                )
                for blt in zip(other.time_array, other.baseline_array)
            ]
        )
        # Check we don't have overlapping data
        both_pol, this_pol_ind, other_pol_ind = np.intersect1d(
            this.polarization_array, other.polarization_array, return_indices=True
        )

        # If we have a 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: you 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
                )

        both_blts, this_blts_ind, other_blts_ind = np.intersect1d(
            this_blts, other_blts, return_indices=True
        )
        if not self.metadata_only and (
            len(both_pol) > 0 and len(both_freq) > 0 and len(both_blts) > 0
        ):
            # check that overlapping data is not valid
            if this.future_array_shapes:
                this_inds = np.ravel_multi_index(
                    (
                        this_blts_ind[:, np.newaxis, np.newaxis],
                        this_freq_ind[np.newaxis, :, np.newaxis],
                        this_pol_ind[np.newaxis, np.newaxis, :],
                    ),
                    this.data_array.shape,
                ).flatten()
                other_inds = np.ravel_multi_index(
                    (
                        other_blts_ind[:, np.newaxis, np.newaxis],
                        other_freq_ind[np.newaxis, :, np.newaxis],
                        other_pol_ind[np.newaxis, np.newaxis, :],
                    ),
                    other.data_array.shape,
                ).flatten()
            else:
                this_inds = np.ravel_multi_index(
                    (
                        this_blts_ind[:, np.newaxis, np.newaxis, np.newaxis],
                        np.zeros((1, 1, 1, 1), dtype=np.int64),
                        this_freq_ind[np.newaxis, np.newaxis, :, np.newaxis],
                        this_pol_ind[np.newaxis, np.newaxis, np.newaxis, :],
                    ),
                    this.data_array.shape,
                ).flatten()
                other_inds = np.ravel_multi_index(
                    (
                        other_blts_ind[:, np.newaxis, np.newaxis, np.newaxis],
                        np.zeros((1, 1, 1, 1), dtype=np.int64),
                        other_freq_ind[np.newaxis, np.newaxis, :, np.newaxis],
                        other_pol_ind[np.newaxis, np.newaxis, np.newaxis, :],
                    ),
                    other.data_array.shape,
                ).flatten()
            this_all_zero = np.all(this.data_array.flatten()[this_inds] == 0)
            this_all_flag = np.all(this.flag_array.flatten()[this_inds])
            other_all_zero = np.all(other.data_array.flatten()[other_inds] == 0)
            other_all_flag = np.all(other.flag_array.flatten()[other_inds])

            if this_all_zero and this_all_flag:
                # we're fine to overwrite; update history accordingly
                history_update_string = " Overwrote invalid data using pyuvdata."
                this.history += history_update_string
            elif other_all_zero and other_all_flag:
                raise ValueError(
                    "To combine these data, please run the add operation again, "
                    "but with the object whose data is to be overwritten as the "
                    "first object in the add operation."
                )
            else:
                raise ValueError(
                    "These objects have overlapping data and cannot be combined."
                )

        # find the blt indices in "other" but not in "this"
        temp = np.nonzero(~np.in1d(other_blts, this_blts))[0]
        if len(temp) > 0:
            bnew_inds = temp
            new_blts = other_blts[temp]
            history_update_string += "baseline-time"
            n_axes += 1
        else:
            bnew_inds, new_blts = ([], [])

        # if there's any overlap in blts, check extra params
        temp = np.nonzero(np.in1d(other_blts, this_blts))[0]
        if len(temp) > 0:
            # add metadata to be checked to compatibility params
            extra_params = [
                "_integration_time",
                "_lst_array",
                "_phase_center_catalog",
                "_phase_center_id_array",
                "_phase_center_app_ra",
                "_phase_center_app_dec",
                "_phase_center_frame_pa",
                "_Nphase",
                "_uvw_array",
            ]
            compatibility_params.extend(extra_params)

        # find the freq indices in "other" but not in "this"
        if this.flex_spw:
            if (this.flex_spw_polarization_array is None) != (
                other.flex_spw_polarization_array is None
            ):
                raise ValueError(
                    "Cannot add a flex-pol and non-flex-pol UVData objects. Use "
                    "the `remove_flex_pol` method to convert the objects to "
                    "have a regular polarization axis."
                )
            elif this.flex_spw_polarization_array is not None:
                this_flexpol_dict = dict(
                    zip(this.spw_array, this.flex_spw_polarization_array)
                )
                other_flexpol_dict = dict(
                    zip(other.spw_array, other.flex_spw_polarization_array)
                )
                for key in other_flexpol_dict.keys():
                    try:
                        if this_flexpol_dict[key] != other_flexpol_dict[key]:
                            raise ValueError(
                                "Cannot add a flex-pol UVData objects where the same "
                                "spectral window contains different polarizations. Use "
                                "the `remove_flex_pol` method to convert the objects "
                                "to have a regular polarization axis."
                            )
                    except KeyError:
                        this_flexpol_dict[key] = other_flexpol_dict[key]

            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 = []

        # if channel width is an array and there's any overlap in freqs,
        # check extra params
        if this.future_array_shapes or this.flex_spw:
            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:
                # add metadata to be checked to compatibility params
                extra_params = ["_channel_width"]
                compatibility_params.extend(extra_params)

        # find the pol indices in "other" but not in "this"
        temp = np.nonzero(~np.in1d(other.polarization_array, this.polarization_array))[
            0
        ]
        if len(temp) > 0:
            pnew_inds = temp
            if n_axes > 0:
                history_update_string += ", polarization"
            else:
                history_update_string += "polarization"
            n_axes += 1
        else:
            pnew_inds = []

        # Actually check compatibility parameters
        blt_inds_params = [
            "_integration_time",
            "_lst_array",
            "_phase_center_app_ra",
            "_phase_center_app_dec",
            "_phase_center_frame_pa",
            "_phase_center_id_array",
        ]
        for cp in compatibility_params:
            if cp in blt_inds_params:
                # only check that overlapping blt indices match
                this_param = getattr(this, cp)
                other_param = getattr(other, cp)
                params_match = np.allclose(
                    this_param.value[this_blts_ind],
                    other_param.value[other_blts_ind],
                    rtol=this_param.tols[0],
                    atol=this_param.tols[1],
                )
            elif cp == "_uvw_array":
                # only check that overlapping blt indices match
                params_match = np.allclose(
                    this.uvw_array[this_blts_ind, :],
                    other.uvw_array[other_blts_ind, :],
                    rtol=this._uvw_array.tols[0],
                    atol=this._uvw_array.tols[1],
                )
            elif cp == "_channel_width" and this.future_array_shapes or this.flex_spw:
                # only check that overlapping freq indices match
                params_match = np.allclose(
                    this.channel_width[this_freq_ind],
                    other.channel_width[other_freq_ind],
                    rtol=this._channel_width.tols[0],
                    atol=this._channel_width.tols[1],
                )
            else:
                params_match = getattr(this, cp) == getattr(other, cp)
            if not params_match:
                msg = (
                    "UVParameter " + cp[1:] + " does not match. "
                    "Cannot combine objects."
                )
                raise ValueError(msg)

        # Begin manipulating the objects.

        # First, handle the internal source catalogs, since merging them is kind of a
        # weird, one-off process (i.e., nothing is cat'd across a particular axis)
        this._consolidate_phase_center_catalogs(other=other, ignore_name=ignore_name)

        # 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_blts_ind) != 0:
            this_argsort = np.argsort(this_blts_ind)
            other_argsort = np.argsort(other_blts_ind)

            if np.any(this_argsort != other_argsort):
                temp_ind = np.arange(this.Nblts)
                temp_ind[this_blts_ind[this_argsort]] = temp_ind[
                    this_blts_ind[other_argsort]
                ]

                this.reorder_blts(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):
                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_pol_ind) != 0:
            this_argsort = np.argsort(this_pol_ind)
            other_argsort = np.argsort(other_pol_ind)
            if np.any(this_argsort != other_argsort):
                temp_ind = np.arange(this.Npols)
                temp_ind[this_pol_ind[this_argsort]] = temp_ind[
                    this_pol_ind[other_argsort]
                ]

                this.reorder_pols(temp_ind)

        # Pad out self to accommodate new data
        blt_order = None
        if len(bnew_inds) > 0:
            this_blts = np.concatenate((this_blts, new_blts))
            blt_order = np.argsort(this_blts)
            if not self.metadata_only:
                if this.future_array_shapes:
                    zero_pad = np.zeros((len(bnew_inds), this.Nfreqs, this.Npols))
                else:
                    zero_pad = np.zeros((len(bnew_inds), 1, this.Nfreqs, this.Npols))
                this.data_array = np.concatenate([this.data_array, zero_pad], axis=0)
                this.nsample_array = np.concatenate(
                    [this.nsample_array, zero_pad], axis=0
                )
                this.flag_array = np.concatenate(
                    [this.flag_array, 1 - zero_pad], axis=0
                ).astype(np.bool_)
            this.uvw_array = np.concatenate(
                [this.uvw_array, other.uvw_array[bnew_inds, :]], axis=0
            )[blt_order, :]
            this.time_array = np.concatenate(
                [this.time_array, other.time_array[bnew_inds]]
            )[blt_order]
            this.integration_time = np.concatenate(
                [this.integration_time, other.integration_time[bnew_inds]]
            )[blt_order]
            this.lst_array = np.concatenate(
                [this.lst_array, other.lst_array[bnew_inds]]
            )[blt_order]
            this.ant_1_array = np.concatenate(
                [this.ant_1_array, other.ant_1_array[bnew_inds]]
            )[blt_order]
            this.ant_2_array = np.concatenate(
                [this.ant_2_array, other.ant_2_array[bnew_inds]]
            )[blt_order]
            this.baseline_array = np.concatenate(
                [this.baseline_array, other.baseline_array[bnew_inds]]
            )[blt_order]
            this.phase_center_app_ra = np.concatenate(
                [this.phase_center_app_ra, other.phase_center_app_ra[bnew_inds]]
            )[blt_order]
            this.phase_center_app_dec = np.concatenate(
                [this.phase_center_app_dec, other.phase_center_app_dec[bnew_inds]]
            )[blt_order]
            this.phase_center_frame_pa = np.concatenate(
                [this.phase_center_frame_pa, other.phase_center_frame_pa[bnew_inds]]
            )[blt_order]
            this.phase_center_id_array = np.concatenate(
                [this.phase_center_id_array, other.phase_center_id_array[bnew_inds]]
            )[blt_order]

        f_order = None
        if len(fnew_inds) > 0:
            if this.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 or this.future_array_shapes:
                this.channel_width = np.concatenate(
                    [this.channel_width, other.channel_width[fnew_inds]]
                )

            if this.flex_spw:
                this.flex_spw_id_array = np.concatenate(
                    [this.flex_spw_id_array, other.flex_spw_id_array[fnew_inds]]
                )
                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)

                if this.flex_spw_polarization_array is not None:
                    this.flex_spw_polarization_array = np.array(
                        [this_flexpol_dict[key] for key in this.spw_array]
                    )
                # 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)]
            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.future_array_shapes:
                    f_order = np.argsort(this.freq_array)
                else:
                    f_order = np.argsort(this.freq_array[0, :])

            if not self.metadata_only:
                if this.future_array_shapes:
                    zero_pad = np.zeros(
                        (this.data_array.shape[0], len(fnew_inds), this.Npols)
                    )
                    this.data_array = np.concatenate(
                        [this.data_array, zero_pad], axis=1
                    )
                    this.nsample_array = np.concatenate(
                        [this.nsample_array, zero_pad], axis=1
                    )
                    this.flag_array = np.concatenate(
                        [this.flag_array, 1 - zero_pad], axis=1
                    ).astype(np.bool_)
                else:
                    zero_pad = np.zeros(
                        (this.data_array.shape[0], 1, len(fnew_inds), this.Npols)
                    )
                    this.data_array = np.concatenate(
                        [this.data_array, zero_pad], axis=2
                    )
                    this.nsample_array = np.concatenate(
                        [this.nsample_array, zero_pad], axis=2
                    )
                    this.flag_array = np.concatenate(
                        [this.flag_array, 1 - zero_pad], axis=2
                    ).astype(np.bool_)

        p_order = None
        if len(pnew_inds) > 0:
            this.polarization_array = np.concatenate(
                [this.polarization_array, other.polarization_array[pnew_inds]]
            )
            p_order = np.argsort(np.abs(this.polarization_array))
            if not self.metadata_only:
                if this.future_array_shapes:
                    zero_pad = np.zeros(
                        (
                            this.data_array.shape[0],
                            this.data_array.shape[1],
                            len(pnew_inds),
                        )
                    )
                    this.data_array = np.concatenate(
                        [this.data_array, zero_pad], axis=2
                    )
                    this.nsample_array = np.concatenate(
                        [this.nsample_array, zero_pad], axis=2
                    )
                    this.flag_array = np.concatenate(
                        [this.flag_array, 1 - zero_pad], axis=2
                    ).astype(np.bool_)
                else:
                    zero_pad = np.zeros(
                        (
                            this.data_array.shape[0],
                            1,
                            this.data_array.shape[2],
                            len(pnew_inds),
                        )
                    )
                    this.data_array = np.concatenate(
                        [this.data_array, zero_pad], axis=3
                    )
                    this.nsample_array = np.concatenate(
                        [this.nsample_array, zero_pad], axis=3
                    )
                    this.flag_array = np.concatenate(
                        [this.flag_array, 1 - zero_pad], axis=3
                    ).astype(np.bool_)

        # Now populate the data
        pol_t2o = np.nonzero(
            np.in1d(this.polarization_array, other.polarization_array)
        )[0]
        # Special handling here needed for flex_spw data
        this_freqs = this.freq_array
        other_freqs = other.freq_array
        if not this.future_array_shapes:
            this_freqs = this_freqs[0]
            other_freqs = other_freqs[0]

        if this.flex_spw:
            freq_t2o = np.zeros(this_freqs.shape, dtype=bool)
            for spw_id in set(this.spw_array).intersection(other.spw_array):
                mask = this.flex_spw_id_array == spw_id
                freq_t2o[mask] |= np.in1d(
                    this_freqs[mask], other_freqs[other.flex_spw_id_array == spw_id]
                )
            freq_t2o = np.nonzero(freq_t2o)[0]
        else:
            freq_t2o = np.nonzero(np.in1d(this_freqs, other_freqs))[0]
        blt_t2o = np.nonzero(np.in1d(this_blts, other_blts))[0]

        if not self.metadata_only:
            if this.future_array_shapes:
                this.data_array[np.ix_(blt_t2o, freq_t2o, pol_t2o)] = other.data_array
                this.nsample_array[
                    np.ix_(blt_t2o, freq_t2o, pol_t2o)
                ] = other.nsample_array
                this.flag_array[np.ix_(blt_t2o, freq_t2o, pol_t2o)] = other.flag_array
            else:
                this.data_array[
                    np.ix_(blt_t2o, [0], freq_t2o, pol_t2o)
                ] = other.data_array
                this.nsample_array[
                    np.ix_(blt_t2o, [0], freq_t2o, pol_t2o)
                ] = other.nsample_array
                this.flag_array[
                    np.ix_(blt_t2o, [0], freq_t2o, pol_t2o)
                ] = other.flag_array

            # Fix ordering
            baxis_num = 0
            if this.future_array_shapes:
                faxis_num = 1
                paxis_num = 2
            else:
                faxis_num = 2
                paxis_num = 3

            axis_dict = {
                baxis_num: {"inds": bnew_inds, "order": blt_order},
                faxis_num: {"inds": fnew_inds, "order": f_order},
                paxis_num: {"inds": pnew_inds, "order": p_order},
            }
            for axis, subdict in axis_dict.items():
                for name, param in zip(this._data_params, this.data_like_parameters):
                    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))

        if len(fnew_inds) > 0:
            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:
                this.flex_spw_id_array = this.flex_spw_id_array[f_order]

        if len(pnew_inds) > 0:
            this.polarization_array = this.polarization_array[p_order]

        # Update N parameters (e.g. Npols)
        this.Ntimes = len(np.unique(this.time_array))
        this.Nbls = len(np.unique(this.baseline_array))
        this.Nblts = this.uvw_array.shape[0]
        this.Nfreqs = this.freq_array.size
        this.Npols = this.polarization_array.shape[0]
        this.Nants_data = this._calc_nants_data()

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

        # Check specific requirements
        if 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 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
                        )

        # Reset blt_order if blt axis was added to and it is set
        if len(blt_t2o) > 0:
            this.blt_order = None

        this.set_rectangularity(force=True)

        # Check final object is self-consistent
        if run_check:
            this.check(
                check_extra=check_extra,
                run_check_acceptability=run_check_acceptability,
                strict_uvw_antpos_check=strict_uvw_antpos_check,
            )

        if not inplace:
            return this

    def __iadd__(
        self,
        other,
        phase_center_radec=None,
        unphase_to_drift=None,
        phase_frame=None,
        orig_phase_frame=None,
        use_ant_pos=None,
        run_check=True,
        check_extra=True,
        run_check_acceptability=True,
        strict_uvw_antpos_check=False,
        make_multi_phase=None,
        ignore_name=False,
    ):
        """
        In place add.

        Parameters
        ----------
        other : UVData object
            Another UVData object which will be added to self.
        phase_center_radec : array_like of float
            Deprecated and will be removed in version 2.4, instead call `phase` on the
            combined object after this method. The phase center to phase the files to
            before adding the objects in radians.
        unphase_to_drift : bool
            Deprecated, instead call `unproject_phase` on the combined object after
            this method. Default is False.
        phase_frame : str
            The astropy frame to phase to. Only used if `phase_center_radec` is set.
            Default is "icrs".
            Deprecated and will be removed in version 2.4 with `phase_center_radec`.
        orig_phase_frame : str
            Deprecated, has no effect. Will be removed in version 2.4.
        use_ant_pos : bool
            If True, calculate the uvws directly from the antenna positions rather than
            from the existing uvws. Only used if `unphase_to_drift` or
            `phase_center_radec` are set. Deprecated and will be removed in version 2.4
            with `unphase_to_drift` and `phase_center_radec`.
        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.
        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.
        make_multi_phase : bool
            Deprecated and has no effect. Will be removed in version 2.4.
        ignore_name : bool
            Option to ignore the name of the phase center (`cat_name` in
            `phase_center_catalog`) when combining two UVData objects. If set to True,
            phase centers that are the same up to their name will be combined with the
            name set to the name found in the first UVData object in the sum. If set to
            False, phase centers that are the same up to the name will be kept as
            separate phase centers. Default is False.

        Raises
        ------
        ValueError
            If other is not a UVData object, self and other are not compatible
            or if data in self and other overlap.
            If `phase_center_radec` is not None and is not length 2.

        """
        self.__add__(
            other,
            inplace=True,
            phase_center_radec=phase_center_radec,
            unphase_to_drift=unphase_to_drift,
            phase_frame=phase_frame,
            orig_phase_frame=orig_phase_frame,
            use_ant_pos=use_ant_pos,
            run_check=run_check,
            check_extra=check_extra,
            run_check_acceptability=run_check_acceptability,
            strict_uvw_antpos_check=strict_uvw_antpos_check,
            make_multi_phase=make_multi_phase,
            ignore_name=ignore_name,
        )
        return self

    def fast_concat(
        self,
        other,
        axis,
        inplace=False,
        phase_center_radec=None,
        unphase_to_drift=None,
        phase_frame=None,
        orig_phase_frame=None,
        use_ant_pos=None,
        verbose_history=False,
        run_check=True,
        check_extra=True,
        run_check_acceptability=True,
        strict_uvw_antpos_check=False,
        ignore_name=None,
    ):
        """
        Concatenate two UVData 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.

        Note that if objects have different phasing (different phase_center_catalogs)
        they can still be combined and the phasing information will be preserved.
        However, the phase center ID numbers may be changed on any of the objects passed
        into this method.

        Parameters
        ----------
        other : UVData object or list of UVData objects
            UVData object or list of UVData 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: 'blt', 'freq', 'polarization'.
        inplace : bool
            If True, overwrite self as we go, otherwise create a third object
            as the sum of the two.
        phase_center_radec : array_like of float
            Deprecated and will be removed in version 2.4, instead call `phase` on the
            combined object after this method. The phase center to phase the files to
            before concatenating the objects in radians.
        unphase_to_drift : bool
            Deprecated, instead call `unproject_phase` on the combined object after
            this method. Unproject the objects before combining them.
        phase_frame : str
            The astropy frame to phase to. Only used if `phase_center_radec` is set.
            Deprecated and will be removed in version 2.4 with `phase_center_radec`.
        orig_phase_frame : str
            Deprecated, has no effect. Will be removed in version 2.4.
        use_ant_pos : bool
            If True, calculate the uvws directly from the antenna positions rather than
            from the existing uvws. Only used if `unphase_to_drift` or
            `phase_center_radec` are set. Deprecated and will be removed in version 2.4
            with `unphase_to_drift` and `phase_center_radec`.
        verbose_history : bool
            Option to allow more verbose history. If True and if the histories for the
            objects are different, the combined object will keep all the history of
            all input objects (if many objects are combined this can lead to very long
            histories). If False and if the histories for the objects are different,
            the combined object will have the history of the first object and only the
            parts of the other object histories 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.
        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.
        ignore_name : bool
            Option to ignore the name of the phase center (`cat_name` in
            `phase_center_catalog`) when combining two UVData objects. If set to True,
            phase centers that are the same up to their name will be combined with the
            name set to the name found in the first UVData object in the sum. If set to
            False, phase centers that are the same up to the name will be kept as
            separate phase centers. Default is False.

        Raises
        ------
        ValueError
            If other is not a UVData object, axis is not an allowed value or if
            self and other are not compatible.

        """
        allowed_axes = ["blt", "freq", "polarization"]
        if axis not in allowed_axes:
            raise ValueError("Axis must be one of: " + ", ".join(allowed_axes))

        if inplace:
            this = self
        else:
            this = self.copy()
        if not isinstance(other, (list, tuple, np.ndarray)):
            # if this is a UVData object already, stick it in a list
            other = [other]

        # Check that both objects are UVData and valid
        this.check(
            check_extra=check_extra,
            run_check_acceptability=run_check_acceptability,
            strict_uvw_antpos_check=strict_uvw_antpos_check,
        )
        for obj in other:
            if not issubclass(obj.__class__, this.__class__):
                if not issubclass(this.__class__, obj.__class__):
                    raise ValueError(
                        "Only UVData (or subclass) objects can be "
                        "added to a UVData (or subclass) object"
                    )
            obj.check(
                check_extra=check_extra,
                run_check_acceptability=run_check_acceptability,
                strict_uvw_antpos_check=strict_uvw_antpos_check,
            )

        # Check to make sure that both objects are consistent w/ use of flex_spw
        for obj in other:
            if this.flex_spw != obj.flex_spw:
                raise ValueError(
                    "All objects must have the same `flex_spw` value (True or False)."
                )

        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."
            )

        # 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."
                )

        this._deprecated_phase_helper(
            other,
            phase_center_radec,
            unphase_to_drift,
            phase_frame,
            orig_phase_frame,
            use_ant_pos,
            None,
        )

        # update the phase_center_catalog to make them consistent across objects
        # Doing this as a binary tree merge
        # The left object in each loop will have its phase center IDs updated.
        uv_list = [this] + other
        while len(uv_list) > 1:
            for uv1, uv2 in zip(uv_list[0::2], uv_list[1::2]):
                uv1._consolidate_phase_center_catalogs(
                    other=uv2, ignore_name=ignore_name
                )
            uv_list = uv_list[0::2]
        # Because self was at the beginning of the list,
        # all the phase centers are merged into it at the end of this loop

        compatibility_params = [
            "_vis_units",
            "_telescope_name",
            "_instrument",
            "_telescope_location",
            "_Nants_telescope",
            "_antenna_names",
            "_antenna_numbers",
            "_antenna_positions",
        ]
        if not this.future_array_shapes and not this.flex_spw:
            compatibility_params.append("_channel_width")

        history_update_string = " Combined data along "

        if axis == "freq":
            history_update_string += "frequency"
            compatibility_params += [
                "_polarization_array",
                "_ant_1_array",
                "_ant_2_array",
                "_integration_time",
                "_uvw_array",
                "_lst_array",
                "_phase_center_id_array",
            ]
        elif axis == "polarization":
            history_update_string += "polarization"
            compatibility_params += [
                "_freq_array",
                "_channel_width",
                "_ant_1_array",
                "_ant_2_array",
                "_integration_time",
                "_uvw_array",
                "_lst_array",
                "_phase_center_id_array",
            ]
        elif axis == "blt":
            history_update_string += "baseline-time"
            compatibility_params += ["_freq_array", "_polarization_array"]

        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)

        if 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]
                )
                this.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 = len(this.spw_array)
            elif this_has_spw_id or np.any(other_has_spw_id):
                this.flex_spw_id_array = np.full(
                    this.Nfreqs, this.spw_array[0], dtype=int
                )

            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 not self.metadata_only:
                if this.future_array_shapes:
                    axis_num = 1
                else:
                    axis_num = 2
                this.data_array = np.concatenate(
                    [this.data_array] + [obj.data_array for obj in other], axis=axis_num
                )
                this.nsample_array = np.concatenate(
                    [this.nsample_array] + [obj.nsample_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
                )
        elif axis == "polarization":
            this.polarization_array = np.concatenate(
                [this.polarization_array] + [obj.polarization_array for obj in other]
            )
            this.Npols = sum([this.Npols] + [obj.Npols for obj in other])

            if not uvutils._test_array_constant_spacing(this._polarization_array):
                warnings.warn(
                    "Combined polarizations are not evenly spaced. This will "
                    "make it impossible to write this data out to some file types."
                )

            if not self.metadata_only:
                if this.future_array_shapes:
                    axis_num = 2
                else:
                    axis_num = 3
                this.data_array = np.concatenate(
                    [this.data_array] + [obj.data_array for obj in other], axis=axis_num
                )
                this.nsample_array = np.concatenate(
                    [this.nsample_array] + [obj.nsample_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
                )
        elif axis == "blt":
            this.Nblts = sum([this.Nblts] + [obj.Nblts for obj in other])
            this.ant_1_array = np.concatenate(
                [this.ant_1_array] + [obj.ant_1_array for obj in other]
            )
            this.ant_2_array = np.concatenate(
                [this.ant_2_array] + [obj.ant_2_array for obj in other]
            )
            this.Nants_data = this._calc_nants_data()
            this.uvw_array = np.concatenate(
                [this.uvw_array] + [obj.uvw_array for obj in other], axis=0
            )
            this.time_array = np.concatenate(
                [this.time_array] + [obj.time_array for obj in other]
            )
            this.Ntimes = len(np.unique(this.time_array))
            this.lst_array = np.concatenate(
                [this.lst_array] + [obj.lst_array for obj in other]
            )
            this.baseline_array = np.concatenate(
                [this.baseline_array] + [obj.baseline_array for obj in other]
            )
            this.Nbls = len(np.unique(this.baseline_array))
            this.integration_time = np.concatenate(
                [this.integration_time] + [obj.integration_time for obj in other]
            )
            this.phase_center_app_ra = np.concatenate(
                [this.phase_center_app_ra] + [obj.phase_center_app_ra for obj in other]
            )
            this.phase_center_app_dec = np.concatenate(
                [this.phase_center_app_dec]
                + [obj.phase_center_app_dec for obj in other]
            )
            this.phase_center_frame_pa = np.concatenate(
                [this.phase_center_frame_pa]
                + [obj.phase_center_frame_pa for obj in other]
            )
            this.phase_center_id_array = np.concatenate(
                [this.phase_center_id_array]
                + [obj.phase_center_id_array for obj in other]
            )
            if not self.metadata_only:
                this.data_array = np.concatenate(
                    [this.data_array] + [obj.data_array for obj in other], axis=0
                )
                this.nsample_array = np.concatenate(
                    [this.nsample_array] + [obj.nsample_array for obj in other], axis=0
                )
                this.flag_array = np.concatenate(
                    [this.flag_array] + [obj.flag_array for obj in other], axis=0
                )

        # 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,
                strict_uvw_antpos_check=strict_uvw_antpos_check,
            )

        if not inplace:
            return this

    def sum_vis(
        self,
        other,
        inplace=False,
        difference=False,
        verbose_history=False,
        run_check=True,
        check_extra=True,
        run_check_acceptability=True,
        strict_uvw_antpos_check=False,
        override_params=None,
    ):
        """
        Sum visibilities between two UVData objects.

        By default requires that all UVParameters are the same on the two objects
        except for `history`, `data_array`, and `extra_keywords`.
        If keys in `extra_keywords` have different values the values from the first
        object are taken.

        Parameters
        ----------
        other : UVData object
            Another UVData object which will be added to self.
        difference : bool
            If True, differences the visibilities of the two UVData objects
            rather than summing them.
        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 (this can lead to 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.
        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.
        override_params : array_like of strings
            List of object UVParameters to omit from compatibility check. Overridden
            parameters will not be compared between the objects, and the values
            for these parameters will be taken from the first object.

        Returns
        -------
        UVData Object
            If inplace parameter is False.

        Raises
        ------
        ValueError
            If other is not a UVData object, or if self and other
            are not compatible.

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

        # Check that both objects are UVData and valid
        this.check(
            check_extra=check_extra,
            run_check_acceptability=run_check_acceptability,
            strict_uvw_antpos_check=strict_uvw_antpos_check,
        )
        if not issubclass(other.__class__, this.__class__):
            if not issubclass(this.__class__, other.__class__):
                raise ValueError(
                    "Only UVData (or subclass) objects can be "
                    "added to a UVData (or subclass) object"
                )
        other.check(
            check_extra=check_extra,
            run_check_acceptability=run_check_acceptability,
            strict_uvw_antpos_check=strict_uvw_antpos_check,
        )

        # 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."
            )

        compatibility_params = list(this.__iter__())
        remove_params = ["_history", "_data_array", "_extra_keywords"]

        # Add underscores to override_params to match list from __iter__()
        # Add to parameters to be removed
        if override_params and all(isinstance(param, str) for param in override_params):
            for param in override_params:
                if param[0] != "_":
                    param = "_" + param
                if param not in compatibility_params:
                    msg = (
                        "Provided parameter " + param[1:] + " is not a recognizable "
                        "UVParameter."
                    )
                    raise ValueError(msg)
                remove_params.append(param)

        # compatibility_params should define the parameters that need to
        # be the same for objects to be summed or diffed
        compatibility_params = list(set(compatibility_params) - set(remove_params))

        # Check each UVParameter in compatibility_params
        for param in compatibility_params:
            params_match = getattr(this, param) == getattr(other, param)
            if not params_match:
                msg = (
                    "UVParameter " + param[1:] + " does not match. Cannot "
                    "combine objects."
                )
                raise ValueError(msg)

        # Merge extra keywords
        for intersection in set(this.extra_keywords.keys()) & set(
            other.extra_keywords.keys()
        ):
            if this.extra_keywords[intersection] != other.extra_keywords[intersection]:
                warnings.warn(
                    "Keyword " + intersection + " in _extra_keywords is different "
                    "in the two objects. Taking the first object's entry."
                )

        # Merge extra_keywords lists, taking values from the first object
        this.extra_keywords = dict(
            list(other.extra_keywords.items()) + list(this.extra_keywords.items())
        )

        # Do the summing / differencing
        if difference:
            this.data_array = this.data_array - other.data_array
            history_update_string = " Visibilities differenced using pyuvdata."
        else:
            this.data_array = this.data_array + other.data_array
            history_update_string = " Visibilities summed using pyuvdata."

        histories_match = uvutils._check_histories(this.history, other.history)

        this.history += history_update_string
        if not histories_match:
            if verbose_history:
                this.history += " Second 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 second object history follows. "
                        + extra_history
                    )

        # merge file names
        this.filename = uvutils._combine_filenames(this.filename, other.filename)

        # Check final object is self-consistent
        if run_check:
            this.check(
                check_extra=check_extra,
                run_check_acceptability=run_check_acceptability,
                strict_uvw_antpos_check=strict_uvw_antpos_check,
            )

        if not inplace:
            return this

    def diff_vis(
        self,
        other,
        inplace=False,
        run_check=True,
        check_extra=True,
        run_check_acceptability=True,
        strict_uvw_antpos_check=False,
        override_params=None,
    ):
        """
        Difference visibilities between two UVData objects.

        By default requires that all UVParameters are the same on the two objects
        except for `history`, `data_array`, and `extra_keywords`.
        If keys in `extra_keywords` have different values the values from the first
        object are taken.

        Parameters
        ----------
        other : UVData object
            Another UVData object which will be added to self.
        inplace : bool
            If True, overwrite self as we go, otherwise create a third object
            as the sum of the two.
        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.
        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.
        override_params : array_like of strings
            List of object UVParameters to omit from compatibility check. Overridden
            parameters will not be compared between the objects, and the values
            for these parameters will be taken from the first object.

        Returns
        -------
        UVData Object
            If inplace parameter is False.

        Raises
        ------
        ValueError
            If other is not a UVData object, or if self and other
            are not compatible.

        """
        if inplace:
            self.sum_vis(
                other,
                difference=True,
                inplace=inplace,
                run_check=run_check,
                check_extra=check_extra,
                run_check_acceptability=run_check_acceptability,
                strict_uvw_antpos_check=strict_uvw_antpos_check,
                override_params=override_params,
            )
        else:
            return self.sum_vis(
                other,
                difference=True,
                inplace=inplace,
                run_check=run_check,
                check_extra=check_extra,
                run_check_acceptability=run_check_acceptability,
                strict_uvw_antpos_check=strict_uvw_antpos_check,
                override_params=override_params,
            )

    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. 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.

        """
        return uvutils.parse_ants(
            uv=self,
            ant_str=ant_str,
            print_toggle=print_toggle,
            x_orientation=self.x_orientation,
        )

    def _select_preprocess(
        self,
        antenna_nums,
        antenna_names,
        ant_str,
        bls,
        frequencies,
        freq_chans,
        times,
        time_range,
        lsts,
        lst_range,
        polarizations,
        blt_inds,
        phase_center_ids,
        catalog_names,
    ):
        """
        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). 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 unless
            `keep_all_metadata` is False). This cannot be provided if
            `antenna_nums` is also provided.
        bls : list of tuple or list of int, optional
            A list of antenna number tuples (e.g. [(0, 1), (3, 2)]), a list of
            baseline 3-tuples (e.g. [(0, 1, 'xx'), (2, 3, 'yy')]), or a list of
            baseline numbers (e.g. [67599, 71699, 73743]) 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. Cannot be used with `time_range`, `lsts`, or
            `lst_array`.
        time_range : array_like of float, optional
            The time range in Julian Date to keep in the object, 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`, or `lst_array`.
        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 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.
        sources : str or list of str, optional
            Names of the phase centers to keep in the object, matched against entries
            in the phase center catalog ("cat_name").
        phase_center_ids : array_like of int, optional
            Phase center IDs to keep on the object (effectively a selection on
            baseline-times). Cannot be used with `catalog_names`.
        catalog_names : str or array-like of str, optional
            The names of the phase centers (sources) to keep in the object, which should
            match exactly in spelling and capitalization. Cannot be used with
            `phase_center_ids`.

        Returns
        -------
        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.
        """
        # build up history string as we go
        history_update_string = "  Downselected to specific "
        n_selects = 0

        if ant_str is not None:
            if not (
                antenna_nums is None
                and antenna_names is None
                and bls is None
                and polarizations is None
            ):
                raise ValueError(
                    "Cannot provide ant_str with antenna_nums, antenna_names, "
                    "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()
            history_update_string += "baseline-times"
            n_selects += 1

        if (phase_center_ids is not None) and (catalog_names is not None):
            raise ValueError("Cannot set both phase_center_ids and catalog_names.")

        if catalog_names is not None:
            phase_center_ids = self._look_for_name(catalog_names)

        if phase_center_ids is not None:
            phase_center_ids = np.array(uvutils._get_iterable(phase_center_ids))
            pc_blt_inds = np.nonzero(
                np.isin(self.phase_center_id_array, phase_center_ids)
            )[0]
            if blt_inds is not None:
                # Use intersection (and) to join phase_center_ids
                # with blt_inds
                blt_inds = np.array(
                    list(set(blt_inds).intersection(pc_blt_inds)), dtype=np.int64
                )
            else:
                blt_inds = pc_blt_inds

            update_substring = (
                "phase center IDs" if (catalog_names is None) else "catalog names"
            )
            if n_selects > 0:
                history_update_string += ", " + update_substring
            else:
                history_update_string += update_substring
            n_selects += 1

        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."
                )

            if not isinstance(antenna_names, (list, tuple, np.ndarray)):
                antenna_names = (antenna_names,)
            if np.array(antenna_names).ndim > 1:
                antenna_names = np.array(antenna_names).flatten()
            antenna_nums = []
            for s in antenna_names:
                if s not in self.antenna_names:
                    raise ValueError(
                        "Antenna name {a} is not present in the antenna_names"
                        " array".format(a=s)
                    )
                antenna_nums.append(
                    self.antenna_numbers[np.where(np.array(self.antenna_names) == s)][0]
                )

        if antenna_nums is not None:
            antenna_nums = uvutils._get_iterable(antenna_nums)
            antenna_nums = np.asarray(antenna_nums)
            if antenna_nums.ndim > 1:
                antenna_nums = antenna_nums.flatten()
            if n_selects > 0:
                history_update_string += ", antennas"
            else:
                history_update_string += "antennas"
            n_selects += 1
            # Check to make sure that we actually have these antenna nums in the data
            ant_check = np.logical_or(
                np.isin(antenna_nums, self.ant_1_array),
                np.isin(antenna_nums, self.ant_2_array),
            )
            if not np.all(ant_check):
                raise ValueError(
                    f"Antenna number {antenna_nums[~ant_check]} is not present in the "
                    "ant_1_array or ant_2_array"
                )
            ant_blt_inds = np.where(
                np.logical_and(
                    np.isin(self.ant_1_array, antenna_nums),
                    np.isin(self.ant_2_array, antenna_nums),
                )
            )[0]
        else:
            ant_blt_inds = None

        if bls is not None:
            if isinstance(bls, list) and all(
                isinstance(bl_ind, (int, np.integer)) for bl_ind in bls
            ):
                for bl_ind in bls:
                    if not (bl_ind in self.baseline_array):
                        raise ValueError(
                            "Baseline number {i} is not present in the "
                            "baseline_array".format(i=bl_ind)
                        )
                bls = list(zip(*self.baseline_to_antnums(bls)))
            elif isinstance(bls, tuple) and (len(bls) == 2 or len(bls) == 3):
                bls = [bls]
            if len(bls) == 0 or not all(isinstance(item, tuple) for item in bls):
                raise ValueError(
                    "bls must be a list of tuples of antenna numbers "
                    "(optionally with polarization) or a list of baseline numbers."
                )
            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 antenna numbers "
                    "(optionally with polarization) or a list of baseline numbers."
                )
            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 ant_str is None:
                if n_selects > 0:
                    history_update_string += ", baselines"
                else:
                    history_update_string += "baselines"
            else:
                history_update_string += "antenna pairs"
            n_selects += 1
            bls_blt_inds = np.zeros(0, dtype=np.int64)
            bl_pols = set()
            for bl in bls:
                wh1 = np.where(
                    np.logical_and(self.ant_1_array == bl[0], self.ant_2_array == bl[1])
                )[0]
                if len(wh1) > 0:
                    bls_blt_inds = np.append(bls_blt_inds, list(wh1))
                    if len(bl) == 3:
                        bl_pols.add(bl[2])
                else:
                    wh2 = np.where(
                        np.logical_and(
                            self.ant_1_array == bl[1], self.ant_2_array == bl[0]
                        )
                    )[0]

                    if len(wh2) > 0:
                        bls_blt_inds = np.append(bls_blt_inds, list(wh2))
                        if len(bl) == 3:
                            # find conjugate polarization
                            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 = np.array(
                    list(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 = np.array(
                    list(set(blt_inds).intersection(ant_blt_inds)), dtype=np.int64
                )
            else:
                blt_inds = ant_blt_inds

        have_times = times is not None
        have_time_range = time_range is not None
        have_lsts = lsts is not None
        have_lst_range = lst_range is not None
        if (
            np.count_nonzero([have_times, have_time_range, have_lsts, have_lst_range])
            > 1
        ):
            raise ValueError(
                "Only one of [times, time_range, lsts, lst_range] may be "
                "specified per selection operation."
            )

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

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

        if time_range is not None:
            if np.size(time_range) != 2:
                raise ValueError("time_range must be length 2.")

            time_blt_inds = np.nonzero(
                (self.time_array <= time_range[1]) & (self.time_array >= time_range[0])
            )[0]
            if time_blt_inds.size == 0:
                raise ValueError(
                    f"No elements in time range between {time_range[0]} and "
                    f"{time_range[1]}."
                )

        if lsts is not None:
            if np.any(np.asarray(lsts) > 2 * np.pi):
                warnings.warn(
                    "The lsts parameter contained a value greater than 2*pi. "
                    "LST values are assumed to be in radians, not hours."
                )
            lsts = uvutils._get_iterable(lsts)
            if np.array(lsts).ndim > 1:
                lsts = np.array(lsts).flatten()

            time_blt_inds = np.zeros(0, dtype=np.int64)
            for lst in lsts:
                if np.any(
                    np.isclose(
                        self.lst_array,
                        lst,
                        rtol=self._lst_array.tols[0],
                        atol=self._lst_array.tols[1],
                    )
                ):
                    time_blt_inds = np.append(
                        time_blt_inds,
                        np.where(
                            np.isclose(
                                self.lst_array,
                                lst,
                                rtol=self._lst_array.tols[0],
                                atol=self._lst_array.tols[1],
                            )
                        )[0],
                    )
                else:
                    raise ValueError(f"LST {lst} is not present in the lst_array")

        if lst_range is not None:
            if np.size(lst_range) != 2:
                raise ValueError("lst_range must be length 2.")
            if np.any(np.asarray(lst_range) > 2 * np.pi):
                warnings.warn(
                    "The lst_range contained a value greater than 2*pi. "
                    "LST values are assumed to be in radians, not hours."
                )
            if lst_range[1] < lst_range[0]:
                # we're wrapping around LST = 2*pi = 0
                lst_range_1 = [lst_range[0], 2 * np.pi]
                lst_range_2 = [0, lst_range[1]]
                time_blt_inds1 = np.nonzero(
                    (self.lst_array <= lst_range_1[1])
                    & (self.lst_array >= lst_range_1[0])
                )[0]
                time_blt_inds2 = np.nonzero(
                    (self.lst_array <= lst_range_2[1])
                    & (self.lst_array >= lst_range_2[0])
                )[0]
                time_blt_inds = np.union1d(time_blt_inds1, time_blt_inds2)
            else:
                time_blt_inds = np.nonzero(
                    (self.lst_array <= lst_range[1]) & (self.lst_array >= lst_range[0])
                )[0]
            if time_blt_inds.size == 0:
                raise ValueError(
                    f"No elements in LST range between {lst_range[0]} and "
                    f"{lst_range[1]}."
                )

        if times is not None or time_range is not None:
            if n_selects > 0:
                history_update_string += ", times"
            else:
                history_update_string += "times"
            n_selects += 1

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

        if lsts is not None or lst_range is not None:
            if n_selects > 0:
                history_update_string += ", lsts"
            else:
                history_update_string += "lsts"
            n_selects += 1

            if blt_inds is not None:
                # Use intesection (and) to join
                # antenna_names/nums/ant_pairs_nums/blt_inds with times
                blt_inds = np.array(
                    list(set(blt_inds).intersection(time_blt_inds)), dtype=np.int64
                )
            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 max(blt_inds) >= self.Nblts:
                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:
                if self.future_array_shapes:
                    frequencies = self.freq_array[freq_chans]
                else:
                    frequencies = self.freq_array[0, freq_chans]
            else:
                frequencies = uvutils._get_iterable(frequencies)
                if self.future_array_shapes:
                    frequencies = np.sort(
                        list(set(frequencies) | set(self.freq_array[freq_chans]))
                    )
                else:
                    frequencies = np.sort(
                        list(set(frequencies) | set(self.freq_array[0, 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

            if self.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(
                    "Frequency %g is not present in the freq_array"
                    % frequencies[np.where(~freq_check)[0][0]]
                )
            freq_inds = np.where(np.isin(freq_arr_use, frequencies))[0]

            if len(frequencies) > 1:
                freq_ind_separation = freq_inds[1:] - freq_inds[:-1]
                if self.flex_spw_id_array is not None:
                    freq_ind_separation = freq_ind_separation[
                        np.diff(self.flex_spw_id_array[freq_inds]) == 0
                    ]
                if not uvutils._test_array_constant(freq_ind_separation):
                    warnings.warn(
                        "Selected frequencies are not evenly spaced. This "
                        "will make it impossible to write this data out to "
                        "some file types"
                    )
                elif np.max(freq_ind_separation) > 1:
                    warnings.warn(
                        "Selected frequencies are not contiguous. This "
                        "will make it impossible to write this data out to "
                        "some file types."
                    )

            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)
            spw_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]
                    )
                elif (
                    False
                    if (self.flex_spw_polarization_array is None)
                    else (p_num in self.flex_spw_polarization_array)
                ):
                    spw_inds = np.append(
                        spw_inds, np.where(self.flex_spw_polarization_array == p_num)[0]
                    )
                else:
                    raise ValueError(
                        "Polarization {p} is not present in the "
                        "polarization_array".format(p=p)
                    )

            if len(spw_inds) > 0:
                # Since this is a flex-pol data set, we need to filter on the freq
                # axis instead of the pol axis
                pol_inds = None

                if freq_inds is None:
                    freq_inds = np.where(
                        np.isin(self.flex_spw_id_array, self.spw_array[spw_inds])
                    )[0]
                else:
                    freq_inds = np.array(freq_inds)
                    freq_inds = freq_inds[
                        np.isin(
                            self.flex_spw_id_array[freq_inds], self.spw_array[spw_inds]
                        )
                    ]

                freq_inds = sorted(freq_inds)
                # Trap a corner case here where the frequency and polarization selects
                # on a flex-pol data set end up with no actual data being selected.
                if len(freq_inds) == 0:
                    raise ValueError(
                        "No data matching this polarization and frequency selection "
                        "in this UVData object."
                    )
                if not uvutils._test_array_constant_spacing(
                    np.unique(self.flex_spw_polarization_array[spw_inds])
                ):
                    warnings.warn(
                        "Selected polarization values are not evenly spaced. This "
                        "will make it impossible to write this data out to "
                        "some file types"
                    )
            else:
                pol_inds = np.unique(pol_inds)
                if len(pol_inds) > 2:
                    if not uvutils._test_array_constant_spacing(pol_inds):
                        warnings.warn(
                            "Selected polarization values are not evenly spaced. This "
                            "will make it impossible to write this data out to "
                            "some file types"
                        )
                pol_inds = pol_inds.tolist()
        else:
            pol_inds = None

        history_update_string += " using pyuvdata."

        return blt_inds, freq_inds, pol_inds, history_update_string

    def _select_by_index(
        self,
        blt_inds,
        freq_inds,
        pol_inds,
        history_update_string,
        keep_all_metadata=True,
    ):
        """
        Perform select based on indexing 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.
        """
        # Create a dictionary that we can loop over an update if need be
        ind_dict = {"Nblts": blt_inds, "Nfreqs": freq_inds, "Npols": pol_inds}

        # During each loop interval, we pop off an element of this dict, so continue
        # until the dict is empty.
        while len(ind_dict):
            # This is an easy way to grab the first key in the dict
            key = next(iter(ind_dict))
            # Grab the corresponding index array
            ind_arr = ind_dict.pop(key)

            # If nothing to select on, bail!
            if ind_arr is None:
                continue

            for param in self:
                # For each attribute, if the value is None, then bail, otherwise
                # attempt to figure out along which axis ind_arr will apply.
                attr = getattr(self, param)
                if attr.value is not None:
                    try:
                        sel_axis = attr.form.index(key)
                    except (AttributeError, ValueError):
                        # If form is not a tuple/list (and therefore not
                        # array-like), it'll throw an AttributeError, and if key is
                        # not found in the tuple/list, it'll throw a ValueError.
                        # In both cases, skip!
                        continue

                    if isinstance(attr.value, np.ndarray):
                        # If we're working with an ndarray, use take to slice along
                        # the axis that we want to grab from.
                        attr.value = attr.value.take(ind_arr, axis=sel_axis)
                    elif isinstance(attr.value, list):
                        # If this is a list, it _should_ always have 1-dimension.
                        assert sel_axis == 0, (
                            "Something is wrong, sel_axis != 0 when selecting on a "
                            "list, which should not be possible. Please file an "
                            "issue in our GitHub issue log so that we can fix it."
                        )
                        attr.value = [attr.value[idx] for idx in ind_arr]

            if key == "Nblts":
                # Process post blt-specific selection actions, including counting
                # unique times antennas/baselines in the data.
                self.Nblts = len(ind_arr)
                self.Nbls = len(np.unique(self.baseline_array))
                self.Ntimes = len(np.unique(self.time_array))
                self.Nants_data = self._calc_nants_data()

                if not keep_all_metadata:
                    # If we are dropping metadata and selecting on blts, then add
                    # evaluate the antenna axis of all parameters
                    ind_dict["Nants_telescope"] = np.where(
                        np.isin(
                            self.antenna_numbers,
                            list(set(self.ant_1_array).union(self.ant_2_array)),
                        )
                    )[0]
            elif key == "Nfreqs":
                # Process post freq-specific selection actions
                self.Nfreqs = len(ind_arr)
                if self.flex_spw_id_array is not None:
                    # If we are dropping channels, then evaluate the spw axis
                    ind_dict["Nspws"] = np.where(
                        np.isin(self.spw_array, self.flex_spw_id_array)
                    )[0]
            elif key == "Npols":
                # Count the number of unique pols after pol-based selection
                self.Npols = len(ind_arr)
            elif key == "Nspws":
                # Count the number of unique pols after spw-based selection
                self.Nspws = len(ind_arr)
            elif key == "Nants_telescope":
                # Count the number of unique ants after ant-based selection
                self.Nants_telescope = len(ind_arr)

        # Update the history string
        self.history += history_update_string

    def select(
        self,
        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,
        inplace=True,
        keep_all_metadata=True,
        run_check=True,
        check_extra=True,
        run_check_acceptability=True,
        strict_uvw_antpos_check=False,
    ):
        """
        Downselect data to keep on the object along various axes.

        Axes that can be selected along include antenna names or numbers,
        antenna pairs, frequencies, times and polarizations. Specific
        baseline-time indices can also be selected, 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.
        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 unless
            `keep_all_metadata` is False). This cannot be provided if
            `antenna_nums` 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. Cannot be used with `time_range`.
        time_range : array_like of float, optional
            The time range in Julian Date to keep in the object, 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 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.
        phase_center_ids : array_like of int, optional
            Phase center IDs to keep on the object (effectively a selection on
            baseline-times). Cannot be used with `catalog_names`.
        catalog_names : str or array-like of str, optional
            The names of the phase centers (sources) to keep in the object, which should
            match exactly in spelling and capitalization. Cannot be used with
            `phase_center_ids`.
        inplace : bool
            Option to perform the select directly on self or return a new UVData
            object with just the selected data (the default is True, meaning the
            select will be done on self).
        keep_all_metadata : bool
            Option to keep all the metadata associated with antennas, even those
            that do do not have data associated with them after the select option.
        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).
        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.

        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_obj = self
        else:
            uv_obj = self.copy()

        # Figure out which index positions we want to hold on to.
        (
            blt_inds,
            freq_inds,
            pol_inds,
            history_update_string,
        ) = uv_obj._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,
        )

        # Call the low-level selection method.
        uv_obj._select_by_index(
            blt_inds, freq_inds, pol_inds, history_update_string, keep_all_metadata
        )

        # Update the rectangularity attributes
        if blt_inds is not None:
            self.set_rectangularity(force=True)

        # If we have a flex-pol data set, but we only have one pol, then this doesn't
        # need to be flex-pol anymore, and we can drop it here
        if uv_obj.flex_spw_polarization_array is not None:
            if len(np.unique(uv_obj.flex_spw_polarization_array)) == 1:
                uv_obj.remove_flex_pol()

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

        if not inplace:
            return uv_obj

    def _harmonize_resample_arrays(
        self,
        inds_to_keep,
        temp_baseline,
        temp_id_array,
        temp_time,
        temp_int_time,
        temp_data,
        temp_flag,
        temp_nsample,
    ):
        """
        Make a self-consistent object after up/downsampling.

        This function is called by both upsample_in_time and downsample_in_time.
        See those functions for more information about arguments.
        """
        self.baseline_array = self.baseline_array[inds_to_keep]
        self.time_array = self.time_array[inds_to_keep]
        self.integration_time = self.integration_time[inds_to_keep]
        self.phase_center_id_array = self.phase_center_id_array[inds_to_keep]

        self.baseline_array = np.concatenate((self.baseline_array, temp_baseline))
        self.time_array = np.concatenate((self.time_array, temp_time))
        self.integration_time = np.concatenate((self.integration_time, temp_int_time))
        self.phase_center_id_array = np.concatenate(
            (self.phase_center_id_array, temp_id_array)
        )
        if not self.metadata_only:
            self.data_array = self.data_array[inds_to_keep]
            self.flag_array = self.flag_array[inds_to_keep]
            self.nsample_array = self.nsample_array[inds_to_keep]

            # concatenate temp array with existing arrays
            self.data_array = np.concatenate((self.data_array, temp_data), axis=0)
            self.flag_array = np.concatenate((self.flag_array, temp_flag), axis=0)
            self.nsample_array = np.concatenate(
                (self.nsample_array, temp_nsample), axis=0
            )

        # set antenna arrays from baseline_array
        self.ant_1_array, self.ant_2_array = self.baseline_to_antnums(
            self.baseline_array
        )

        # update metadata
        self.Nblts = self.baseline_array.shape[0]
        self.Ntimes = np.unique(self.time_array).size
        self.uvw_array = np.zeros((self.Nblts, 3))

        # set lst array
        self.set_lsts_from_time_array()

        # update app source coords to new times
        self._set_app_coords_helper()

        # temporarily store the metadata only to calculate UVWs correctly
        uv_temp = self.copy(metadata_only=True)

        # properly calculate the UVWs self-consistently
        uv_temp.set_uvws_from_antenna_positions(allow_phasing=True)
        self.uvw_array = uv_temp.uvw_array

        return

    def upsample_in_time(
        self,
        max_int_time,
        blt_order="time",
        minor_order="baseline",
        summing_correlator_mode=False,
        allow_drift=False,
    ):
        """
        Resample to a shorter integration time.

        This method will resample a UVData object such that all data samples have
        an integration time less than or equal to the `max_int_time`. The new
        samples are copied from the original samples (not interpolated).

        Parameters
        ----------
        max_int_time : float
            Maximum integration time to upsample to in seconds.
        blt_order : str
            Major baseline ordering for output object. Default is "time". See
            the documentation on the `reorder_blts` method for more info.
        minor_order : str
            Minor baseline ordering for output object. Default is "baseline".
        summing_correlator_mode : bool
            Option to split the flux from the original samples into the new
            samples rather than duplicating the original samples in all the new
            samples (undoing an integration rather than an average) to emulate
            undoing the behavior in some correlators (e.g. HERA).
        allow_drift : bool
            Option to allow resampling of unprojected or driftscan data. If this is
            False, unprojected or driftscan data will be phased to the ra/dec of zenith
            before resampling and then unprojected or rephased to a driftscan after
            resampling. Note that resampling unprojected or driftscan phased data may
            result in unexpected behavior.

        Returns
        -------
        None

        """
        # check that max_int_time is sensible given integration_time
        min_integration_time = np.amin(self.integration_time)
        sensible_min = 1e-2 * min_integration_time
        if max_int_time < sensible_min:
            raise ValueError(
                "Decreasing the integration time by more than a "
                "factor of 100 is not supported. Also note that "
                "max_int_time should be in seconds."
            )

        # figure out where integration_time is longer than max_int_time
        inds_to_upsample = np.nonzero(
            (self.integration_time > max_int_time)
            & (
                ~np.isclose(
                    self.integration_time,
                    max_int_time,
                    rtol=self._integration_time.tols[0],
                    atol=self._integration_time.tols[1],
                )
            )
        )
        if len(inds_to_upsample[0]) == 0:
            warnings.warn(
                "All values in the integration_time array are already "
                "longer than the value specified; doing nothing."
            )
            return

        unprojected_blts = self._check_for_cat_type("unprojected")
        driftscan_blts = self._check_for_cat_type("driftscan")
        initial_driftscan = np.any(driftscan_blts)
        initial_unprojected = np.any(unprojected_blts)
        initial_nphase_ids = np.unique(self.phase_center_id_array).size
        if initial_driftscan:
            initial_phase_catalog = self.phase_center_catalog.copy()
            initial_ids = self.phase_center_id_array
        phased = False
        if initial_unprojected or initial_driftscan:
            if allow_drift:
                print(
                    "Data are unprojected or phased as a driftscan and allow_drift is "
                    "True, so resampling will be done without phasing."
                )
            else:
                phased = True
                # phase to RA/dec of zenith
                print(
                    "Data are unprojected or phased as a driftscan, phasing before "
                    "resampling."
                )
                phase_time = Time(self.time_array[0], format="jd")
                self.phase_to_time(phase_time)

        # we want the ceil of this, but we don't want to get the wrong answer
        # when the number is very close to an integer but just barely above it.
        temp_new_samples = self.integration_time[inds_to_upsample] / max_int_time
        mask_close_floor = np.isclose(temp_new_samples, np.floor(temp_new_samples))
        temp_new_samples[mask_close_floor] = np.floor(
            temp_new_samples[mask_close_floor]
        )

        n_new_samples = np.asarray(list(map(int, np.ceil(temp_new_samples))))

        temp_Nblts = np.sum(n_new_samples)

        temp_baseline = np.zeros((temp_Nblts,), dtype=np.int64)
        temp_id_array = np.zeros((temp_Nblts,), dtype=int)
        if initial_nphase_ids > 1 and initial_driftscan:
            temp_initial_ids = np.zeros((temp_Nblts,), dtype=int)
        else:
            temp_initial_ids = None
        if initial_nphase_ids > 1 and initial_unprojected:
            temp_unprojected_blts = np.zeros((temp_Nblts,), dtype=bool)
        else:
            temp_unprojected_blts = None
        temp_time = np.zeros((temp_Nblts,))
        temp_int_time = np.zeros((temp_Nblts,))
        if self.metadata_only:
            temp_data = None
            temp_flag = None
            temp_nsample = None
        else:
            if self.future_array_shapes:
                temp_data = np.zeros(
                    (temp_Nblts, self.Nfreqs, self.Npols), dtype=self.data_array.dtype
                )
                temp_flag = np.zeros(
                    (temp_Nblts, self.Nfreqs, self.Npols), dtype=self.flag_array.dtype
                )
                temp_nsample = np.zeros(
                    (temp_Nblts, self.Nfreqs, self.Npols),
                    dtype=self.nsample_array.dtype,
                )
            else:
                temp_data = np.zeros(
                    (temp_Nblts, 1, self.Nfreqs, self.Npols),
                    dtype=self.data_array.dtype,
                )
                temp_flag = np.zeros(
                    (temp_Nblts, 1, self.Nfreqs, self.Npols),
                    dtype=self.flag_array.dtype,
                )
                temp_nsample = np.zeros(
                    (temp_Nblts, 1, self.Nfreqs, self.Npols),
                    dtype=self.nsample_array.dtype,
                )

        i0 = 0
        for i, ind in enumerate(inds_to_upsample[0]):
            i1 = i0 + n_new_samples[i]
            temp_baseline[i0:i1] = self.baseline_array[ind]
            temp_id_array[i0:i1] = self.phase_center_id_array[ind]
            if initial_nphase_ids > 1:
                if initial_driftscan:
                    temp_initial_ids[i0:i1] = initial_ids[ind]
                if initial_unprojected:
                    temp_unprojected_blts[i0:i1] = unprojected_blts[ind]

            if not self.metadata_only:
                if summing_correlator_mode:
                    temp_data[i0:i1] = self.data_array[ind] / n_new_samples[i]
                else:
                    temp_data[i0:i1] = self.data_array[ind]
                temp_flag[i0:i1] = self.flag_array[ind]
                temp_nsample[i0:i1] = self.nsample_array[ind]

            # compute the new times of the upsampled array
            t0 = self.time_array[ind]
            dt = self.integration_time[ind] / n_new_samples[i]

            # `offset` will be 0.5 or 1, depending on whether n_new_samples for
            # this baseline is even or odd.
            offset = 0.5 + 0.5 * (n_new_samples[i] % 2)
            n2 = n_new_samples[i] // 2

            # Figure out the new center for sample ii taking offset into
            # account. Because `t0` is the central time for the original time
            # sample, `nt` will range from negative to positive so that
            # `temp_time` will result in the central time for the new samples.
            # `idx2` tells us how to far to shift and in what direction for each
            # new sample.
            for ii, idx in enumerate(range(i0, i1)):
                idx2 = ii + offset + n2 - n_new_samples[i]
                nt = ((t0 * units.day) + (dt * idx2 * units.s)).to(units.day).value
                temp_time[idx] = nt

            temp_int_time[i0:i1] = dt

            i0 = i1

        # harmonize temporary arrays with existing ones
        inds_to_keep = np.nonzero(self.integration_time <= max_int_time)
        self._harmonize_resample_arrays(
            inds_to_keep,
            temp_baseline,
            temp_id_array,
            temp_time,
            temp_int_time,
            temp_data,
            temp_flag,
            temp_nsample,
        )

        if phased:
            print("Undoing phasing.")
            if initial_unprojected:
                if initial_nphase_ids > 1:
                    select_mask = unprojected_blts[inds_to_keep]
                    select_mask = np.concatenate((select_mask, temp_unprojected_blts))
                else:
                    select_mask = None
                self.unproject_phase(select_mask=select_mask)
            if initial_driftscan:
                if initial_nphase_ids > 1:
                    initial_ids = initial_ids[inds_to_keep]
                    initial_ids = np.concatenate((initial_ids, temp_initial_ids))
                for cat_id, cat_dict in initial_phase_catalog.items():
                    if cat_dict["cat_type"] != "driftscan":
                        continue
                    if initial_nphase_ids > 1:
                        select_mask = initial_ids == cat_id
                        if not np.any(select_mask):
                            select_mask = None
                    else:
                        select_mask = None
                    self.phase(
                        cat_dict["cat_lon"],
                        cat_dict["cat_lat"],
                        cat_name=cat_dict["cat_name"],
                        cat_type=cat_dict["cat_type"],
                        phase_frame=cat_dict["cat_frame"],
                        epoch=cat_dict["cat_epoch"],
                        select_mask=select_mask,
                    )

        # reorganize along blt axis
        self.reorder_blts(order=blt_order, minor_order=minor_order)

        # check the resulting object
        self.check()

        # add to the history
        history_update_string = (
            " Upsampled data to {:f} second integration time "
            "using pyuvdata.".format(max_int_time)
        )
        self.history = self.history + history_update_string

        return

    def downsample_in_time(
        self,
        min_int_time=None,
        n_times_to_avg=None,
        blt_order="time",
        minor_order="baseline",
        keep_ragged=True,
        summing_correlator_mode=False,
        allow_drift=False,
    ):
        """
        Average to a longer integration time.

        This method will average a UVData object either by an integer factor
        (by setting `n_times_to_avg`) or by a factor that can differ by
        baseline-time sample such that after averaging, the samples have an
        integration time greater than or equal to the `min_int_time` (up to the
        tolerance on the integration_time).

        Note that if the integrations for a baseline do not divide evenly by the
        `n_times_to_avg` or into the specified `min_int_time`, the final
        integrations for that baseline may have integration times less than
        `min_int_time` or be composed of fewer input integrations than `n_times_to_avg`.
        This behavior can be controlled with the `keep_ragged` argument.
        The new samples are averages of the original samples (not interpolations).

        Parameters
        ----------
        min_int_time : float
            Minimum integration time to average the UVData integration_time to
            in seconds.
        n_times_to_avg : int
            Number of time integrations to average together.
        blt_order : str
            Major baseline ordering for output object. Default is "time". See the
            documentation on the `reorder_blts` method for more details.
        minor_order : str
            Minor baseline ordering for output object. Default is "baseline".
        keep_ragged : bool
            When averaging baselines that do not evenly divide into min_int_time,
            or that have a number of integrations that do not evenly divide by
            n_times_to_avg, keep_ragged controls whether to keep the (averaged)
            integrations corresponding to the remaining samples (keep_ragged=True),
            or discard them (keep_ragged=False).
        summing_correlator_mode : bool
            Option to integrate the flux from the original samples rather than
            average the flux to emulate the behavior in some correlators (e.g. HERA).
        allow_drift : bool
            Option to allow resampling of unprojected or driftscan data. If this is
            False, unprojected or driftscan data will be phased to the ra/dec of zenith
            before resampling and then unprojected or rephased to a driftscan after
            resampling. Note that resampling unprojected or driftscan phased data may
            result in unexpected behavior.

        Returns
        -------
        None

        Raises
        ------
        ValueError
            If neither or both of `min_int_time` and `n_times_to_avg` are set.
            If there's only one time on the object.
            If `min_int_time` is more than 100 time the max integration time.
            If `n_times_to_avg` is not an integer.

        """
        if min_int_time is None and n_times_to_avg is None:
            raise ValueError("Either min_int_time or n_times_to_avg must be set.")

        if min_int_time is not None and n_times_to_avg is not None:
            raise ValueError("Only one of min_int_time or n_times_to_avg can be set.")

        if self.Ntimes == 1:
            raise ValueError("Only one time in this object, cannot downsample.")

        if min_int_time is not None:
            # check that min_int_time is sensible given integration_time
            max_integration_time = np.amax(self.integration_time)
            sensible_max = 1e2 * max_integration_time
            if min_int_time > sensible_max:
                raise ValueError(
                    "Increasing the integration time by more than a "
                    "factor of 100 is not supported. Also note that "
                    "min_int_time should be in seconds."
                )

            # first figure out where integration_time is shorter than min_int_time
            inds_to_downsample = np.nonzero(
                (self.integration_time < min_int_time)
                & (
                    ~np.isclose(
                        self.integration_time,
                        min_int_time,
                        rtol=self._integration_time.tols[0],
                        atol=self._integration_time.tols[1],
                    )
                )
            )

            if len(inds_to_downsample[0]) == 0:
                warnings.warn(
                    "All values in the integration_time array are already "
                    "longer than the value specified; doing nothing."
                )
                return
        else:
            if not isinstance(n_times_to_avg, (int, np.integer)):
                raise ValueError("n_times_to_avg must be an integer.")
        # If we're going to do actual work, reorder the baselines to ensure time is
        # monotonically increasing.
        # Default of reorder_blts is baseline major, time minor, which is what we want.
        self.reorder_blts()

        if min_int_time is not None:
            # now re-compute inds_to_downsample, in case things have changed
            inds_to_downsample = np.nonzero(
                (self.integration_time < min_int_time)
                & ~np.isclose(
                    self.integration_time,
                    min_int_time,
                    rtol=self._integration_time.tols[0],
                    atol=self._integration_time.tols[1],
                )
            )
            bls_to_downsample = np.unique(self.baseline_array[inds_to_downsample])
        else:
            bls_to_downsample = np.unique(self.baseline_array)

        # figure out how many baseline times we'll end up with at the end
        n_new_samples = 0
        for bl in bls_to_downsample:
            bl_inds = np.nonzero(self.baseline_array == bl)[0]
            int_times = self.integration_time[bl_inds]

            if min_int_time is not None:
                running_int_time = 0.0
                for itime, int_time in enumerate(int_times):
                    running_int_time += int_time
                    over_min_int_time = running_int_time > min_int_time or np.isclose(
                        running_int_time,
                        min_int_time,
                        rtol=self._integration_time.tols[0],
                        atol=self._integration_time.tols[1],
                    )
                    last_sample = itime == len(bl_inds) - 1
                    # We sum up all the samples found so far if we're over the
                    # target minimum time, or we've hit the end of the time
                    # samples for this baseline.
                    if over_min_int_time or last_sample:
                        if last_sample and not (over_min_int_time or keep_ragged):
                            # don't do anything -- implicitly drop these integrations
                            continue
                        n_new_samples += 1
                        running_int_time = 0.0
            else:
                n_bl_times = self.time_array[bl_inds].size
                nsample_temp = np.sum(n_bl_times / n_times_to_avg)
                if keep_ragged and not np.isclose(nsample_temp, np.floor(nsample_temp)):
                    n_new_samples += np.ceil(nsample_temp).astype(int)
                else:
                    n_new_samples += np.floor(nsample_temp).astype(int)

            # figure out if there are any time gaps in the data
            # meaning that the time differences are larger than the integration times
            # time_array is in JD, need to convert to seconds for the diff
            dtime = np.ediff1d(self.time_array[bl_inds]) * 24 * 3600
            int_times = int_times
            if len(np.unique(int_times)) == 1:
                # this baseline has all the same integration times
                if len(np.unique(dtime)) > 1 and not uvutils._test_array_constant(
                    dtime, self._integration_time.tols
                ):
                    warnings.warn(
                        "There is a gap in the times of baseline {bl}. "
                        "The output may include averages across long "
                        "time gaps.".format(bl=self.baseline_to_antnums(bl))
                    )
                elif not np.isclose(
                    dtime[0],
                    int_times[0],
                    rtol=self._integration_time.tols[0],
                    atol=self._integration_time.tols[1],
                ):
                    warnings.warn(
                        "The time difference between integrations is "
                        "not the same as the integration time for "
                        "baseline {bl}. The output may average across "
                        "longer time intervals than "
                        "expected".format(bl=self.baseline_to_antnums(bl))
                    )

            else:
                # varying integration times for this baseline, need to be more careful
                expected_dtimes = (int_times[:-1] + int_times[1:]) / 2
                wh_diff = np.nonzero(~np.isclose(dtime, expected_dtimes))
                if wh_diff[0].size > 1:
                    warnings.warn(
                        "The time difference between integrations is "
                        "different than the expected given the "
                        "integration times for baseline {bl}. The "
                        "output may include averages across long time "
                        "gaps.".format(bl=self.baseline_to_antnums(bl))
                    )

        temp_Nblts = n_new_samples

        unprojected_blts = self._check_for_cat_type("unprojected")
        driftscan_blts = self._check_for_cat_type("driftscan")
        initial_driftscan = np.any(driftscan_blts)
        initial_unprojected = np.any(unprojected_blts)
        initial_nphase_ids = np.unique(self.phase_center_id_array).size
        if initial_driftscan:
            initial_phase_catalog = self.phase_center_catalog.copy()
            initial_ids = self.phase_center_id_array
        phased = False
        if initial_unprojected or initial_driftscan:
            if allow_drift:
                print(
                    "Data are unprojected or phased as a driftscan and allow_drift is "
                    "True, so resampling will be done without phasing."
                )
            else:
                phased = True
                # phase to RA/dec of zenith
                print(
                    "Data are unprojected or phased as a driftscan, phasing before "
                    "resampling."
                )
                phase_time = Time(self.time_array[0], format="jd")
                self.phase_to_time(phase_time)

        # make temporary arrays
        temp_baseline = np.zeros((temp_Nblts,), dtype=np.int64)
        temp_id_array = np.zeros((temp_Nblts,), dtype=int)
        temp_time = np.zeros((temp_Nblts,))
        temp_int_time = np.zeros((temp_Nblts,))
        if initial_nphase_ids > 1 and initial_driftscan:
            temp_initial_ids = np.zeros((temp_Nblts,), dtype=int)
        else:
            temp_initial_ids = None
        if initial_nphase_ids > 1 and initial_unprojected:
            temp_unprojected_blts = np.zeros((temp_Nblts,), dtype=bool)
        else:
            temp_unprojected_blts = None
        if self.metadata_only:
            temp_data = None
            temp_flag = None
            temp_nsample = None
        else:
            if self.future_array_shapes:
                temp_data = np.zeros(
                    (temp_Nblts, self.Nfreqs, self.Npols), dtype=self.data_array.dtype
                )
                temp_flag = np.zeros(
                    (temp_Nblts, self.Nfreqs, self.Npols), dtype=self.flag_array.dtype
                )
                temp_nsample = np.zeros(
                    (temp_Nblts, self.Nfreqs, self.Npols),
                    dtype=self.nsample_array.dtype,
                )
            else:
                temp_data = np.zeros(
                    (temp_Nblts, 1, self.Nfreqs, self.Npols),
                    dtype=self.data_array.dtype,
                )
                temp_flag = np.zeros(
                    (temp_Nblts, 1, self.Nfreqs, self.Npols),
                    dtype=self.flag_array.dtype,
                )
                temp_nsample = np.zeros(
                    (temp_Nblts, 1, self.Nfreqs, self.Npols),
                    dtype=self.nsample_array.dtype,
                )

        temp_idx = 0
        for bl in bls_to_downsample:
            bl_inds = np.nonzero(self.baseline_array == bl)[0]
            running_int_time = 0.0
            summing_idx = 0
            n_sum = 0
            for itime, int_time in enumerate(self.integration_time[bl_inds]):
                running_int_time += int_time
                n_sum += 1
                if min_int_time is not None:
                    over_min_int_time = running_int_time > min_int_time or np.isclose(
                        running_int_time,
                        min_int_time,
                        rtol=self._integration_time.tols[0],
                        atol=self._integration_time.tols[1],
                    )
                else:
                    over_min_int_time = n_sum >= n_times_to_avg
                last_sample = itime == len(bl_inds) - 1
                # We sum up all the samples found so far if we're over the
                # target minimum time, or we've hit the end of the time
                # samples for this baseline.
                if over_min_int_time or last_sample:
                    if last_sample and not (over_min_int_time or keep_ragged):
                        # don't do anything -- implicitly drop these integrations
                        continue
                    # sum together that number of samples
                    temp_baseline[temp_idx] = bl
                    # this might be wrong if some of the constituent times are
                    # *totally* flagged
                    averaging_idx = bl_inds[summing_idx : summing_idx + n_sum]
                    if self.Nphase > 1:
                        unique_phase_centers = np.unique(
                            self.phase_center_id_array[averaging_idx]
                        )
                        if unique_phase_centers.size > 1:
                            raise ValueError(
                                "Multiple phase centers included in a downsampling "
                                "window. Use `phase` to phase to a single phase center "
                                "or decrease the `min_int_time` or `n_times_to_avg` "
                                "parameter to avoid multiple phase centers being "
                                "included in a downsampling window."
                            )
                    temp_id_array[temp_idx] = self.phase_center_id_array[
                        averaging_idx[0]
                    ]
                    if initial_nphase_ids > 1:
                        if initial_unprojected:
                            unique_unprojected = np.unique(
                                unprojected_blts[averaging_idx]
                            )
                            temp_unprojected_blts[temp_idx] = unique_unprojected[0]
                        if initial_driftscan:
                            unique_initial_ids = np.unique(initial_ids[averaging_idx])
                            temp_initial_ids[temp_idx] = unique_initial_ids[0]
                    # take potential non-uniformity of integration_time into account
                    temp_time[temp_idx] = np.sum(
                        self.time_array[averaging_idx]
                        * self.integration_time[averaging_idx]
                    ) / np.sum(self.integration_time[averaging_idx])
                    temp_int_time[temp_idx] = running_int_time
                    if not self.metadata_only:
                        # if all inputs are flagged, the flag array should be True,
                        # otherwise it should be False.
                        # The sum below will be zero if it's all flagged and
                        # greater than zero otherwise
                        # Then we use a test against 0 to turn it into a Boolean
                        temp_flag[temp_idx] = (
                            np.sum(~self.flag_array[averaging_idx], axis=0) == 0
                        )

                        mask = self.flag_array[averaging_idx]
                        # need to update mask if a downsampled visibility will
                        # be flagged so that we don't set it to zero
                        if (temp_flag[temp_idx]).any():
                            if self.future_array_shapes:
                                ax1_inds, ax2_inds = np.nonzero(temp_flag[temp_idx])
                                mask[:, ax1_inds, ax2_inds] = False
                            else:
                                ax1_inds, ax2_inds, ax3_inds = np.nonzero(
                                    temp_flag[temp_idx]
                                )
                                mask[:, ax1_inds, ax2_inds, ax3_inds] = False

                        masked_data = np.ma.masked_array(
                            self.data_array[averaging_idx], mask=mask
                        )

                        # nsample array is the fraction of data that we actually kept,
                        # relative to the amount that went into the sum or average
                        nsample_dtype = self.nsample_array.dtype.type
                        # promote nsample dtype if half-precision
                        if nsample_dtype is np.float16:
                            masked_nsample_dtype = np.float32
                        else:
                            masked_nsample_dtype = nsample_dtype
                        masked_nsample = np.ma.masked_array(
                            self.nsample_array[averaging_idx],
                            mask=mask,
                            dtype=masked_nsample_dtype,
                        )

                        if self.future_array_shapes:
                            int_time_arr = self.integration_time[
                                averaging_idx, np.newaxis, np.newaxis
                            ]
                        else:
                            int_time_arr = self.integration_time[
                                averaging_idx, np.newaxis, np.newaxis, np.newaxis
                            ]
                        masked_int_time = np.ma.masked_array(
                            np.ones_like(
                                self.data_array[averaging_idx],
                                dtype=self.integration_time.dtype,
                            )
                            * int_time_arr,
                            mask=mask,
                        )
                        if summing_correlator_mode:
                            temp_data[temp_idx] = np.sum(masked_data, axis=0)
                        else:
                            # take potential non-uniformity of integration_time
                            # and nsamples into account
                            weights = masked_nsample * masked_int_time
                            weighted_data = masked_data * weights
                            temp_data[temp_idx] = np.sum(
                                weighted_data, axis=0
                            ) / np.sum(weights, axis=0)

                        # output of masked array calculation should be coerced
                        # to the datatype of temp_nsample (which has the same
                        # precision as the original nsample_array)
                        temp_nsample[temp_idx] = np.sum(
                            masked_nsample * masked_int_time, axis=0
                        ) / np.sum(self.integration_time[averaging_idx])
                    # increment counters and reset values
                    temp_idx += 1
                    summing_idx += n_sum
                    running_int_time = 0.0
                    n_sum = 0

        # make sure we've populated the right number of baseline-times
        assert temp_idx == temp_Nblts, (
            "Wrong number of baselines. Got {:d},  expected {:d}. This is a bug, "
            "please make an issue at https://github.com/RadioAstronomySoftwareGroup/"
            "pyuvdata/issues".format(temp_idx, temp_Nblts)
        )

        # harmonize temporary arrays with existing ones
        if min_int_time is not None:
            bls_not_downsampled = set(self.baseline_array) - set(bls_to_downsample)
            inds_to_keep = []
            for bl in bls_not_downsampled:
                inds_to_keep += np.nonzero(self.baseline_array == bl)[0].tolist()
            inds_to_keep = np.array(inds_to_keep, dtype=np.int64)
        else:
            inds_to_keep = np.array([], dtype=bool)
        self._harmonize_resample_arrays(
            inds_to_keep,
            temp_baseline,
            temp_id_array,
            temp_time,
            temp_int_time,
            temp_data,
            temp_flag,
            temp_nsample,
        )

        if phased:
            print("Undoing phasing.")
            if initial_unprojected:
                if initial_nphase_ids > 1:
                    select_mask = unprojected_blts[inds_to_keep]
                    select_mask = np.concatenate((select_mask, temp_unprojected_blts))
                else:
                    select_mask = None
                self.unproject_phase(select_mask=select_mask)
            if initial_driftscan:
                if initial_nphase_ids > 1:
                    initial_ids = initial_ids[inds_to_keep]
                    initial_ids = np.concatenate((initial_ids, temp_initial_ids))
                for cat_id, cat_dict in initial_phase_catalog.items():
                    if cat_dict["cat_type"] != "driftscan":
                        continue
                    if initial_nphase_ids > 1:
                        select_mask = initial_ids == cat_id
                        if not np.any(select_mask):
                            select_mask = None
                    else:
                        select_mask = None
                    self.phase(
                        cat_dict["cat_lon"],
                        cat_dict["cat_lat"],
                        cat_name=cat_dict["cat_name"],
                        cat_type=cat_dict["cat_type"],
                        phase_frame=cat_dict["cat_frame"],
                        epoch=cat_dict["cat_epoch"],
                        select_mask=select_mask,
                    )

        # reorganize along blt axis
        self.reorder_blts(order=blt_order, minor_order=minor_order)

        # check the resulting object
        self.check()

        # add to the history
        if min_int_time is not None:
            history_update_string = (
                " Downsampled data to {:f} second integration "
                "time using pyuvdata.".format(min_int_time)
            )
        else:
            history_update_string = (
                " Downsampled data by a factor of {} in "
                "time using pyuvdata.".format(n_times_to_avg)
            )
        self.history = self.history + history_update_string

        return

    def resample_in_time(
        self,
        target_time,
        only_downsample=False,
        only_upsample=False,
        blt_order="time",
        minor_order="baseline",
        keep_ragged=True,
        summing_correlator_mode=False,
        allow_drift=False,
    ):
        """
        Intelligently upsample or downsample a UVData object to the target time.

        Parameters
        ----------
        target_time : float
            The target integration time to resample to, in seconds.
        only_downsample : bool
            Option to only call bda_downsample.
        only_upsample : bool
            Option to only call bda_upsample.
        blt_order : str
            Major baseline ordering for output object. Default is "time". See the
            documentation on the `reorder_blts` method for more details.
        minor_order : str
            Minor baseline ordering for output object. Default is "baseline".
        keep_ragged : bool
            When averaging baselines that do not evenly divide into min_int_time,
            keep_ragged controls whether to keep the (summed) integrations
            corresponding to the remaining samples (keep_ragged=True), or
            discard them (keep_ragged=False). Note this option only applies to the
            `bda_downsample` method.
        summing_correlator_mode : bool
            Option to integrate or split the flux from the original samples
            rather than average or duplicate the flux from the original samples
            to emulate the behavior in some correlators (e.g. HERA).
        allow_drift : bool
            Option to allow resampling of unprojected or driftscan data. If this is
            False, unprojected or driftscan data will be phased to the ra/dec of zenith
            before resampling and then unprojected or rephased to a driftscan after
            resampling. Note that resampling unprojected or driftscan phased data may
            result in unexpected behavior.

        Returns
        -------
        None
        """
        # figure out integration times relative to target time
        min_int_time = np.amin(self.integration_time)
        max_int_time = np.amax(self.integration_time)

        if int(np.floor(target_time / min_int_time)) >= 2 and not only_upsample:
            downsample = True
        else:
            downsample = False

        if int(np.floor(max_int_time / target_time)) >= 2 and not only_downsample:
            upsample = True
        else:
            upsample = False

        if not downsample and not upsample:
            warnings.warn(
                "No resampling will be done because target time is not "
                "a factor of 2 or more off from integration_time. To "
                "force resampling set only_upsample or only_downsample "
                "keywords or call upsample_in_time or downsample_in_time."
            )
            return

        if downsample:
            self.downsample_in_time(
                target_time,
                blt_order=blt_order,
                minor_order=minor_order,
                keep_ragged=keep_ragged,
                summing_correlator_mode=summing_correlator_mode,
                allow_drift=allow_drift,
            )
        if upsample:
            self.upsample_in_time(
                target_time,
                blt_order=blt_order,
                minor_order=minor_order,
                summing_correlator_mode=summing_correlator_mode,
                allow_drift=allow_drift,
            )

        return

    def frequency_average(
        self, n_chan_to_avg, summing_correlator_mode=False, propagate_flags=False
    ):
        """
        Average in frequency.

        Does a simple average over an integer number of input channels, leaving
        flagged samples out of the average.

        In the future, this method will support non-equally spaced channels
        and varying channel widths. It will also support setting the frequency
        to the true mean of the averaged non-flagged frequencies rather than
        the simple mean of the input channel frequencies. For now it does not.

        Parameters
        ----------
        n_chan_to_avg : int
            Number of channels to average together. If Nfreqs does not divide
            evenly by this number, the frequencies at the end of the freq_array
            will be dropped to make it evenly divisable. To control which
            frequencies are removed, use select before calling this method.
        summing_correlator_mode : bool
            Option to integrate or split the flux from the original samples
            rather than average or duplicate the flux from the original samples
            to emulate the behavior in some correlators (e.g. HERA).
        propagate_flags: bool
            Option to flag an averaged entry even if some of its contributors
            are not flagged. The averaged result will still leave the flagged
            samples out of the average, except when all contributors are
            flagged.
        """
        if self.Nspws > 1:
            raise NotImplementedError(
                "Frequency averaging not (yet) available for multiple spectral windows"
            )

        spacing_error, chanwidth_error = self._check_freq_spacing(raise_errors=False)
        if spacing_error:
            raise NotImplementedError(
                "Frequency averaging not (yet) available for unevenly spaced "
                "frequencies or varying channel widths."
            )
        elif chanwidth_error:
            warnings.warn(
                "The frequency spacing is even but not equal to the channel width, so "
                "after averaging the channel_width will also not match the frequency "
                "spacing."
            )

        n_final_chan = int(np.floor(self.Nfreqs / n_chan_to_avg))
        nfreq_mod_navg = self.Nfreqs % n_chan_to_avg
        if nfreq_mod_navg != 0:
            # not an even number of final channels
            warnings.warn(
                "Nfreqs does not divide by `n_chan_to_avg` evenly. "
                "The final {} frequencies will be excluded, to "
                "control which frequencies to exclude, use a "
                "select to control.".format(nfreq_mod_navg)
            )
            chan_to_keep = np.arange(n_final_chan * n_chan_to_avg)
            self.select(freq_chans=chan_to_keep)

        if self.future_array_shapes:
            self.freq_array = self.freq_array.reshape(
                (n_final_chan, n_chan_to_avg)
            ).mean(axis=1)
            self.channel_width = self.channel_width.reshape(
                (n_final_chan, n_chan_to_avg)
            ).sum(axis=1)
        else:
            self.freq_array = self.freq_array.reshape(
                (1, n_final_chan, n_chan_to_avg)
            ).mean(axis=2)
            self.channel_width = self.channel_width * n_chan_to_avg
        self.Nfreqs = n_final_chan
        if self.flex_spw_id_array is not None:
            self.flex_spw_id_array = self.flex_spw_id_array.reshape(
                (n_final_chan, n_chan_to_avg)
            ).mean(axis=1)

        if self.eq_coeffs is not None:
            eq_coeff_diff = np.diff(self.eq_coeffs, axis=1)
            if np.abs(np.max(eq_coeff_diff)) > 0:
                warnings.warn(
                    "eq_coeffs vary by frequency. They should be "
                    "applied to the data using `remove_eq_coeffs` "
                    "before frequency averaging."
                )
            self.eq_coeffs = self.eq_coeffs.reshape(
                (self.Nants_telescope, n_final_chan, n_chan_to_avg)
            ).mean(axis=2)

        if not self.metadata_only:
            if self.future_array_shapes:
                shape_tuple = (self.Nblts, n_final_chan, n_chan_to_avg, self.Npols)
            else:
                shape_tuple = (self.Nblts, 1, n_final_chan, n_chan_to_avg, self.Npols)

            mask = self.flag_array.reshape(shape_tuple)

            if propagate_flags:
                # if any contributors are flagged, the result should be flagged
                if self.future_array_shapes:
                    self.flag_array = np.any(
                        self.flag_array.reshape(shape_tuple), axis=2
                    )
                else:
                    self.flag_array = np.any(
                        self.flag_array.reshape(shape_tuple), axis=3
                    )
            else:
                # if all inputs are flagged, the flag array should be True,
                # otherwise it should be False.
                # The sum below will be zero if it's all flagged and
                # greater than zero otherwise
                # Then we use a test against 0 to turn it into a Boolean
                if self.future_array_shapes:
                    self.flag_array = (
                        np.sum(~self.flag_array.reshape(shape_tuple), axis=2) == 0
                    )
                else:
                    self.flag_array = (
                        np.sum(~self.flag_array.reshape(shape_tuple), axis=3) == 0
                    )

            # need to update mask if a downsampled visibility will be flagged
            # so that we don't set it to zero
            for n_chan in np.arange(n_final_chan):
                if self.future_array_shapes:
                    if (self.flag_array[:, n_chan]).any():
                        ax0_inds, ax2_inds = np.nonzero(self.flag_array[:, n_chan, :])
                        # Only if all entries are masked
                        # May not happen due to propagate_flags keyword
                        # mask should be left alone otherwise
                        if np.all(mask[ax0_inds, n_chan, :, ax2_inds]):
                            mask[ax0_inds, n_chan, :, ax2_inds] = False
                else:
                    if (self.flag_array[:, :, n_chan]).any():
                        ax0_inds, ax1_inds, ax3_inds = np.nonzero(
                            self.flag_array[:, :, n_chan, :]
                        )
                        # Only if all entries are masked
                        # May not happen due to propagate_flags keyword
                        # mask should be left alone otherwise
                        if np.all(mask[ax0_inds, ax1_inds, n_chan, :, ax3_inds]):
                            mask[ax0_inds, ax1_inds, n_chan, :, ax3_inds] = False

            masked_data = np.ma.masked_array(
                self.data_array.reshape(shape_tuple), mask=mask
            )

            self.nsample_array = self.nsample_array.reshape(shape_tuple)
            # promote nsample dtype if half-precision
            nsample_dtype = self.nsample_array.dtype.type
            if nsample_dtype is np.float16:
                masked_nsample_dtype = np.float32
            else:
                masked_nsample_dtype = nsample_dtype
            masked_nsample = np.ma.masked_array(
                self.nsample_array, mask=mask, dtype=masked_nsample_dtype
            )

            if summing_correlator_mode:
                if self.future_array_shapes:
                    self.data_array = np.sum(masked_data, axis=2).data
                else:
                    self.data_array = np.sum(masked_data, axis=3).data
            else:
                # need to weight by the nsample_array
                if self.future_array_shapes:
                    self.data_array = (
                        np.sum(masked_data * masked_nsample, axis=2)
                        / np.sum(masked_nsample, axis=2)
                    ).data
                else:
                    self.data_array = (
                        np.sum(masked_data * masked_nsample, axis=3)
                        / np.sum(masked_nsample, axis=3)
                    ).data

            # nsample array is the fraction of data that we actually kept,
            # relative to the amount that went into the sum or average.
            # Need to take care to return precision back to original value.
            if self.future_array_shapes:
                self.nsample_array = (
                    np.sum(masked_nsample, axis=2) / float(n_chan_to_avg)
                ).data.astype(nsample_dtype)
            else:
                self.nsample_array = (
                    np.sum(masked_nsample, axis=3) / float(n_chan_to_avg)
                ).data.astype(nsample_dtype)

    def get_redundancies(
        self,
        tol=1.0,
        use_antpos=False,
        include_conjugates=False,
        include_autos=True,
        conjugate_bls=False,
    ):
        """
        Get redundant baselines to a given tolerance.

        This can be used to identify redundant baselines present in the data,
        or find all possible redundant baselines given the antenna positions.

        Parameters
        ----------
        tol : float
            Redundancy tolerance in meters (default 1m).
        use_antpos : bool
            Use antenna positions to find all possible redundant groups for this
            telescope (default False).
            The returned baselines are in the 'u>0' convention.
        include_conjugates : bool
            Option to include baselines that are redundant under conjugation.
            Only used if use_antpos is False.
        include_autos : bool
            Option to include autocorrelations in the full redundancy list.
            Only used if use_antpos is True.
        conjugate_bls : bool
            If using antenna positions, this will conjugate baselines on this
            object to correspond with those in the returned groups.

        Returns
        -------
        baseline_groups : list of lists of int
            List of lists of redundant baseline numbers
        vec_bin_centers : list of ndarray of float
            List of vectors describing redundant group uvw centers
        lengths : list of float
            List of redundant group baseline lengths in meters
        conjugates : list of int, or None, optional
            List of indices for baselines that must be conjugated to fit into their
            redundant groups.
            Will return None if use_antpos is True and include_conjugates is True
            Only returned if include_conjugates is True

        Notes
        -----
        If use_antpos is set, then this function will find all redundant baseline groups
        for this telescope, under the u>0 antenna ordering convention.
        If use_antpos is not set, this function will look for redundant groups
        in the data.

        """
        if use_antpos:
            antpos, numbers = self.get_ENU_antpos(center=False)
            result = uvutils.get_antenna_redundancies(
                numbers, antpos, tol=tol, include_autos=include_autos
            )
            if conjugate_bls:
                self.conjugate_bls(convention="u>0", uvw_tol=tol)

            if include_conjugates:
                result = result + (None,)
            return result

        _, unique_inds = np.unique(self.baseline_array, return_index=True)
        unique_inds.sort()
        baseline_vecs = np.take(self.uvw_array, unique_inds, axis=0)
        baselines = np.take(self.baseline_array, unique_inds)

        return uvutils.get_baseline_redundancies(
            baselines, baseline_vecs, tol=tol, include_conjugates=include_conjugates
        )

    def compress_by_redundancy(
        self, method="select", tol=1.0, inplace=True, keep_all_metadata=True
    ):
        """
        Downselect or average to only have one baseline per redundant group.

        Either select the first baseline in the redundant group or average over
        the baselines in the redundant group. When averaging, only unflagged data are
        averaged and the nsample_array reflects the amount of unflagged data that was
        averaged over. In the case that all the data for a particular visibility to be
        averaged is flagged, all the flagged data is averaged (with an nsample value
        that represents all the data) but the flag array is set to True for that
        visibility.

        Parameters
        ----------
        tol : float
            Redundancy tolerance in meters, default is 1.0 corresponding to 1 meter.
            This specifies what tolerance to use when identifying baselines as
            redundant.
        method : str
            Options are "select", which just keeps the first baseline in each
            redundant group or "average" which averages over the baselines in each
            redundant group and assigns the average to the first baseline in the group.
        inplace : bool
            Option to do selection on current object.
        keep_all_metadata : bool
            Option to keep all the metadata associated with antennas,
            even those that do not remain after the select option.

        Returns
        -------
        UVData object or None
            if inplace is False, return the compressed UVData object

        """
        allowed_methods = ["select", "average"]
        if method not in allowed_methods:
            raise ValueError(f"method must be one of {allowed_methods}")

        red_gps, centers, lengths, conjugates = self.get_redundancies(
            tol, include_conjugates=True
        )
        bl_ants = [self.baseline_to_antnums(gp[0]) for gp in red_gps]

        if method == "average":
            # do a metadata only select to get all the metadata right
            new_obj = self.copy(metadata_only=True)
            new_obj.select(bls=bl_ants, keep_all_metadata=keep_all_metadata)

            if not self.metadata_only:
                # If the baseline is in the "conjugated" list, we will need to
                # fix the conjugation on assignment (since the visibilities are
                # tabulated assuming that the baseline position is on the opposite
                # side of the uvw origin).
                fix_conj = np.isin(new_obj.baseline_array, conjugates)

                # initalize the data like arrays
                if new_obj.future_array_shapes:
                    temp_data_array = np.zeros(
                        (new_obj.Nblts, new_obj.Nfreqs, new_obj.Npols),
                        dtype=self.data_array.dtype,
                    )
                    temp_nsample_array = np.zeros(
                        (new_obj.Nblts, new_obj.Nfreqs, new_obj.Npols),
                        dtype=self.nsample_array.dtype,
                    )
                    temp_flag_array = np.zeros(
                        (new_obj.Nblts, new_obj.Nfreqs, new_obj.Npols),
                        dtype=self.flag_array.dtype,
                    )
                else:
                    temp_data_array = np.zeros(
                        (new_obj.Nblts, 1, new_obj.Nfreqs, new_obj.Npols),
                        dtype=self.data_array.dtype,
                    )
                    temp_nsample_array = np.zeros(
                        (new_obj.Nblts, 1, new_obj.Nfreqs, new_obj.Npols),
                        dtype=self.nsample_array.dtype,
                    )
                    temp_flag_array = np.zeros(
                        (new_obj.Nblts, 1, new_obj.Nfreqs, new_obj.Npols),
                        dtype=self.flag_array.dtype,
                    )
            for grp_ind, group in enumerate(red_gps):
                if len(conjugates) > 0:
                    conj_group = set(group).intersection(conjugates)
                    reg_group = list(set(group) - conj_group)
                    conj_group = list(conj_group)
                else:
                    reg_group = group
                    conj_group = []
                group_times = []
                group_inds = []
                conj_group_inds = []
                conj_group_times = []
                for bl in reg_group:
                    bl_inds = np.where(self.baseline_array == bl)[0]
                    group_inds.extend(bl_inds)
                    group_times.extend(self.time_array[bl_inds])
                for bl in conj_group:
                    bl_inds = np.where(self.baseline_array == bl)[0]
                    conj_group_inds.extend(bl_inds)
                    conj_group_times.extend(self.time_array[bl_inds])

                group_inds = np.array(group_inds, dtype=np.int64)
                conj_group_inds = np.array(conj_group_inds, dtype=np.int64)
                # now we have to figure out which times are the same to a tolerance
                # so we can average over them.
                time_inds = np.arange(len(group_times + conj_group_times))
                time_gps = uvutils.find_clusters(
                    time_inds,
                    np.array(group_times + conj_group_times),
                    self._time_array.tols[1],
                )

                # average over the same times
                obj_bl = bl_ants[grp_ind]
                obj_inds = new_obj._key2inds(obj_bl)[0]
                obj_times = new_obj.time_array[obj_inds]

                for gp in time_gps:
                    # Note that this average time is just used for identifying the
                    # index to use for the blt axis on the averaged data set.
                    # We do not update the actual time on that data set because it can
                    # result in confusing behavior -- small numerical rounding errors
                    # can result in many more unique times in the final data set than
                    # in the initial data set.
                    avg_time = np.average(np.array(group_times + conj_group_times)[gp])

                    obj_time_ind = np.where(
                        np.abs(obj_times - avg_time) < self._time_array.tols[1]
                    )[0]

                    if obj_time_ind.size == 1:
                        this_obj_ind = obj_inds[obj_time_ind[0]]
                    else:
                        warnings.warn(
                            "Index baseline in the redundant group does not "
                            "have all the times, compressed object will be "
                            "missing those times."
                        )
                        continue

                    # time_ind contains indices for both regular and conjugated bls
                    # because we needed to group them together in time.
                    # The regular ones are first and extend the length of group_times,
                    # so we use that to split them back up.
                    regular_orientation = np.array(
                        [time_ind for time_ind in gp if time_ind < len(group_times)],
                        dtype=np.int64,
                    )
                    regular_inds = group_inds[np.array(regular_orientation)]
                    conj_orientation = np.array(
                        [
                            time_ind - len(group_times)
                            for time_ind in gp
                            if time_ind >= len(group_times)
                        ],
                        dtype=np.int64,
                    )
                    conj_inds = conj_group_inds[np.array(conj_orientation)]
                    # check that the integration times are all the same
                    int_times = np.concatenate(
                        (
                            self.integration_time[regular_inds],
                            self.integration_time[conj_inds],
                        )
                    )
                    if not np.all(
                        np.abs(int_times - new_obj.integration_time[obj_time_ind])
                        < new_obj._integration_time.tols[1]
                    ):
                        warnings.warn(
                            "Integrations times are not identical in a redundant "
                            "group. Averaging anyway but this may cause unexpected "
                            "behavior."
                        )

                    if not self.metadata_only:
                        vis_to_avg = np.concatenate(
                            (
                                self.data_array[regular_inds],
                                np.conj(self.data_array[conj_inds]),
                            )
                        )
                        nsample_to_avg = np.concatenate(
                            (
                                self.nsample_array[regular_inds],
                                self.nsample_array[conj_inds],
                            )
                        )
                        flags_to_avg = np.concatenate(
                            (self.flag_array[regular_inds], self.flag_array[conj_inds])
                        )
                        # if all data is flagged, average it all as if it were not
                        if np.all(flags_to_avg):
                            mask = np.zeros_like(flags_to_avg)
                        else:
                            mask = flags_to_avg

                        vis_to_avg = np.ma.masked_array(vis_to_avg, mask=mask)

                        nsample_to_avg = np.ma.masked_array(nsample_to_avg, mask=mask)

                        avg_vis = np.ma.average(
                            vis_to_avg, weights=nsample_to_avg, axis=0
                        )
                        avg_nsample = np.sum(nsample_to_avg, axis=0)
                        avg_flag = np.all(flags_to_avg, axis=0)
                        temp_data_array[this_obj_ind] = avg_vis

                        # If we need to flip the conjugation, to that on the
                        # value assignment here.
                        if fix_conj[this_obj_ind]:
                            temp_data_array[this_obj_ind] = np.conj(avg_vis)
                        else:
                            temp_data_array[this_obj_ind] = avg_vis
                        temp_nsample_array[this_obj_ind] = avg_nsample
                        temp_flag_array[this_obj_ind] = avg_flag

            if inplace:
                self.select(bls=bl_ants, keep_all_metadata=keep_all_metadata)
                if not self.metadata_only:
                    self.data_array = temp_data_array
                    self.nsample_array = temp_nsample_array
                    self.flag_array = temp_flag_array
                self.check()
                return
            else:
                if not self.metadata_only:
                    new_obj.data_array = temp_data_array
                    new_obj.nsample_array = temp_nsample_array
                    new_obj.flag_array = temp_flag_array
                new_obj.check()
                return new_obj
        else:
            return self.select(
                bls=bl_ants, inplace=inplace, keep_all_metadata=keep_all_metadata
            )

    def inflate_by_redundancy(self, tol=1.0, blt_order="time", blt_minor_order=None):
        """
        Expand data to full size, copying data among redundant baselines.

        Note that this method conjugates baselines to the 'u>0' convention in order
        to inflate the redundancies.

        Parameters
        ----------
        tol : float
            Redundancy tolerance in meters, default is 1.0 corresponding to 1 meter.
        blt_order : str
            string specifying primary order along the blt axis (see `reorder_blts`)
        blt_minor_order : str
            string specifying minor order along the blt axis (see `reorder_blts`)

        """
        self.conjugate_bls(convention="u>0")
        red_gps, centers, lengths = self.get_redundancies(
            tol=tol, use_antpos=True, conjugate_bls=True
        )

        # Stack redundant groups into one array.
        group_index, bl_array_full = zip(
            *[(i, bl) for i, gp in enumerate(red_gps) for bl in gp]
        )

        # TODO should be an assert that each baseline only ends up in one group

        # Map group index to blt indices in the compressed array.
        bl_array_comp = self.baseline_array
        uniq_bl = np.unique(bl_array_comp)

        group_blti = {}
        Nblts_full = 0
        for i, gp in enumerate(red_gps):
            for bl in gp:
                # First baseline in the group that is also in the compressed
                # baseline array.
                if bl in uniq_bl:
                    group_blti[i] = np.where(bl == bl_array_comp)[0]
                    # add number of blts for this group
                    Nblts_full += group_blti[i].size * len(gp)
                    break

        blt_map = np.zeros(Nblts_full, dtype=int)
        full_baselines = np.zeros(Nblts_full, dtype=int)
        missing = []
        counter = 0
        for bl, gi in zip(bl_array_full, group_index):
            try:
                # this makes the time the fastest axis
                blt_map[counter : counter + group_blti[gi].size] = group_blti[gi]
                full_baselines[counter : counter + group_blti[gi].size] = bl
                counter += group_blti[gi].size
            except KeyError:
                missing.append(bl)
                pass

        if np.any(missing):
            warnings.warn("Missing some redundant groups. Filling in available data.")

        # blt_map is an index array mapping compressed blti indices to uncompressed
        self.data_array = self.data_array[blt_map, ...]
        self.nsample_array = self.nsample_array[blt_map, ...]
        self.flag_array = self.flag_array[blt_map, ...]
        self.time_array = self.time_array[blt_map]
        self.lst_array = self.lst_array[blt_map]
        self.integration_time = self.integration_time[blt_map]
        self.uvw_array = self.uvw_array[blt_map, ...]

        self.baseline_array = full_baselines
        self.ant_1_array, self.ant_2_array = self.baseline_to_antnums(
            self.baseline_array
        )
        self.Nants_data = self._calc_nants_data()
        self.Nbls = np.unique(self.baseline_array).size
        self.Nblts = Nblts_full

        self.phase_center_app_ra = self.phase_center_app_ra[blt_map]
        self.phase_center_app_dec = self.phase_center_app_dec[blt_map]
        self.phase_center_frame_pa = self.phase_center_frame_pa[blt_map]
        self.phase_center_id_array = self.phase_center_id_array[blt_map]

        self.reorder_blts(order=blt_order, minor_order=blt_minor_order)

        self.check()

    def _convert_from_filetype(self, other):
        """
        Convert from a file-type specific object to a UVData object.

        Used in reads.

        Parameters
        ----------
        other : object that inherits from UVData
            File type specific object to convert to UVData
        """
        for p in other:
            param = getattr(other, p)
            setattr(self, p, param)

    def _convert_to_filetype(self, filetype):
        """
        Convert from a UVData object to a file-type specific object.

        Used in writes.

        Parameters
        ----------
        filetype : str
            Specifies what file type object to convert to. Options are: 'uvfits',
            'fhd', 'miriad', 'uvh5', 'mir', 'ms'

        Raises
        ------
        ValueError
            if filetype is not a known type
        """
        if filetype == "uvfits":
            from . import uvfits

            other_obj = uvfits.UVFITS()
        elif filetype == "fhd":
            from . import fhd

            other_obj = fhd.FHD()
        elif filetype == "miriad":
            from . import miriad

            other_obj = miriad.Miriad()
        elif filetype == "uvh5":
            from . import uvh5

            other_obj = uvh5.UVH5()
        elif filetype == "mir":
            from . import mir

            other_obj = mir.Mir()
        elif filetype == "ms":
            from . import ms

            other_obj = ms.MS()
        else:
            raise ValueError("filetype must be uvfits, mir, miriad, ms, fhd, or uvh5")

        with warnings.catch_warnings():
            warnings.filterwarnings("ignore", "The older phase attributes, including")
            for par in self:
                param = getattr(self, par)
                setattr(other_obj, par, param)
            if self.phase_center_catalog is None or len(self.phase_center_catalog) == 0:
                for attr in old_phase_attrs:
                    if hasattr(self, attr):
                        attr_val = getattr(self, attr)
                        setattr(other_obj, attr, attr_val)
        return other_obj

    def read_fhd(
        self,
        filelist,
        use_model=False,
        read_data=True,
        background_lsts=True,
        run_check=True,
        check_extra=True,
        run_check_acceptability=True,
        strict_uvw_antpos_check=False,
        check_autos=True,
        fix_autos=True,
        use_future_array_shapes=False,
    ):
        """
        Read in data from a list of FHD files.

        Parameters
        ----------
        filelist : array_like of str
            The list/array of FHD save files to read from. Must include at
            least one polarization file, a params file, a layout file and a flag file.
            An obs file is also required if `read_data` is False.
        use_model : bool
            Option to read in the model visibilities rather than the dirty
            visibilities (the default is False, meaning the dirty visibilities
            will be read).
        read_data : bool
            Read in the visibility, nsample 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, an obs file must be
            included in the filelist.
        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 after reading in the file (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
            reading in the file (the default is True, meaning the acceptable
            range check will be done).
        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 True.
        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.

        Raises
        ------
        ValueError
            If required files are missing or multiple files for any polarization
            are included in filelist.
            If there is no recognized key for visibility weights in the flags_file.

        """
        from . import fhd

        if isinstance(filelist[0], (list, tuple, np.ndarray)):
            raise ValueError(
                "Reading multiple files from class specific "
                "read functions is no longer supported. "
                "Use the generic `uvdata.read` function instead."
            )

        fhd_obj = fhd.FHD()
        fhd_obj.read_fhd(
            filelist,
            use_model=use_model,
            background_lsts=background_lsts,
            read_data=read_data,
            run_check=run_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,
            use_future_array_shapes=use_future_array_shapes,
        )
        self._convert_from_filetype(fhd_obj)
        del fhd_obj

    def read_mir(
        self,
        filepath,
        antenna_nums=None,
        antenna_names=None,
        bls=None,
        time_range=None,
        lst_range=None,
        polarizations=None,
        catalog_names=None,
        corrchunk=None,
        receivers=None,
        sidebands=None,
        select_where=None,
        apply_tsys=True,
        apply_flags=True,
        apply_dedoppler=False,
        pseudo_cont=False,
        rechunk=None,
        run_check=True,
        check_extra=True,
        run_check_acceptability=True,
        strict_uvw_antpos_check=False,
        allow_flex_pol=True,
        check_autos=True,
        fix_autos=True,
    ):
        """
        Read in data from an SMA MIR file.

        Note that with the exception of filepath, the rest of the parameters are
        used to sub-select a range of data.

        Parameters
        ----------
        filepath : str
            The file path to the MIR folder to read from.
        antenna_nums : array_like of int, optional
            The antennas numbers to include when reading data into 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.
        antenna_names : array_like of str, optional
            The antennas names to include when reading data into 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_nums` is also provided.
        bls : list of tuple, optional
            A list of antenna number tuples (e.g. [(0, 1), (3, 2)]) specifying baselines
            to include when reading data in to the object.
        time_range : array_like of float, optional
            The time range in Julian Date to include when reading data into
            the object, must be length 2. Some of the times in the file should
            fall between the first and last elements.
        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 numbers to include when reading data into the
            object, each value passed here should exist in the polarization_array.
        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.
        corrchunk : int or array-like of int
            Correlator "chunk" (spectral window) to include when reading data into the
            object, where 0 corresponds to the pseudo-continuum channel.
        receivers : str or array-like of str
            The names of the receivers ("230", "240", "345", "400") to include when
            reading data into the object.
        sidebands : str or array-like of str
            The names of the sidebands ("l" for lower, "u" for upper) to include when
            reading data into the object.
        select_where : tuple or list of tuples, optional
            Argument to pass to the `MirParser.select` method, which will downselect
            which data is read into the object.
        apply_flags : bool
            If set to True, apply "wideband" flags to the visibilities, which are
            recorded by the realtime system to denote when data are expected to be bad
            (e.g., antennas not on source, dewar warm). Default it true.
        apply_tsys : bool
            If set to False, data are returned as correlation coefficients (normalized
            by the auto-correlations). Default is True, which instead scales the raw
            visibilities and forward-gain of the antenna to produce values in Jy
            (uncalibrated).
        apply_dedoppler : bool
            If set to True, data will be corrected for any doppler-tracking performed
            during observations, and brought into the topocentric rest frame (default
            for UVData objects). Default is False.
        pseudo_cont : boolean
            Read in only pseudo-continuum values. Default is false.
        rechunk : int
            Number of channels to average over when reading in the dataset. Optional
            argument, typically required to be a power of 2.
        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
            uvw coordinates match antenna positions does not pass.
        allow_flex_pol : bool
            If only one polarization per spectral window is read (and the polarization
            differs from window to window), allow for the `UVData` object to use
            "flexible polarization", which compresses the polarization-axis of various
            attributes to be of length 1, sets the `flex_spw_polarization_array`
            attribute to define the polarization per spectral window. Default is True.
        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.
        """
        from . import mir

        mir_obj = mir.Mir()
        mir_obj.read_mir(
            filepath,
            antenna_nums=antenna_nums,
            antenna_names=antenna_names,
            bls=bls,
            time_range=time_range,
            lst_range=lst_range,
            polarizations=polarizations,
            catalog_names=catalog_names,
            corrchunk=corrchunk,
            receivers=receivers,
            sidebands=sidebands,
            select_where=select_where,
            apply_tsys=apply_tsys,
            apply_flags=apply_flags,
            apply_dedoppler=apply_dedoppler,
            pseudo_cont=pseudo_cont,
            rechunk=rechunk,
            run_check=run_check,
            check_extra=check_extra,
            run_check_acceptability=run_check_acceptability,
            strict_uvw_antpos_check=strict_uvw_antpos_check,
            allow_flex_pol=allow_flex_pol,
            check_autos=check_autos,
            fix_autos=fix_autos,
        )
        self._convert_from_filetype(mir_obj)
        del mir_obj

    def read_miriad(
        self,
        filepath,
        antenna_nums=None,
        ant_str=None,
        bls=None,
        polarizations=None,
        time_range=None,
        read_data=True,
        phase_type=None,
        projected=None,
        correct_lat_lon=True,
        background_lsts=True,
        run_check=True,
        check_extra=True,
        run_check_acceptability=True,
        strict_uvw_antpos_check=False,
        calc_lst=True,
        fix_old_proj=False,
        fix_use_ant_pos=True,
        check_autos=True,
        fix_autos=True,
        use_future_array_shapes=False,
    ):
        """
        Read in data from a miriad file.

        Parameters
        ----------
        filepath : str
            The miriad root directory to read from.
        antenna_nums : array_like of int, optional
            The antennas numbers to read into the object.
        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 include when reading data into 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 include when reading 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 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` or `polarizations` parameters, if it is a ValueError will be raised.
        polarizations : array_like of int or str, optional
            List of polarization integers or strings to read-in. e.g. ['xx', 'yy', ...]
        time_range : list of float, optional
            len-2 list containing min and max range of times in Julian Date to
            include when reading data into the object. e.g. [2458115.20, 2458115.40]
        read_data : bool
            Read in the visibility and flag data. If set to false,
            only the metadata will be read in. Setting read_data to False
            results in an incompletely defined object (check will not pass).
        phase_type : str, optional
            Deprecated, use the `projected` parameter insted. Option to specify the
            phasing status of the data. Options are 'drift', 'phased' or None. 'drift'
            is the same as setting the projected parameter to False, 'phased' is the
            same as setting the projected parameter to True. Ignored if `projected`
            is set.
        projected : bool or None
            Option to force the dataset to be labelled as projected or unprojected
            regardless of the evidence in the file. The default is None which means that
            the projection will be set based on the file contents. Be careful setting
            this keyword unless you are confident about the contents of the file.
        correct_lat_lon : bool
            Option to update the latitude and longitude from the known_telescopes
            list if the altitude is missing.
        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 after reading in the file (the default is True,
            meaning the check will be run). Ignored if read_data is False.
        check_extra : bool
            Option to check optional parameters as well as required ones (the
            default is True, meaning the optional parameters will be checked).
            Ignored if read_data is False.
        run_check_acceptability : bool
            Option to check acceptable range of the values of parameters after
            reading in the file (the default is True, meaning the acceptable
            range check will be done). Ignored if read_data is False.
        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.
        calc_lst : bool
            Recalculate the LST values that are present within the file, useful in
            cases where the "online" calculate values have precision or value errors.
            Default is True.
        fix_old_proj : bool
            Applies a fix to uvw-coordinates and phasing, assuming that the old `phase`
            method was used prior to writing the data, which had errors of the order of
            one part in 1e4 - 1e5. See the phasing memo for more details. Default is
            False.
        fix_use_ant_pos : bool
            If setting `fix_old_proj` to True, use the antenna positions to derive the
            correct uvw-coordinates rather than using the baseline vectors. Default is
            True.
        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 True.
        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.

        Raises
        ------
        IOError
            If root file directory doesn't exist.
        ValueError
            If incompatible select keywords are set (e.g. `ant_str` with other
            antenna selectors, `times` and `time_range`) or select keywords
            exclude all data or if keywords are set to the wrong type.
            If the data are multi source or have multiple
            spectral windows.
            If the metadata are not internally consistent.

        """
        from . import miriad

        if isinstance(filepath, (list, tuple, np.ndarray)):
            raise ValueError(
                "Reading multiple files from class specific "
                "read functions is no longer supported. "
                "Use the generic `uvdata.read` function instead."
            )

        miriad_obj = miriad.Miriad()
        miriad_obj.read_miriad(
            filepath,
            correct_lat_lon=correct_lat_lon,
            read_data=read_data,
            phase_type=phase_type,
            projected=projected,
            antenna_nums=antenna_nums,
            ant_str=ant_str,
            bls=bls,
            polarizations=polarizations,
            time_range=time_range,
            background_lsts=background_lsts,
            run_check=run_check,
            check_extra=check_extra,
            run_check_acceptability=run_check_acceptability,
            strict_uvw_antpos_check=strict_uvw_antpos_check,
            calc_lst=calc_lst,
            fix_old_proj=fix_old_proj,
            fix_use_ant_pos=fix_use_ant_pos,
            check_autos=check_autos,
            fix_autos=fix_autos,
            use_future_array_shapes=use_future_array_shapes,
        )
        self._convert_from_filetype(miriad_obj)
        del miriad_obj

    def read_ms(
        self,
        filepath,
        data_column="DATA",
        pol_order="AIPS",
        background_lsts=True,
        ignore_single_chan=True,
        raise_error=True,
        read_weights=True,
        allow_flex_pol=True,
        run_check=True,
        check_extra=True,
        run_check_acceptability=True,
        strict_uvw_antpos_check=False,
        check_autos=True,
        fix_autos=True,
        use_future_array_shapes=False,
    ):
        """
        Read in data from a measurement set.

        Parameters
        ----------
        filepath : str
            The measurement set root directory to read from.
        data_column : str
            name of CASA data column to read into data_array. Options are:
            'DATA', 'MODEL', or 'CORRECTED_DATA'
        pol_order : str
            Option to specify polarizations order convention, options are
            'CASA' or 'AIPS'.
        background_lsts : bool
            When set to True, the lst_array is calculated in a background thread.
        ignore_single_chan : bool
            Some measurement sets (e.g., those from ALMA) use single channel spectral
            windows for recording pseudo-continuum channels or storing other metadata
            in the track when the telescopes are not on source. Because of the way
            the object is strutured (where all spectral windows are assumed to be
            simultaneously recorded), this can significantly inflate the size and memory
            footprint of UVData objects. By default, single channel windows are ignored
            to avoid this issue, although they can be included if setting this parameter
            equal to True.
        raise_error : bool
            The measurement set format allows for different spectral windows and
            polarizations to have different metdata for the same time-baseline
            combination, but UVData objects do not. It also allows for timescales that
            are not supported by astropy. If any of these problems are detected, by
            default the reader will throw an error. However, if set to False, the reader
            will simply give a warning and try to do the best it can. If the problem is
            with differing metadata, it will use the first value read in the file as the
            "correct" metadata in the UVData object. If the problem is with the
            timescale, it will just assume UTC.
        read_weights : bool
            Read in the weights from the MS file, default is True. If false, the method
            will set the `nsamples_array` to the same uniform value (namely 1.0).
        allow_flex_pol : bool
            If only one polarization per spectral window is read (and the polarization
            differs from window to window), allow for the `UVData` object to use
            "flexible polarization", which compresses the polarization-axis of various
            attributes to be of length 1, sets the `flex_spw_polarization_array`
            attribute to define the polarization per spectral window.  Default is True.
        run_check : bool
            Option to check for the existence and proper shapes of parameters
            after after reading in the file (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
            reading in the file (the default is True, meaning the acceptable
            range check will be done).
        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 True.
        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.

        Raises
        ------
        IOError
            If root file directory doesn't exist.
        ValueError
            If the `data_column` is not set to an allowed value.
            If the data are have multiple subarrays or are multi source or have
            multiple spectral windows.
            If the data have multiple data description ID values.

        """
        if isinstance(filepath, (list, tuple, np.ndarray)):
            raise ValueError(
                "Reading multiple files from class specific "
                "read functions is no longer supported. "
                "Use the generic `uvdata.read` function instead."
            )

        from . import ms

        ms_obj = ms.MS()
        ms_obj.read_ms(
            filepath,
            data_column=data_column,
            pol_order=pol_order,
            background_lsts=background_lsts,
            ignore_single_chan=ignore_single_chan,
            raise_error=raise_error,
            read_weights=read_weights,
            allow_flex_pol=allow_flex_pol,
            run_check=run_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,
            use_future_array_shapes=use_future_array_shapes,
        )
        self._convert_from_filetype(ms_obj)
        del ms_obj

    def read_mwa_corr_fits(
        self,
        filelist,
        use_aoflagger_flags=None,
        use_cotter_flags=None,
        remove_dig_gains=True,
        remove_coarse_band=True,
        correct_cable_len=None,
        correct_van_vleck=False,
        cheby_approx=True,
        flag_small_auto_ants=True,
        flag_small_sig_ants=None,
        propagate_coarse_flags=True,
        flag_init=True,
        edge_width=80e3,
        start_flag="goodtime",
        end_flag=0.0,
        flag_dc_offset=True,
        remove_flagged_ants=True,
        phase_to_pointing_center=False,
        read_data=True,
        data_array_dtype=np.complex64,
        nsample_array_dtype=np.float32,
        background_lsts=True,
        run_check=True,
        check_extra=True,
        run_check_acceptability=True,
        strict_uvw_antpos_check=False,
        check_autos=True,
        fix_autos=True,
        use_future_array_shapes=False,
    ):
        """
        Read in MWA correlator gpu box files.

        The default settings remove some of the instrumental effects in the bandpass
        by dividing out the coarse band shape (for legacy data only) and the digital
        gains, and applying a cable length correction.
        If the desired output is raw correlator data, set remove_dig_gains=False,
        remove_coarse_band=False, correct_cable_len=False, and
        phase_to_pointing_center=False.

        Parameters
        ----------
        filelist : list of str
            The list of MWA correlator files to read from. Must include at
            least one fits file and only one metafits file per data set.
        use_aoflagger_flags : bool
            Option to use aoflagger mwaf flag files. Defaults to true if aoflagger
            flag files are submitted.
        use_cotter_flags : bool
            Being replaced by use_aoflagger_flags and will be removed in v2.4.
        remove_dig_gains : bool
            Option to divide out digital gains.
        remove_coarse_band : bool
            Option to divide out coarse band shape.
        correct_cable_len : bool
            Option to apply a cable delay correction.
        correct_van_vleck : bool
            Option to apply a van vleck correction.
        cheby_approx : bool
            Only used if correct_van_vleck is True. Option to implement the van
            vleck correction with a chebyshev polynomial approximation.
        flag_small_auto_ants : bool
            Only used if correct_van_vleck is True. Option to completely flag any
            antenna for which the autocorrelation falls below a threshold found by
            the Van Vleck correction to indicate bad data. Specifically, the
            threshold used is 0.5 * integration_time * channel_width. If set to False,
            only the times and frequencies at which the auto is below the
            threshold will be flagged for the antenna.
        flag_small_sig_ants : bool
            Being replaced with flag_small_auto_ants and will be removed in v2.4.
        propagate_coarse_flags : bool
            Option to propagate flags for missing coarse channel integrations
            across frequency.
        flag_init: bool
            Set to True in order to do routine flagging of coarse channel edges,
            start or end integrations, or the center fine channel of each coarse
            channel. See associated keywords.
        edge_width: float
            Only used if flag_init is True. Set to the width to flag on the edge
            of each coarse channel, in hz. Errors if not equal to integer
            multiple of channel_width. Set to 0 for no edge flagging.
        start_flag: float or str
            Only used if flag_init is True. The number of seconds to flag at the
            beginning of the observation. Set to 0 for no flagging. Default is
            'goodtime', which uses information in the metafits file to determine
            the length of time that should be flagged. Errors if input is not a
            float or 'goodtime'. Errors if float input is not equal to an
            integer multiple of the integration time.
        end_flag: floats
            Only used if flag_init is True. Set to the number of seconds to flag
            at the end of the observation. Set to 0 for no flagging. Errors if
            not an integer multiple of the integration time.
        flag_dc_offset: bool
            Only used if flag_init is True. Set to True to flag the center fine
            channel of each coarse channel. Only used if file_type is
            'mwa_corr_fits'.
        remove_flagged_ants : bool
            Option to perform a select to remove antennas flagged in the metafits
            file. If correct_van_vleck and flag_small_auto_ants are both True then
            antennas flagged by the Van Vleck correction are also removed.
        phase_to_pointing_center : bool
            Option to phase to the observation pointing center.
        read_data : bool
            Read in the visibility and flag data. If set to false, only the
            basic header info and metadata read in. Setting read_data to False
            results in a metdata only object.
        data_array_dtype : numpy dtype
            Datatype to store the output data_array as. Must be either
            np.complex64 (single-precision real and imaginary) or np.complex128
            (double-precision real and imaginary).
        nsample_array_dtype : numpy dtype
            Datatype to store the output nsample_array as. Must be either
            np.float64 (double-precision), np.float32 (single-precision), or
            np.float16 (half-precision). Half-precision is only recommended for
            cases where no sampling or averaging of baselines will occur,
            because round-off errors can be quite large (~1e-3).
        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 after reading in the file (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
            reading in the file (the default is True, meaning the acceptable
            range check will be done).
        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 True.
        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.

        Raises
        ------
        ValueError
            If required files are missing or multiple files metafits files are
            included in filelist.
            If files from different observations are included in filelist.
            If files in fileslist have different fine channel widths
            If file types other than fits, metafits, and mwaf files are included
            in filelist.

        """
        from . import mwa_corr_fits

        if isinstance(filelist[0], (list, tuple, np.ndarray)):
            raise ValueError(
                "Reading multiple files from class specific "
                "read functions is no longer supported. "
                "Use the generic `uvdata.read` function instead."
            )
        if use_cotter_flags is not None:
            use_aoflagger_flags = use_cotter_flags
            warnings.warn(
                "Use `use_aoflagger_flags` instead of `use_cotter_flags`."
                "`use_cotter_flags` is deprecated, and will be removed in "
                "pyuvdata v2.4.",
                DeprecationWarning,
            )
        if flag_small_sig_ants is not None:
            flag_small_auto_ants = flag_small_sig_ants
            warnings.warn(
                "Use `flag_small_auto_ants` instead of `flag_small_sig_ants`."
                "`flag_small_sig_ants` is deprecated, and will be removed in "
                "pyuvdata v2.4.",
                DeprecationWarning,
            )

        corr_obj = mwa_corr_fits.MWACorrFITS()
        corr_obj.read_mwa_corr_fits(
            filelist,
            use_aoflagger_flags=use_aoflagger_flags,
            remove_dig_gains=remove_dig_gains,
            remove_coarse_band=remove_coarse_band,
            correct_cable_len=correct_cable_len,
            correct_van_vleck=correct_van_vleck,
            cheby_approx=cheby_approx,
            flag_small_auto_ants=flag_small_auto_ants,
            propagate_coarse_flags=propagate_coarse_flags,
            flag_init=flag_init,
            edge_width=edge_width,
            start_flag=start_flag,
            end_flag=end_flag,
            flag_dc_offset=flag_dc_offset,
            remove_flagged_ants=remove_flagged_ants,
            phase_to_pointing_center=phase_to_pointing_center,
            read_data=read_data,
            data_array_dtype=data_array_dtype,
            nsample_array_dtype=nsample_array_dtype,
            background_lsts=background_lsts,
            run_check=run_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,
            use_future_array_shapes=use_future_array_shapes,
        )
        self._convert_from_filetype(corr_obj)
        del corr_obj

    def read_uvfits(
        self,
        filename,
        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,
        keep_all_metadata=True,
        read_data=True,
        background_lsts=True,
        run_check=True,
        check_extra=True,
        run_check_acceptability=True,
        strict_uvw_antpos_check=False,
        fix_old_proj=False,
        fix_use_ant_pos=True,
        check_autos=True,
        fix_autos=True,
        use_future_array_shapes=False,
    ):
        """
        Read in header, metadata and data from a single uvfits file.

        Parameters
        ----------
        filename : str
            The uvfits file to read from.
        antenna_nums : array_like of int, optional
            The antennas numbers to include when reading data into 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. Ignored if read_data is False.
        antenna_names : array_like of str, optional
            The antennas names to include when reading data into 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_nums` is also provided. Ignored if read_data 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 include when reading data into 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. Ignored if read_data is False.
        ant_str : str, optional
            A string containing information about what antenna numbers
            and polarizations to include when reading 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 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. Ignored if read_data is False.
        frequencies : array_like of float, optional
            The frequencies to include when reading data into the object, each
            value passed here should exist in the freq_array. Ignored if
            read_data is False.
        freq_chans : array_like of int, optional
            The frequency channel numbers to include when reading data into the
            object. Ignored if read_data is False.
        times : array_like of float, optional
            The times to include when reading data into the object, each value
            passed here should exist in the time_array in the file.
            Cannot be used with `time_range`.
        time_range : array_like of float, optional
            The time range in Julian Date to include when reading data into
            the object, must be length 2. Some of the times in the file 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 numbers to include when reading data into the
            object, each value passed here should exist in the polarization_array.
            Ignored if read_data is False.
        blt_inds : array_like of int, optional
            The baseline-time indices to include when reading data into the
            object. This is not commonly used. Ignored if read_data is False.
        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, optional
            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.
        keep_all_metadata : bool
            Option to keep all the metadata associated with antennas, even those
            that do not have data associated with them after the select option.
        read_data : bool
            Read in the visibility and flag data. If set to false, only the
            basic header info and metadata read in. Setting read_data to False
            results in a metdata 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 after reading in the file (the default is True,
            meaning the check will be run). Ignored if read_data is False.
        check_extra : bool
            Option to check optional parameters as well as required ones (the
            default is True, meaning the optional parameters will be checked).
            Ignored if read_data is False.
        run_check_acceptability : bool
            Option to check acceptable range of the values of parameters after
            reading in the file (the default is True, meaning the acceptable
            range check will be done). Ignored if read_data is False.
        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.
        fix_old_proj : bool
            Applies a fix to uvw-coordinates and phasing, assuming that the old `phase`
            method was used prior to writing the data, which had errors of the order of
            one part in 1e4 - 1e5. See the phasing memo for more details. Default is
            False.
        fix_use_ant_pos : bool
            If setting `fix_old_proj` to True, use the antenna positions to derive the
            correct uvw-coordinates rather than using the baseline vectors. Default is
            True.
        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 True.
        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.

        Raises
        ------
        IOError
            If filename doesn't exist.
        ValueError
            If incompatible select keywords are set (e.g. `ant_str` with other
            antenna selectors, `times` and `time_range`) or select keywords
            exclude all data or if keywords are set to the wrong type.
            If the data are multi source or have multiple
            spectral windows.
            If the metadata are not internally consistent or missing.

        """
        from . import uvfits

        if isinstance(filename, (list, tuple, np.ndarray)):
            raise ValueError(
                "Reading multiple files from class specific "
                "read functions is no longer supported. "
                "Use the generic `uvdata.read` function instead."
            )

        uvfits_obj = uvfits.UVFITS()
        uvfits_obj.read_uvfits(
            filename,
            antenna_nums=antenna_nums,
            antenna_names=antenna_names,
            ant_str=ant_str,
            bls=bls,
            frequencies=frequencies,
            freq_chans=freq_chans,
            times=times,
            time_range=time_range,
            lsts=lsts,
            lst_range=lst_range,
            polarizations=polarizations,
            blt_inds=blt_inds,
            catalog_names=catalog_names,
            phase_center_ids=phase_center_ids,
            keep_all_metadata=keep_all_metadata,
            read_data=read_data,
            background_lsts=background_lsts,
            run_check=run_check,
            check_extra=check_extra,
            run_check_acceptability=run_check_acceptability,
            strict_uvw_antpos_check=strict_uvw_antpos_check,
            fix_old_proj=fix_old_proj,
            fix_use_ant_pos=fix_use_ant_pos,
            check_autos=check_autos,
            fix_autos=fix_autos,
            use_future_array_shapes=use_future_array_shapes,
        )
        self._convert_from_filetype(uvfits_obj)
        del uvfits_obj

    def read_uvh5(
        self,
        filename,
        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,
        keep_all_metadata=True,
        read_data=True,
        data_array_dtype=np.complex128,
        multidim_index=False,
        remove_flex_pol=True,
        background_lsts=True,
        run_check=True,
        check_extra=True,
        run_check_acceptability=True,
        strict_uvw_antpos_check=False,
        fix_old_proj=None,
        fix_use_ant_pos=True,
        check_autos=True,
        fix_autos=True,
        use_future_array_shapes=False,
        time_axis_faster_than_bls=None,
        blts_are_rectangular=None,
        blt_order=None,
        recompute_nbls: bool | None = None,
    ):
        """
        Read a UVH5 file.

        Parameters
        ----------
        filename : str
             The UVH5 file to read from.
        antenna_nums : array_like of int, optional
            The antennas numbers to include when reading data into 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. Ignored if read_data is False.
        antenna_names : array_like of str, optional
            The antennas names to include when reading data into 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_nums` is also provided. Ignored if read_data 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 include when reading data into 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. Ignored if read_data is False.
        ant_str : str, optional
            A string containing information about what antenna numbers
            and polarizations to include when reading 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 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. Ignored if read_data is False.
        frequencies : array_like of float, optional
            The frequencies to include when reading data into the object, each
            value passed here should exist in the freq_array. Ignored if
            read_data is False.
        freq_chans : array_like of int, optional
            The frequency channel numbers to include when reading data into the
            object. Ignored if read_data is False.
        times : array_like of float, optional
            The times to include when reading data into the object, each value
            passed here should exist in the time_array in the file.
            Cannot be used with `time_range`.
        time_range : array_like of float, optional
            The time range in Julian Date to include when reading data into
            the object, must be length 2. Some of the times in the file 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 numbers to include when reading data into the
            object, each value passed here should exist in the polarization_array.
            Ignored if read_data is False.
        blt_inds : array_like of int, optional
            The baseline-time indices to include when reading data into the
            object. This is not commonly used. Ignored if read_data is False.
        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, optional
            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.
        keep_all_metadata : bool
            Option to keep all the metadata associated with antennas, even those
            that do not have data associated with them after the select option.
        read_data : bool
            Read in the visibility and flag data. If set to false, only the
            basic header info and metadata will be read in. Setting read_data to
            False results in an incompletely defined object (check will not pass).
        data_array_dtype : numpy dtype
            Datatype to store the output data_array as. Must be either
            np.complex64 (single-precision real and imaginary) or np.complex128 (double-
            precision real and imaginary). Only used if the datatype of the visibility
            data on-disk is not 'c8' or 'c16'.
        multidim_index : bool
            [Only for HDF5] If True, attempt to index the HDF5 dataset
            simultaneously along all data axes. Otherwise index one axis at-a-time.
            This only works if data selection is sliceable along all but one axis.
            If indices are not well-matched to data chunks, this can be slow.
        remove_flex_pol : bool
            If True and if the file is a flex_pol file, convert back to a standard
            UVData 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 after reading in the file (the default is True,
            meaning the check will be run). Ignored if read_data is False.
        check_extra : bool
            Option to check optional parameters as well as required ones (the
            default is True, meaning the optional parameters will be checked).
            Ignored if read_data is False.
        run_check_acceptability : bool
            Option to check acceptable range of the values of parameters after
            reading in the file (the default is True, meaning the acceptable
            range check will be done). Ignored if read_data is False.
        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.
        fix_old_proj : bool
            Applies a fix to uvw-coordinates and phasing, assuming that the old `phase`
            method was used prior to writing the data, which had errors of the order of
            one part in 1e4 - 1e5. See the phasing memo for more details. Default is
            to apply the correction if the attributes `phase_center_app_ra` and
            `phase_center_app_dec` are missing (as they were introduced alongside the
            new phasing method).
        fix_use_ant_pos : bool
            If setting `fix_old_proj` to True, use the antenna positions to derive the
            correct uvw-coordinates rather than using the baseline vectors. Default is
            True.
        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 True.
        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.
        blt_order : tuple of str or "determine", optional
            The order of the baseline-time axis *in the file*. This can be determined,
            or read directly from file, however since it has been optional in the past,
            many existing files do not contain it in the metadata.
            Some reading operations are significantly faster if this is known, so
            providing it here can provide a speedup. Default is to try and read it from
            file, and if not there, just leave it as None. Set to "determine" to
            auto-detect the blt_order from the metadata (takes extra time to do so).
        blts_are_rectangular : bool, optional
            Whether the baseline-time axis is rectangular. This can be read from
            metadata in new files, but many old files do not contain it. If not
            provided, the rectangularity will be determined from the data. This is a
            non-negligible operation, so if you know it, it can be provided here to
            speed up reading.
        time_axis_faster_than_bls : bool, optional
            If blts are rectangular, this variable specifies whether the time axis is
            the fastest-moving virtual axis. Various reading functions benefit from
            knowing this, so if it is known, it can be provided here to speed up
            reading. It will be determined from the data if not provided.
        recompute_nbls : bool, optional
            Whether to recompute the number of unique baselines from the data. Before
            v1.2 of the UVH5 spec, it was possible to have an incorrect number of
            baselines in the header without error, so this provides an opportunity to
            rectify it. Old HERA files (< March 2023) may have this issue, but in this
            case the correct number of baselines can be computed more quickly than by
            fully re=computing, and so we do this.

        Raises
        ------
        IOError
            If filename doesn't exist.
        ValueError
            If the data_array_dtype is not a complex dtype.
            If incompatible select keywords are set (e.g. `ant_str` with other
            antenna selectors, `times` and `time_range`) or select keywords
            exclude all data or if keywords are set to the wrong type.

        """
        from . import uvh5

        if isinstance(filename, (list, tuple, np.ndarray)):
            raise ValueError(
                "Reading multiple files from class specific "
                "read functions is no longer supported. "
                "Use the generic `uvdata.read` function instead."
            )

        uvh5_obj = uvh5.UVH5()
        uvh5_obj.read_uvh5(
            filename,
            antenna_nums=antenna_nums,
            antenna_names=antenna_names,
            ant_str=ant_str,
            bls=bls,
            frequencies=frequencies,
            freq_chans=freq_chans,
            times=times,
            time_range=time_range,
            lsts=lsts,
            lst_range=lst_range,
            polarizations=polarizations,
            blt_inds=blt_inds,
            phase_center_ids=phase_center_ids,
            catalog_names=catalog_names,
            data_array_dtype=data_array_dtype,
            keep_all_metadata=keep_all_metadata,
            read_data=read_data,
            multidim_index=multidim_index,
            remove_flex_pol=remove_flex_pol,
            background_lsts=background_lsts,
            run_check=run_check,
            check_extra=check_extra,
            run_check_acceptability=run_check_acceptability,
            strict_uvw_antpos_check=strict_uvw_antpos_check,
            fix_old_proj=fix_old_proj,
            fix_use_ant_pos=fix_use_ant_pos,
            check_autos=check_autos,
            fix_autos=fix_autos,
            use_future_array_shapes=use_future_array_shapes,
            recompute_nbls=recompute_nbls,
            blt_order=blt_order,
            blts_are_rectangular=blts_are_rectangular,
            time_axis_faster_than_bls=time_axis_faster_than_bls,
        )
        self._convert_from_filetype(uvh5_obj)
        del uvh5_obj

    def read(
        self,
        filename,
        axis=None,
        file_type=None,
        read_data=True,
        skip_bad_files=False,
        background_lsts=True,
        ignore_name=False,
        use_future_array_shapes=False,
        # phasing parameters
        allow_rephase=None,
        phase_center_radec=None,
        unphase_to_drift=None,
        phase_frame=None,
        phase_epoch=None,
        orig_phase_frame=None,
        phase_use_ant_pos=None,
        fix_old_proj=None,
        fix_use_ant_pos=True,
        make_multi_phase=False,
        # selecting parameters
        antenna_nums=None,
        antenna_names=None,
        ant_str=None,
        bls=None,
        catalog_names=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,
        keep_all_metadata=True,
        # checking parameters
        run_check=True,
        check_extra=True,
        run_check_acceptability=True,
        strict_uvw_antpos_check=False,
        check_autos=True,
        fix_autos=True,
        # file-type specific parameters
        # miriad
        phase_type=None,
        projected=None,
        correct_lat_lon=True,
        calc_lst=True,
        # FHD
        use_model=False,
        # MS
        data_column="DATA",
        pol_order="AIPS",
        ignore_single_chan=True,
        raise_error=True,
        read_weights=True,
        # MS & MIR
        allow_flex_pol=True,
        # uvh5
        multidim_index=False,
        remove_flex_pol=True,
        blt_order: tuple[str] | Literal["determine"] | None = None,
        blts_are_rectangular: bool | None = None,
        time_axis_faster_than_bls: bool | None = None,
        # uvh5 & mwa_corr_fits
        data_array_dtype=np.complex128,
        # mwa_corr_fits
        use_aoflagger_flags=None,
        use_cotter_flags=None,
        remove_dig_gains=True,
        remove_coarse_band=True,
        correct_cable_len=None,
        correct_van_vleck=False,
        cheby_approx=True,
        flag_small_auto_ants=True,
        flag_small_sig_ants=None,
        propagate_coarse_flags=True,
        flag_init=True,
        edge_width=80e3,
        start_flag="goodtime",
        end_flag=0.0,
        flag_dc_offset=True,
        remove_flagged_ants=True,
        phase_to_pointing_center=False,
        nsample_array_dtype=np.float32,
        # MIR
        corrchunk=None,
        receivers=None,
        sidebands=None,
        mir_select_where=None,
        apply_tsys=True,
        apply_flags=True,
        apply_dedoppler=False,
        pseudo_cont=False,
        rechunk=None,
        recompute_nbls: bool | None = None,
    ):
        """
        Read a generic file into a UVData 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 phasing, selecting on
        read (partial read) and 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 ['uvfits', 'miriad', 'ms', 'uvh5', 'fhd', 'mwa_corr_fits', 'mir']
            or None. If None, the code attempts to guess what the file type is.
            For miriad and ms types, this is based on the standard directory
            structure. For FHD, uvfits and uvh5 files it's based on file
            extensions (FHD: .sav, .txt; uvfits: .uvfits; uvh5: .uvh5).
            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: 'blt', 'freq', 'polarization'. Only used if
            multiple files are passed.
        read_data : bool
            Read in the data. Not used if file_type is 'ms' or 'mir'.
            If set to False, only the metadata will be read in. Setting read_data to
            False results in a metdata only object.
        skip_bad_files : bool
            Option when reading multiple files to catch read errors such that
            the read continues even if one or more files are corrupted. Files
            that produce errors will be printed. Default is False (files will
            not be skipped).
        background_lsts : bool
            When set to True, the lst_array is calculated in a background thread.
        ignore_name : bool
            Only relevant when reading in multiple files, which are concatenated into a
            single UVData object. Option to ignore the name of the phase center when
            combining multiple files, which would otherwise result in an error being
            raised because of attributes not matching. Doing so effectively adopts the
            name found in the first file read in. Default is False.
        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.

        Phasing
        -------
        allow_rephase :  bool
            Deprecated, will have no effect starting in version 2.4 and will be removed
            in version 2.6 because data with different phasing can now be combined.
            Allow rephasing of phased file data. Currently defaults to True, which will
            cause data to be rephased (to preserve the previous behavior). Set to False
            to prevent rephasing and mimic the future behavior.
        phase_center_radec : array_like of float
            Deprecated and will be removed in version 2.4, instead call `phase` on the
            combined object after this method. The phase center to phase the files to
            before combining the objects in radians (in the ICRS frame). If set to None
            and allow_rephase is True and multiple files are read with different phase
            centers, the phase center of the first file will be used.
        unphase_to_drift : bool
            Deprecated, instead call `unproject_phase` on the combined object after
            this method. Unproject the data from the files before combining them.
        phase_frame : str
            The astropy frame to phase to.Only used if `phase_center_radec` is set.
            Deprecated and will be removed in version 2.4 with `phase_center_radec`.
        phase_epoch : astropy.time.Time object or str
            The epoch to use for phasing. Either an astropy Time object or the
            string "J2000" (which is the default).
            Note that the epoch is only used to evaluate the ra & dec values,
            if the epoch is not J2000, the ra & dec values are interpreted
            as FK5 ra/dec values and transformed to J2000, the data are then
            phased to the J2000 ra/dec values.
            Deprecated and will be removed in version 2.4 with `phase_center_radec`.
        orig_phase_frame : str
            Deprecated, has no effect. Will be removed in version 2.4.
        phase_use_ant_pos : bool
            If True, calculate the uvws directly from the antenna positions rather than
            from the existing uvws. Only used if `unphase_to_drift` or
            `phase_center_radec` are set. Deprecated and will be removed in version 2.4
            with `unphase_to_drift` and `phase_center_radec`.
        fix_old_proj : bool
            Applies a fix to uvw-coordinates and phasing, assuming that the old `phase`
            method was used prior to writing the data, which had errors of the order of
            one part in 1e4 - 1e5. See the phasing memo for more details. Default is
            False, unless reading a UVH5 file that is missing the `phase_center_app_ra`
            and `phase_center_app_dec` attributes (as these were introduced at the same
            time as the new `phase` method), in which case the default is True.
        fix_use_ant_pos : bool
            If setting `fix_old_proj` to True, use the antenna positions to derive the
            correct uvw-coordinates rather than using the baseline vectors. Default is
            True.
        make_multi_phase : bool
            Deprecated, will be removed in version 2.4. Since all objects now support
            multiple phase centers, this has no effect if set to False. The only effect
            of setting this to True is to prevent rephasing. A better approach is to set
            `allow_rephase=False`.

        Selecting
        ---------
        antenna_nums : array_like of int, optional
            The antennas numbers to include when reading data into 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.
        antenna_names : array_like of str, optional
            The antennas names to include when reading data into 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_nums` is also provided.
        ant_str : str, optional
            A string containing information about what antenna numbers
            and polarizations to include when reading 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 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.
        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 include when reading data into 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.
        catalog_names : str or array-like of str, optional
            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.
        frequencies : array_like of float, optional
            The frequencies to include when reading data into the object, each
            value passed here should exist in the freq_array.
        freq_chans : array_like of int, optional
            The frequency channel numbers to include when reading data into the
            object. Ignored if read_data is False.
        times : array_like of float, optional
            The times to include when reading data into the object, each value
            passed here should exist in the time_array in the file.
            Cannot be used with `time_range`.
        time_range : array_like of float, optional
            The time range in Julian Date to include when reading data into
            the object, must be length 2. Some of the times in the file 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 numbers to include when reading data into the
            object, each value passed here should exist in the polarization_array.
        blt_inds : array_like of int, optional
            The baseline-time indices to include when reading data into the
            object. 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.
        keep_all_metadata : bool
            Option to keep all the metadata associated with antennas, even those
            that do not have data associated with them after the select option.

        Checking
        --------
        run_check : bool
            Option to check for the existence and proper shapes of parameters
            after after reading in the file (the default is True,
            meaning the check will be run). Ignored if read_data is False.
        check_extra : bool
            Option to check optional parameters as well as required ones (the
            default is True, meaning the optional parameters will be checked).
            Ignored if read_data is False.
        run_check_acceptability : bool
            Option to check acceptable range of the values of parameters after
            reading in the file (the default is True, meaning the acceptable
            range check will be done). Ignored if read_data is False.
        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 True.

        Miriad
        ------
        phase_type : str, optional
            Deprecated, use the `projected` parameter insted. Option to specify the
            phasing status of the data. Options are 'drift', 'phased' or None. 'drift'
            is the same as setting the projected parameter to False, 'phased' is the
            same as setting the projected parameter to True. Ignored if `projected`
            is set.
        projected : bool or None
            Option to force the dataset to be labelled as projected or unprojected
            regardless of the evidence in the file. The default is None which means that
            the projection will be set based on the file contents. Be careful setting
            this keyword unless you are confident about the contents of the file.
        correct_lat_lon : bool
            Option to update the latitude and longitude from the known_telescopes
            list if the altitude is missing.
        calc_lst : bool
            Recalculate the LST values that are present within the file, useful in
            cases where the "online" calculate values have precision or value errors.
            Default is True.

        FHD
        ---
        use_model : bool
            Option to read in the model visibilities rather than the dirty
            visibilities (the default is False, meaning the dirty visibilities
            will be read).

        MS
        --
        data_column : str
            name of CASA data column to read into data_array. Options are:
            'DATA', 'MODEL', or 'CORRECTED_DATA'.
        pol_order : str
            Option to specify polarizations order convention, options are
            'CASA' or 'AIPS'.
        ignore_single_chan : bool
            Option to ignore single channel spectral windows in measurement sets to
            limit object size. Some measurement sets (e.g., those from ALMA) use single
            channel spectral windows for recording pseudo-continuum channels or storing
            other metadata in the track when the telescopes are not on source. Because
            of the way the object is strutured (where all spectral windows are assumed
            to be simultaneously recorded), this can significantly inflate the size and
            memory footprint of UVData objects. By default, single channel windows are
            ignored to avoid this issue, they can be included by setting this parameter
            to True.
        raise_error : bool
            The measurement set format allows for different spectral windows and
            polarizations to have different metdata for the same time-baseline
            combination, but UVData objects do not. It also allows for timescales that
            are not supported by astropy. If any of these problems are detected, by
            default the reader will throw an error. However, if set to False, the reader
            will simply give a warning and try to do the best it can. If the problem is
            with differing metadata, it will use the first value read in the file as the
            "correct" metadata in the UVData object. If the problem is with the
            timescale, it will just assume UTC.
        read_weights : bool
            Read in the weights from the MS file. If false, the method
            will set the `nsamples_array` to the same uniform value (namely 1.0).
        allow_flex_pol : bool
            If only one polarization per spectral window is read (and the polarization
            differs from window to window), allow for the `UVData` object to use
            "flexible polarization", which compresses the polarization-axis of various
            attributes to be of length 1, sets the `flex_spw_polarization_array`
            attribute to define the polarization per spectral window. Default is True.

        UVH5
        ----
        multidim_index : bool
            If True, attempt to index the HDF5 dataset simultaneously along all data
            axes. Otherwise index one axis at-a-time. This only works if data selection
            is sliceable along all but one axis. If indices are not well-matched to
            data chunks, this can be slow.
        remove_flex_pol : bool
            If True and if the file is a flex_pol file, convert back to a standard
            UVData object.
        data_array_dtype : numpy dtype
            Datatype to store the output data_array as. Must be either
            np.complex64 (single-precision real and imaginary) or np.complex128 (double-
            precision real and imaginary). Only used if the datatype of the visibility
            data on-disk is not 'c8' or 'c16'.
        blt_order : tuple of str or "determine", optional
            The order of the baseline-time axis *in the file*. This can be determined,
            or read directly from file, however since it has been optional in the past,
            many existing files do not contain it in the metadata.
            Some reading operations are significantly faster if this is known, so
            providing it here can provide a speedup. Default is to try and read it from
            file, and if not there, just leave it as None. Set to "determine" to
            auto-detect the blt_order from the metadata (takes extra time to do so).
        blts_are_rectangular : bool, optional
            Whether the baseline-time axis is rectangular. This can be read from
            metadata in new files, but many old files do not contain it. If not
            provided, the rectangularity will be determined from the data. This is a
            non-negligible operation, so if you know it, it can be provided here to
            speed up reading.
        time_axis_faster_than_bls : bool, optional
            If blts are rectangular, this variable specifies whether the time axis is
            the fastest-moving virtual axis. Various reading functions benefit from
            knowing this, so if it is known, it can be provided here to speed up
            reading. It will be determined from the data if not provided.
        recompute_nbls : bool, optional
            Whether to recompute the number of unique baselines from the data. Before
            v1.2 of the UVH5 spec, it was possible to have an incorrect number of
            baselines in the header without error, so this provides an opportunity to
            rectify it. Old HERA files (< March 2023) may have this issue, but in this
            case the correct number of baselines can be computed more quickly than by
            fully re=computing, and so we do this.


        MWA FITS
        --------
        use_aoflagger_flags : bool
            Option to use aoflagger mwaf flag files. Defaults to true if aoflagger
            flag files are submitted.
        use_cotter_flags : bool
            Being replaced by use_aoflagger_flags and will be removed in v2.4.
        remove_dig_gains : bool
            Option to divide out digital gains.
        remove_coarse_band : bool
            Option to divide out coarse band shape.
        correct_cable_len : bool
            Flag to apply cable length correction.
        correct_van_vleck : bool
            Flag to apply a van vleck correction.
        cheby_approx : bool
            Option to implement the van vleck correction with a chebyshev polynomial
            approximation. Set to False to run the integral version of the correction.
            Only used if correct_van_vleck is True.
        flag_small_auto_ants : bool
            Option to completely flag any antenna for which the autocorrelation falls
            below a threshold found by the Van Vleck correction to indicate bad data.
            Specifically, the threshold used is 0.5 * integration_time * channel_width.
            If set to False, only the times and frequencies at which the auto is below
            the threshold will be flagged for the antenna.
            Only used if correct_van_vleck is True.
        flag_small_sig_ants : bool
            Deprecated, replaced by flag_small_auto_ants and will be removed in v2.4.
        propogate_coarse_flags : bool
            Option to propogate flags for missing coarse channel integrations
            across frequency.
        flag_init: bool
            Set to True in order to do routine flagging of coarse channel edges, start
            or end integrations, or the center fine channel of each coarse
            channel. See associated keywords below.
        edge_width: float
            Only used if flag_init is True. Set to the width to flag on the edge of
            each coarse channel, in hz. Errors if not equal to integer multiple of
            channel_width. Set to 0 for no edge flagging.
        start_flag: float or str
            Only used if flag_init is True. The number of seconds to flag at the
            beginning of the observation. Set to 0 for no flagging. Default is
            'goodtime', which uses information in the metafits file to determine
            the length of time that should be flagged. Errors if input is not a
            float or 'goodtime'. Errors if float input is not equal to an
            integer multiple of the integration time.
        end_flag: floats
            Only used if flag_init is True. Set to the number of seconds to flag at the
            end of the observation. Set to 0 for no flagging. Errors if not an integer
            multiple of the integration time.
        flag_dc_offset: bool
            Only used if flag_init is True. Set to True to flag the center fine channel
            of each coarse channel.
        remove_flagged_ants : bool
            Option to perform a select to remove antennas flagged in the metafits
            file. If correct_van_vleck and flag_small_auto_ants are both True then
            antennas flagged by the Van Vleck correction are also removed.
        phase_to_pointing_center : bool
            Flag to phase to the pointing center.  Cannot be set if phase_center_radec
            is set to a value.
        data_array_dtype : numpy dtype
            Datatype to store the output data_array as. Must be either
            np.complex64 (single-precision real and imaginary) or np.complex128 (double-
            precision real and imaginary).
        nsample_array_dtype : numpy dtype
            Datatype to store the output nsample_array as. Must be either
            np.float64 (double-precision), np.float32 (single-precision), or
            np.float16 (half-precision). Half-precision is only recommended for
            cases where no sampling or averaging of baselines will occur,
            because round-off errors can be quite large (~1e-3).

        MIR
        ---
        corrchunk : int or array-like of int
            Correlator "chunk" (spectral window) to include when reading data into the
            object, where 0 corresponds to the pseudo-continuum channel.
        receivers : str or array-like of str
            The names of the receivers ("230", "240", "345", "400") to include when
            reading data into the object.
        sidebands : str or array-like of str
            The names of the sidebands ("l" for lower, "u" for upper) to include when
            reading data into the object.
        mir_select_where : tuple or list of tuples, optional
            Argument to pass to the `MirParser.select` method, which will downselect
            which data is read into the object.
        apply_flags : bool
            If set to True, apply "wideband" flags to the visibilities, which are
            recorded by the realtime system to denote when data are expected to be bad
            (e.g., antennas not on source, dewar warm). Default it true.
        apply_tsys : bool
            If set to False, data are returned as correlation coefficients (normalized
            by the auto-correlations). Default is True, which instead scales the raw
            visibilities and forward-gain of the antenna to produce values in Jy
            (uncalibrated).
        apply_dedoppler : bool
            If set to True, data will be corrected for any doppler-tracking performed
            during observations, and brought into the topocentric rest frame (default
            for UVData objects). Default is False.
        allow_flex_pol : bool
            If only one polarization per spectral window is read (and the polarization
            differs from window to window), allow for the `UVData` object to use
            "flexible polarization", which compresses the polarization-axis of various
            attributes to be of length 1, sets the `flex_spw_polarization_array`
            attribute to define the polarization per spectral window. Default is True.

        Raises
        ------
        ValueError
            If the file_type is not set and cannot be determined from the file name.
            If incompatible select keywords are set (e.g. `ant_str` with other
            antenna selectors, `times` and `time_range`) or select keywords
            exclude all data or if keywords are set to the wrong type.
            If phase_center_radec is not None and is not length 2.

        """
        if allow_rephase:
            warnings.warn(
                "The `allow_rephase` option is deprecated. It will have no effect "
                "starting in pyuvdata v2.4 and will be removed in pyuvdata v2.6."
                "It is no longer needed as data with different phase centers can now "
                "be combined on one object. Starting in version 2.4, the data will be "
                "combined on one object without rephasing. To get that behavior now, "
                "set `allow_rephase=False`. The final object can be rephased if "
                "desired by calling `phase` after this method.",
                DeprecationWarning,
            )

        if make_multi_phase:
            warnings.warn(
                "The `make_multi_phase` option is deprecated. Since all objects now "
                "support multiple phase centers, the only effect of setting this to "
                "True is to prevent rephasing. A better approach is to set "
                "`allow_rephase=False`. This will become an error in version 3.0.",
                DeprecationWarning,
            )

        if isinstance(filename, (list, tuple, np.ndarray)):
            # this is either a list of separate files to read or a list of
            # FHD files or MWA correlator FITS files
            if isinstance(filename[0], (list, tuple, np.ndarray)):
                if file_type is None:
                    # this must be a list of lists of FHD or MWA correlator FITS
                    _, extension = os.path.splitext(filename[0][0])
                    if extension == ".sav" or extension == ".txt":
                        file_type = "fhd"
                    elif (
                        extension == ".fits"
                        or extension == ".metafits"
                        or extension == ".mwaf"
                    ):
                        file_type = "mwa_corr_fits"
                multi = True
            else:
                if file_type is None:
                    _, extension = os.path.splitext(filename[0])
                    if extension == ".sav" or extension == ".txt":
                        file_type = "fhd"
                    elif (
                        extension == ".fits"
                        or extension == ".metafits"
                        or extension == ".mwaf"
                    ):
                        file_type = "mwa_corr_fits"

                if file_type == "fhd" or file_type == "mwa_corr_fits":
                    multi = False
                else:
                    multi = True
        else:
            multi = False

        if file_type is None:
            if multi:
                file_test = filename[0]
            else:
                file_test = filename

            if os.path.isdir(file_test):
                # it's a directory, so it's either miriad, mir, or ms file type
                if os.path.exists(os.path.join(file_test, "vartable")):
                    # It's miriad.
                    file_type = "miriad"
                elif os.path.exists(os.path.join(file_test, "OBSERVATION")):
                    # It's a measurement set.
                    file_type = "ms"
                elif os.path.exists(os.path.join(file_test, "sch_read")):
                    # It's Submillimeter Array mir format.
                    file_type = "mir"

            else:
                _, extension = os.path.splitext(file_test)
                if extension == ".uvfits":
                    file_type = "uvfits"
                elif extension == ".uvh5":
                    file_type = "uvh5"

        if file_type is None:
            raise ValueError(
                "File type could not be determined, use the "
                "file_type keyword to specify the type."
            )

        if time_range is not None:
            if times is not None:
                raise ValueError("Only one of times and time_range can be provided.")

        if antenna_names is not None and antenna_nums is not None:
            raise ValueError(
                "Only one of antenna_nums and antenna_names can " "be provided."
            )

        if multi:
            file_num = 0
            file_warnings = ""
            unread = True
            f = filename[file_num]
            while unread and file_num < len(filename):
                try:
                    with warnings.catch_warnings():
                        warnings.filterwarnings(
                            "ignore", "The `allow_rephase` option is deprecated"
                        )
                        warnings.filterwarnings(
                            "ignore", "The `make_multi_phase` option is deprecated"
                        )
                        self.read(
                            filename[file_num],
                            file_type=file_type,
                            read_data=read_data,
                            skip_bad_files=skip_bad_files,
                            background_lsts=background_lsts,
                            use_future_array_shapes=use_future_array_shapes,
                            # phasing parameters
                            fix_old_proj=fix_old_proj,
                            fix_use_ant_pos=fix_use_ant_pos,
                            make_multi_phase=make_multi_phase,
                            allow_rephase=allow_rephase,
                            phase_center_radec=phase_center_radec,
                            phase_frame=phase_frame,
                            phase_epoch=phase_epoch,
                            phase_use_ant_pos=phase_use_ant_pos,
                            unphase_to_drift=unphase_to_drift,
                            # selecting parameters
                            antenna_nums=antenna_nums,
                            antenna_names=antenna_names,
                            ant_str=ant_str,
                            bls=bls,
                            frequencies=frequencies,
                            freq_chans=freq_chans,
                            times=times,
                            time_range=time_range,
                            lsts=lsts,
                            lst_range=lst_range,
                            polarizations=polarizations,
                            blt_inds=blt_inds,
                            phase_center_ids=phase_center_ids,
                            keep_all_metadata=keep_all_metadata,
                            # checking parameters
                            run_check=run_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,
                            # file-type specific parameters
                            # miriad
                            phase_type=phase_type,
                            projected=projected,
                            correct_lat_lon=correct_lat_lon,
                            calc_lst=calc_lst,
                            # FHD
                            use_model=use_model,
                            # MS
                            data_column=data_column,
                            pol_order=pol_order,
                            ignore_single_chan=ignore_single_chan,
                            raise_error=raise_error,
                            read_weights=read_weights,
                            # MS & MIR
                            allow_flex_pol=allow_flex_pol,
                            # uvh5
                            multidim_index=multidim_index,
                            remove_flex_pol=remove_flex_pol,
                            # uvh5 & mwa_corr_fits
                            data_array_dtype=data_array_dtype,
                            # mwa_corr_fits
                            use_aoflagger_flags=use_aoflagger_flags,
                            use_cotter_flags=use_cotter_flags,
                            remove_dig_gains=remove_dig_gains,
                            remove_coarse_band=remove_coarse_band,
                            correct_cable_len=correct_cable_len,
                            correct_van_vleck=correct_van_vleck,
                            cheby_approx=cheby_approx,
                            flag_small_auto_ants=flag_small_auto_ants,
                            flag_small_sig_ants=flag_small_sig_ants,
                            propagate_coarse_flags=propagate_coarse_flags,
                            flag_init=flag_init,
                            edge_width=edge_width,
                            start_flag=start_flag,
                            end_flag=end_flag,
                            flag_dc_offset=flag_dc_offset,
                            remove_flagged_ants=remove_flagged_ants,
                            phase_to_pointing_center=phase_to_pointing_center,
                            nsample_array_dtype=nsample_array_dtype,
                            corrchunk=corrchunk,
                            receivers=receivers,
                            sidebands=sidebands,
                            mir_select_where=mir_select_where,
                            apply_tsys=apply_tsys,
                            apply_flags=apply_flags,
                            apply_dedoppler=apply_dedoppler,
                            pseudo_cont=pseudo_cont,
                            rechunk=rechunk,
                            recompute_nbls=recompute_nbls,
                            time_axis_faster_than_bls=time_axis_faster_than_bls,
                            blts_are_rectangular=blts_are_rectangular,
                        )
                    unread = False
                except KeyError as err:
                    file_warnings = (
                        file_warnings + f"Failed to read {f} due to KeyError: {err}\n"
                    )
                    file_num += 1
                    if skip_bad_files is False:
                        raise
                except ValueError as err:
                    file_warnings = (
                        file_warnings + f"Failed to read {f} due to ValueError: {err}\n"
                    )
                    file_num += 1
                    if skip_bad_files is False:
                        raise
                except OSError as err:  # pragma: nocover
                    file_warnings = (
                        file_warnings + f"Failed to read {f} due to OSError: {err}\n"
                    )
                    file_num += 1
                    if skip_bad_files is False:
                        raise

            if unread is True:
                warnings.warn(
                    "########################################################\n"
                    "ALL FILES FAILED ON READ - NO READABLE FILES IN FILENAME\n"
                    "########################################################"
                )
                return

            any_unprojected = np.any(~self._check_for_cat_type("unprojected"))
            phase_dict = None
            if allow_rephase is None and not make_multi_phase and any_unprojected:
                warnings.warn(
                    "The default behavior is to rephase data from the files to make "
                    "them have the same phasing. This is no longer needed as data with "
                    "different phase centers can now be combined on one object. "
                    "Starting in version 2.4, the data will be combined on one object "
                    "without rephasing. To get that behavior now, set "
                    "`allow_rephase=False`. The final object can be rephased if "
                    "desired by calling `phase` after this method.",
                    DeprecationWarning,
                )
                allow_rephase = True

            if (
                allow_rephase
                and phase_center_radec is None
                and not unphase_to_drift
                and any_unprojected
                and not make_multi_phase
            ):
                if self.Nphase > 1:
                    warnings.warn(
                        "Allow_rephase is True but `phase_center_radec` is not set and "
                        "the first file has multiple phase centers, so no rephasing "
                        "will be done."
                    )
                else:
                    phase_dict = list(self.phase_center_catalog.values())[0]

            uv_list = []
            if len(filename) > file_num + 1:
                for f in filename[file_num + 1 :]:
                    uv2 = UVData()
                    try:
                        with warnings.catch_warnings():
                            warnings.filterwarnings(
                                "ignore", "The `allow_rephase` option is deprecated"
                            )
                            warnings.filterwarnings(
                                "ignore", "The `make_multi_phase` option is deprecated"
                            )
                            warnings.filterwarnings(
                                "ignore",
                                "The `phase_center_radec` parameter is deprecated",
                            )
                            warnings.filterwarnings(
                                "ignore",
                                "The `unphase_to_drift` parameter is deprecated",
                            )
                            warnings.filterwarnings(
                                "ignore", "The `allow_rephase` option is deprecated"
                            )
                            uv2.read(
                                f,
                                file_type=file_type,
                                allow_rephase=allow_rephase,
                                read_data=read_data,
                                skip_bad_files=skip_bad_files,
                                background_lsts=background_lsts,
                                use_future_array_shapes=use_future_array_shapes,
                                # phasing parameters
                                fix_old_proj=fix_old_proj,
                                fix_use_ant_pos=fix_use_ant_pos,
                                make_multi_phase=make_multi_phase,
                                phase_center_radec=phase_center_radec,
                                phase_frame=phase_frame,
                                phase_epoch=phase_epoch,
                                phase_use_ant_pos=phase_use_ant_pos,
                                unphase_to_drift=unphase_to_drift,
                                # selecting parameters
                                antenna_nums=antenna_nums,
                                antenna_names=antenna_names,
                                ant_str=ant_str,
                                bls=bls,
                                catalog_names=catalog_names,
                                frequencies=frequencies,
                                freq_chans=freq_chans,
                                times=times,
                                time_range=time_range,
                                lsts=lsts,
                                lst_range=lst_range,
                                polarizations=polarizations,
                                blt_inds=blt_inds,
                                phase_center_ids=phase_center_ids,
                                keep_all_metadata=keep_all_metadata,
                                # checking parameters
                                run_check=run_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,
                                # file-type specific parameters
                                # miriad
                                phase_type=phase_type,
                                projected=projected,
                                correct_lat_lon=correct_lat_lon,
                                calc_lst=calc_lst,
                                # FHD
                                use_model=use_model,
                                # MS
                                data_column=data_column,
                                pol_order=pol_order,
                                ignore_single_chan=ignore_single_chan,
                                raise_error=raise_error,
                                read_weights=read_weights,
                                # MS & MIR
                                allow_flex_pol=allow_flex_pol,
                                # uvh5
                                multidim_index=multidim_index,
                                remove_flex_pol=remove_flex_pol,
                                blts_are_rectangular=blts_are_rectangular,
                                time_axis_faster_than_bls=time_axis_faster_than_bls,
                                recompute_nbls=recompute_nbls,
                                # uvh5 & mwa_corr_fits
                                data_array_dtype=data_array_dtype,
                                # mwa_corr_fits
                                use_aoflagger_flags=use_aoflagger_flags,
                                use_cotter_flags=use_cotter_flags,
                                remove_dig_gains=remove_dig_gains,
                                remove_coarse_band=remove_coarse_band,
                                correct_cable_len=correct_cable_len,
                                correct_van_vleck=correct_van_vleck,
                                cheby_approx=cheby_approx,
                                flag_small_auto_ants=flag_small_auto_ants,
                                flag_small_sig_ants=flag_small_sig_ants,
                                propagate_coarse_flags=propagate_coarse_flags,
                                flag_init=flag_init,
                                edge_width=edge_width,
                                start_flag=start_flag,
                                end_flag=end_flag,
                                flag_dc_offset=flag_dc_offset,
                                remove_flagged_ants=remove_flagged_ants,
                                phase_to_pointing_center=phase_to_pointing_center,
                                nsample_array_dtype=nsample_array_dtype,
                                # MIR
                                corrchunk=corrchunk,
                                pseudo_cont=pseudo_cont,
                                rechunk=rechunk,
                            )
                        if phase_dict is not None:
                            uv2.phase(
                                lon=phase_dict.get("cat_lon"),
                                lat=phase_dict.get("cat_lat"),
                                epoch=phase_dict.get("cat_epoch"),
                                phase_frame=phase_dict.get("cat_frame"),
                                cat_type=phase_dict.get("cat_type"),
                                ephem_times=phase_dict.get("cat_times"),
                                pm_ra=phase_dict.get("cat_pm_ra"),
                                pm_dec=phase_dict.get("cat_pm_dec"),
                                dist=phase_dict.get("cat_dist"),
                                vrad=phase_dict.get("cat_vrad"),
                                cat_name=phase_dict.get("cat_name"),
                                use_ant_pos=phase_use_ant_pos,
                                allow_rephase=allow_rephase,
                            )

                        uv_list.append(uv2)
                    except KeyError as err:
                        file_warnings = (
                            file_warnings
                            + f"Failed to read {f} due to KeyError: {err}\n"
                        )
                        if skip_bad_files:
                            continue
                        else:
                            raise
                    except ValueError as err:
                        file_warnings = (
                            file_warnings
                            + f"Failed to read {f} due to ValueError: {err}\n"
                        )
                        if skip_bad_files:
                            continue
                        else:
                            raise
                    except OSError as err:  # pragma: nocover
                        file_warnings = (
                            file_warnings
                            + f"Failed to read {f} due to OSError: {err}\n"
                        )
                        if skip_bad_files:
                            continue
                        else:
                            raise
            if len(file_warnings) > 0:
                warnings.warn(file_warnings)

            # Concatenate once at end
            if axis is not None:
                # Rewrote fast_concat to operate on lists
                self.fast_concat(
                    uv_list,
                    axis,
                    run_check=run_check,
                    check_extra=check_extra,
                    run_check_acceptability=run_check_acceptability,
                    inplace=True,
                    ignore_name=ignore_name,
                )
            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,
                            use_ant_pos=phase_use_ant_pos,
                            run_check=run_check,
                            check_extra=check_extra,
                            run_check_acceptability=run_check_acceptability,
                            ignore_name=ignore_name,
                        )
                    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 in ["fhd", "ms", "mwa_corr_fits"]:
                if (
                    antenna_nums is not None
                    or antenna_names is not None
                    or ant_str is not None
                    or bls is not None
                    or frequencies is not None
                    or freq_chans is not None
                    or times is not None
                    or lsts is not None
                    or time_range is not None
                    or lst_range is not None
                    or polarizations is not None
                    or blt_inds is not None
                    or phase_center_ids is not None
                ):
                    select = True
                    warnings.warn(
                        "Warning: select on read keyword set, but "
                        'file_type is "{ftype}" which does not support select '
                        "on read. Entire file will be read and then select "
                        "will be performed".format(ftype=file_type)
                    )
                    # these file types do not have select on read, so set all
                    # select parameters
                    select_antenna_nums = antenna_nums
                    select_antenna_names = antenna_names
                    select_ant_str = ant_str
                    select_bls = bls
                    select_frequencies = frequencies
                    select_freq_chans = freq_chans
                    select_times = times
                    select_lsts = lsts
                    select_time_range = time_range
                    select_lst_range = lst_range
                    select_polarizations = polarizations
                    select_blt_inds = blt_inds
                    select_phase_center_ids = phase_center_ids
                else:
                    select = False
            elif file_type in ["uvfits", "uvh5"]:
                select = False
            elif file_type in ["miriad"]:
                if (
                    antenna_names is not None
                    or frequencies is not None
                    or freq_chans is not None
                    or times is not None
                    or blt_inds is not None
                    or phase_center_ids is not None
                ):
                    if blt_inds is not None:
                        if (
                            antenna_nums is not None
                            or ant_str is not None
                            or bls is not None
                            or time_range is not None
                        ):
                            warnings.warn(
                                "Warning: blt_inds is set along with select "
                                "on read keywords that are supported by "
                                "read_miriad and may downselect blts. "
                                "This may result in incorrect results "
                                "because the select on read will happen "
                                "before the blt_inds selection so the indices "
                                "may not match the expected locations."
                            )
                    else:
                        warnings.warn(
                            "Warning: a select on read keyword is set that is "
                            "not supported by read_miriad. This select will "
                            "be done after reading the file."
                        )
                    select = True
                    # these are all done by partial read, so set to None
                    select_antenna_nums = None
                    select_ant_str = None
                    select_bls = None
                    select_time_range = None
                    select_polarizations = None

                    # these aren't supported by partial read, so do it in select
                    select_antenna_names = antenna_names
                    select_frequencies = frequencies
                    select_freq_chans = freq_chans
                    select_times = times
                    select_lsts = lsts
                    select_lst_range = lst_range
                    select_blt_inds = blt_inds
                    select_phase_center_ids = phase_center_ids
                else:
                    select = False
            elif file_type in ["mir"]:
                select = True
                # these are all done by partial read, so set to None
                select_antenna_nums = None
                select_antenna_names = None
                select_ant_str = None
                select_bls = None
                select_lst_range = None
                select_time_range = None
                select_polarizations = None

                # MIR can handle length-two bls tuples, so if any three element tuples
                # are seen, we need to deal w/ that via select.
                if bls is not None:
                    select_bls = bls if any(len(item) == 3 for item in bls) else None

                # these aren't supported by partial read, so do it in select
                select_frequencies = frequencies
                select_freq_chans = freq_chans
                select_blt_inds = blt_inds
                select_phase_center_ids = phase_center_ids
                select_times = times
                select_lsts = lsts

                if all(
                    item is None
                    for item in [
                        select_bls,
                        frequencies,
                        freq_chans,
                        blt_inds,
                        phase_center_ids,
                        times,
                        lsts,
                    ]
                ):
                    # If there's nothing to select, just bypass that operation.
                    select = False
                else:
                    warnings.warn(
                        "Warning: a select on read keyword is set that is "
                        "not supported by read_mir. This select will "
                        "be done after reading the file."
                    )
            # reading a single "file". Call the appropriate file-type read
            if file_type == "uvfits":
                self.read_uvfits(
                    filename,
                    antenna_nums=antenna_nums,
                    antenna_names=antenna_names,
                    ant_str=ant_str,
                    bls=bls,
                    frequencies=frequencies,
                    freq_chans=freq_chans,
                    times=times,
                    time_range=time_range,
                    lsts=lsts,
                    lst_range=lst_range,
                    polarizations=polarizations,
                    blt_inds=blt_inds,
                    phase_center_ids=phase_center_ids,
                    catalog_names=catalog_names,
                    read_data=read_data,
                    keep_all_metadata=keep_all_metadata,
                    background_lsts=background_lsts,
                    run_check=run_check,
                    check_extra=check_extra,
                    run_check_acceptability=run_check_acceptability,
                    strict_uvw_antpos_check=strict_uvw_antpos_check,
                    fix_old_proj=fix_old_proj,
                    fix_use_ant_pos=fix_use_ant_pos,
                    check_autos=check_autos,
                    fix_autos=fix_autos,
                    use_future_array_shapes=use_future_array_shapes,
                )

            elif file_type == "mir":
                self.read_mir(
                    filename,
                    antenna_nums=antenna_nums,
                    antenna_names=antenna_names,
                    bls=bls,
                    time_range=time_range,
                    lst_range=lst_range,
                    polarizations=polarizations,
                    catalog_names=catalog_names,
                    corrchunk=corrchunk,
                    receivers=receivers,
                    sidebands=sidebands,
                    select_where=mir_select_where,
                    apply_dedoppler=apply_dedoppler,
                    apply_tsys=apply_tsys,
                    apply_flags=apply_flags,
                    pseudo_cont=pseudo_cont,
                    rechunk=rechunk,
                    run_check=run_check,
                    check_extra=check_extra,
                    run_check_acceptability=run_check_acceptability,
                    strict_uvw_antpos_check=strict_uvw_antpos_check,
                    allow_flex_pol=allow_flex_pol,
                    check_autos=check_autos,
                    fix_autos=fix_autos,
                )
            elif file_type == "miriad":
                self.read_miriad(
                    filename,
                    antenna_nums=antenna_nums,
                    ant_str=ant_str,
                    bls=bls,
                    polarizations=polarizations,
                    time_range=time_range,
                    read_data=read_data,
                    phase_type=phase_type,
                    projected=projected,
                    correct_lat_lon=correct_lat_lon,
                    background_lsts=background_lsts,
                    run_check=run_check,
                    check_extra=check_extra,
                    run_check_acceptability=run_check_acceptability,
                    strict_uvw_antpos_check=strict_uvw_antpos_check,
                    calc_lst=calc_lst,
                    fix_old_proj=fix_old_proj,
                    fix_use_ant_pos=fix_use_ant_pos,
                    check_autos=check_autos,
                    fix_autos=fix_autos,
                    use_future_array_shapes=use_future_array_shapes,
                )

            elif file_type == "mwa_corr_fits":
                self.read_mwa_corr_fits(
                    filename,
                    use_aoflagger_flags=use_aoflagger_flags,
                    use_cotter_flags=use_cotter_flags,
                    remove_dig_gains=remove_dig_gains,
                    remove_coarse_band=remove_coarse_band,
                    correct_cable_len=correct_cable_len,
                    correct_van_vleck=correct_van_vleck,
                    cheby_approx=cheby_approx,
                    flag_small_auto_ants=flag_small_auto_ants,
                    flag_small_sig_ants=flag_small_sig_ants,
                    propagate_coarse_flags=propagate_coarse_flags,
                    flag_init=flag_init,
                    edge_width=edge_width,
                    start_flag=start_flag,
                    end_flag=end_flag,
                    flag_dc_offset=flag_dc_offset,
                    remove_flagged_ants=remove_flagged_ants,
                    phase_to_pointing_center=phase_to_pointing_center,
                    read_data=read_data,
                    data_array_dtype=data_array_dtype,
                    nsample_array_dtype=nsample_array_dtype,
                    background_lsts=background_lsts,
                    run_check=run_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,
                    use_future_array_shapes=use_future_array_shapes,
                )

            elif file_type == "fhd":
                self.read_fhd(
                    filename,
                    use_model=use_model,
                    background_lsts=background_lsts,
                    read_data=read_data,
                    run_check=run_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,
                    use_future_array_shapes=use_future_array_shapes,
                )

            elif file_type == "ms":
                self.read_ms(
                    filename,
                    data_column=data_column,
                    pol_order=pol_order,
                    background_lsts=background_lsts,
                    ignore_single_chan=ignore_single_chan,
                    raise_error=raise_error,
                    read_weights=read_weights,
                    allow_flex_pol=allow_flex_pol,
                    run_check=run_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,
                    use_future_array_shapes=use_future_array_shapes,
                )

            elif file_type == "uvh5":
                self.read_uvh5(
                    filename,
                    antenna_nums=antenna_nums,
                    antenna_names=antenna_names,
                    ant_str=ant_str,
                    bls=bls,
                    frequencies=frequencies,
                    freq_chans=freq_chans,
                    times=times,
                    time_range=time_range,
                    lsts=lsts,
                    lst_range=lst_range,
                    polarizations=polarizations,
                    blt_inds=blt_inds,
                    phase_center_ids=phase_center_ids,
                    catalog_names=catalog_names,
                    read_data=read_data,
                    data_array_dtype=data_array_dtype,
                    keep_all_metadata=keep_all_metadata,
                    multidim_index=multidim_index,
                    remove_flex_pol=remove_flex_pol,
                    background_lsts=background_lsts,
                    run_check=run_check,
                    check_extra=check_extra,
                    run_check_acceptability=run_check_acceptability,
                    strict_uvw_antpos_check=strict_uvw_antpos_check,
                    fix_old_proj=fix_old_proj,
                    fix_use_ant_pos=fix_use_ant_pos,
                    check_autos=check_autos,
                    fix_autos=fix_autos,
                    use_future_array_shapes=use_future_array_shapes,
                    time_axis_faster_than_bls=time_axis_faster_than_bls,
                    blts_are_rectangular=blts_are_rectangular,
                    recompute_nbls=recompute_nbls,
                )
                select = False

            if select:
                self.select(
                    antenna_nums=select_antenna_nums,
                    antenna_names=select_antenna_names,
                    ant_str=select_ant_str,
                    bls=select_bls,
                    frequencies=select_frequencies,
                    freq_chans=select_freq_chans,
                    times=select_times,
                    lsts=select_lsts,
                    time_range=select_time_range,
                    lst_range=select_lst_range,
                    polarizations=select_polarizations,
                    blt_inds=select_blt_inds,
                    phase_center_ids=select_phase_center_ids,
                    keep_all_metadata=keep_all_metadata,
                    run_check=run_check,
                    check_extra=check_extra,
                    run_check_acceptability=run_check_acceptability,
                    strict_uvw_antpos_check=strict_uvw_antpos_check,
                )

            self._deprecated_phase_helper(
                [],
                phase_center_radec,
                unphase_to_drift,
                phase_frame,
                orig_phase_frame,
                phase_use_ant_pos,
                None,
            )

    @classmethod
    def from_file(
        cls,
        filename,
        axis=None,
        file_type=None,
        read_data=True,
        skip_bad_files=False,
        background_lsts=True,
        ignore_name=False,
        use_future_array_shapes=False,
        # phasing parameters
        allow_rephase=None,
        phase_center_radec=None,
        unphase_to_drift=None,
        phase_frame=None,
        phase_epoch=None,
        orig_phase_frame=None,
        phase_use_ant_pos=None,
        fix_old_proj=None,
        fix_use_ant_pos=True,
        make_multi_phase=False,
        # selecting parameters
        antenna_nums=None,
        antenna_names=None,
        ant_str=None,
        bls=None,
        catalog_names=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,
        keep_all_metadata=True,
        # checking parameters
        run_check=True,
        check_extra=True,
        run_check_acceptability=True,
        strict_uvw_antpos_check=False,
        check_autos=True,
        fix_autos=True,
        # file-type specific parameters
        # miriad
        phase_type=None,
        projected=None,
        correct_lat_lon=True,
        calc_lst=True,
        # FHD
        use_model=False,
        # MS
        data_column="DATA",
        pol_order="AIPS",
        ignore_single_chan=True,
        raise_error=True,
        read_weights=True,
        # MS & MIR
        allow_flex_pol=True,
        # uvh5
        multidim_index=False,
        remove_flex_pol=True,
        blt_order=None,
        time_axis_faster_than_bls=None,
        blts_are_rectangular=None,
        recompute_nbls: bool | None = None,
        # uvh5 & mwa_corr_fits
        data_array_dtype=np.complex128,
        # mwa_corr_fits
        use_aoflagger_flags=None,
        use_cotter_flags=None,
        remove_dig_gains=True,
        remove_coarse_band=True,
        correct_cable_len=None,
        correct_van_vleck=False,
        cheby_approx=True,
        flag_small_auto_ants=True,
        flag_small_sig_ants=None,
        propagate_coarse_flags=True,
        flag_init=True,
        edge_width=80e3,
        start_flag="goodtime",
        end_flag=0.0,
        flag_dc_offset=True,
        remove_flagged_ants=True,
        phase_to_pointing_center=False,
        nsample_array_dtype=np.float32,
        # MIR
        corrchunk=None,
        receivers=None,
        sidebands=None,
        mir_select_where=None,
        apply_tsys=True,
        apply_flags=True,
        apply_dedoppler=False,
        pseudo_cont=False,
        rechunk=None,
    ):
        """
        Initialize a new UVData 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 phasing, selecting on
        read (partial read) and 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 ['uvfits', 'miriad', 'ms', 'uvh5', 'fhd', 'mwa_corr_fits', 'mir']
            or None. If None, the code attempts to guess what the file type is.
            For miriad and ms types, this is based on the standard directory
            structure. For FHD, uvfits and uvh5 files it's based on file
            extensions (FHD: .sav, .txt; uvfits: .uvfits; uvh5: .uvh5).
            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: 'blt', 'freq', 'polarization'. Only used if
            multiple files are passed.
        read_data : bool
            Read in the data. Not used if file_type is 'ms' or 'mir'.
            If set to False, only the metadata will be read in. Setting read_data to
            False results in a metdata only object.
        skip_bad_files : bool
            Option when reading multiple files to catch read errors such that
            the read continues even if one or more files are corrupted. Files
            that produce errors will be printed. Default is False (files will
            not be skipped).
        background_lsts : bool
            When set to True, the lst_array is calculated in a background thread.
        ignore_name : bool
            Only relevant when reading in multiple files, which are concatenated into a
            single UVData object. Option to ignore the name of the phase center when
            combining multiple files, which would otherwise result in an error being
            raised because of attributes not matching. Doing so effectively adopts the
            name found in the first file read in. Default is False.
        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.

        Phasing
        -------
        allow_rephase :  bool
            Deprecated, will have no effect starting in version 2.4 and will be removed
            in version 2.6 because data with different phasing can now be combined.
            Allow rephasing of phased file data. Currently defaults to True, which will
            cause data to be rephased (to preserve the previous behavior). Set to False
            to prevent rephasing and mimic the future behavior.
        phase_center_radec : array_like of float
            Deprecated and will be removed in version 2.4, instead call `phase` on the
            combined object after this method. The phase center to phase the files to
            before combining the objects in radians (in the ICRS frame). If set to None
            and allow_rephase is True and multiple files are read with different phase
            centers, the phase center of the first file will be used.
        unphase_to_drift : bool
            Deprecated, instead call `unproject_phase` on the combined object after
            this method. Unproject the data from the files before combining them.
        phase_frame : str
            The astropy frame to phase to.Only used if `phase_center_radec` is set.
            Deprecated and will be removed in version 2.4 with `phase_center_radec`.
        phase_epoch : astropy.time.Time object or str
            The epoch to use for phasing. Either an astropy Time object or the
            string "J2000" (which is the default).
            Note that the epoch is only used to evaluate the ra & dec values,
            if the epoch is not J2000, the ra & dec values are interpreted
            as FK5 ra/dec values and transformed to J2000, the data are then
            phased to the J2000 ra/dec values.
            Deprecated and will be removed in version 2.4 with `phase_center_radec`.
        orig_phase_frame : str
            Deprecated, has no effect. Will be removed in version 2.4.
        phase_use_ant_pos : bool
            If True, calculate the uvws directly from the antenna positions rather than
            from the existing uvws. Only used if `unphase_to_drift` or
            `phase_center_radec` are set. Deprecated and will be removed in version 2.4
            with `unphase_to_drift` and `phase_center_radec`.
        fix_old_proj : bool
            Applies a fix to uvw-coordinates and phasing, assuming that the old `phase`
            method was used prior to writing the data, which had errors of the order of
            one part in 1e4 - 1e5. See the phasing memo for more details. Default is
            False, unless reading a UVH5 file that is missing the `phase_center_app_ra`
            and `phase_center_app_dec` attributes (as these were introduced at the same
            time as the new `phase` method), in which case the default is True.
        fix_use_ant_pos : bool
            If setting `fix_old_proj` to True, use the antenna positions to derive the
            correct uvw-coordinates rather than using the baseline vectors. Default is
            True.
        make_multi_phase : bool
            Deprecated, will be removed in version 2.4. Since all objects now support
            multiple phase centers, this has no effect if set to False. The only effect
            of setting this to True is to prevent rephasing. A better approach is to set
            `allow_rephase=False`.

        Selecting
        ---------
        antenna_nums : array_like of int, optional
            The antennas numbers to include when reading data into 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.
        antenna_names : array_like of str, optional
            The antennas names to include when reading data into 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_nums` is also provided.
        ant_str : str, optional
            A string containing information about what antenna numbers
            and polarizations to include when reading 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 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.
        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 include when reading data into 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.
        frequencies : array_like of float, optional
            The frequencies to include when reading data into the object, each
            value passed here should exist in the freq_array.
        freq_chans : array_like of int, optional
            The frequency channel numbers to include when reading data into the
            object. Ignored if read_data is False.
        times : array_like of float, optional
            The times to include when reading data into the object, each value
            passed here should exist in the time_array in the file.
            Cannot be used with `time_range`.
        time_range : array_like of float, optional
            The time range in Julian Date to include when reading data into
            the object, must be length 2. Some of the times in the file 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 numbers to include when reading data into the
            object, each value passed here should exist in the polarization_array.
        blt_inds : array_like of int, optional
            The baseline-time indices to include when reading data into the
            object. 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, optional
            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.
        keep_all_metadata : bool
            Option to keep all the metadata associated with antennas, even those
            that do not have data associated with them after the select option.

        Checking
        --------
        run_check : bool
            Option to check for the existence and proper shapes of parameters
            after after reading in the file (the default is True,
            meaning the check will be run). Ignored if read_data is False.
        check_extra : bool
            Option to check optional parameters as well as required ones (the
            default is True, meaning the optional parameters will be checked).
            Ignored if read_data is False.
        run_check_acceptability : bool
            Option to check acceptable range of the values of parameters after
            reading in the file (the default is True, meaning the acceptable
            range check will be done). Ignored if read_data is False.
        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 True.

        Miriad
        ------
        phase_type : str, optional
            Deprecated, use the `projected` parameter insted. Option to specify the
            phasing status of the data. Options are 'drift', 'phased' or None. 'drift'
            is the same as setting the projected parameter to False, 'phased' is the
            same as setting the projected parameter to True. Ignored if `projected`
            is set.
        projected : bool or None
            Option to force the dataset to be labelled as projected or unprojected
            regardless of the evidence in the file. The default is None which means that
            the projection will be set based on the file contents. Be careful setting
            this keyword unless you are confident about the contents of the file.
        correct_lat_lon : bool
            Option to update the latitude and longitude from the known_telescopes
            list if the altitude is missing.
        calc_lst : bool
            Recalculate the LST values that are present within the file, useful in
            cases where the "online" calculate values have precision or value errors.
            Default is True.

        FHD
        ---
        use_model : bool
            Option to read in the model visibilities rather than the dirty
            visibilities (the default is False, meaning the dirty visibilities
            will be read).

        MS
        --
        data_column : str
            name of CASA data column to read into data_array. Options are:
            'DATA', 'MODEL', or 'CORRECTED_DATA'.
        pol_order : str
            Option to specify polarizations order convention, options are
            'CASA' or 'AIPS'.
        ignore_single_chan : bool
            Option to ignore single channel spectral windows in measurement sets to
            limit object size. Some measurement sets (e.g., those from ALMA) use single
            channel spectral windows for recording pseudo-continuum channels or storing
            other metadata in the track when the telescopes are not on source. Because
            of the way the object is strutured (where all spectral windows are assumed
            to be simultaneously recorded), this can significantly inflate the size and
            memory footprint of UVData objects. By default, single channel windows are
            ignored to avoid this issue, they can be included by setting this parameter
            to True.
        raise_error : bool
            The measurement set format allows for different spectral windows and
            polarizations to have different metdata for the same time-baseline
            combination, but UVData objects do not. It also allows for timescales that
            are not supported by astropy. If any of these problems are detected, by
            default the reader will throw an error. However, if set to False, the reader
            will simply give a warning and try to do the best it can. If the problem is
            with differing metadata, it will use the first value read in the file as the
            "correct" metadata in the UVData object. If the problem is with the
            timescale, it will just assume UTC.
        read_weights : bool
            Read in the weights from the MS file. If false, the method
            will set the `nsamples_array` to the same uniform value (namely 1.0).
        allow_flex_pol : bool
            If only one polarization per spectral window is read (and the polarization
            differs from window to window), allow for the `UVData` object to use
            "flexible polarization", which compresses the polarization-axis of various
            attributes to be of length 1, sets the `flex_spw_polarization_array`
            attribute to define the polarization per spectral window. Default is True.

        UVH5
        ----
        multidim_index : bool
            If True, attempt to index the HDF5 dataset simultaneously along all data
            axes. Otherwise index one axis at-a-time. This only works if data selection
            is sliceable along all but one axis. If indices are not well-matched to
            data chunks, this can be slow.
        remove_flex_pol : bool
            If True and if the file is a flex_pol file, convert back to a standard
            UVData object.
        data_array_dtype : numpy dtype
            Datatype to store the output data_array as. Must be either
            np.complex64 (single-precision real and imaginary) or np.complex128 (double-
            precision real and imaginary). Only used if the datatype of the visibility
            data on-disk is not 'c8' or 'c16'.
        blt_order : tuple of str or "determine", optional
            The order of the baseline-time axis *in the file*. This can be determined,
            or read directly from file, however since it has been optional in the past,
            many existing files do not contain it in the metadata.
            Some reading operations are significantly faster if this is known, so
            providing it here can provide a speedup. Default is to try and read it from
            file, and if not there, just leave it as None. Set to "determine" to
            auto-detect the blt_order from the metadata (takes extra time to do so).
        blts_are_rectangular : bool, optional
            Whether the baseline-time axis is rectangular. This can be read from
            metadata in new files, but many old files do not contain it. If not
            provided, the rectangularity will be determined from the data. This is a
            non-negligible operation, so if you know it, it can be provided here to
            speed up reading.
        time_axis_faster_than_bls : bool, optional
            If blts are rectangular, this variable specifies whether the time axis is
            the fastest-moving virtual axis. Various reading functions benefit from
            knowing this, so if it is known, it can be provided here to speed up
            reading. It will be determined from the data if not provided.
        recompute_nbls : bool, optional
            Whether to recompute the number of unique baselines from the data. Before
            v1.2 of the UVH5 spec, it was possible to have an incorrect number of
            baselines in the header without error, so this provides an opportunity to
            rectify it. Old HERA files (< March 2023) may have this issue, but in this
            case the correct number of baselines can be computed more quickly than by
            fully re=computing, and so we do this.


        MWA FITS
        --------
        use_aoflagger_flags : bool
            Option to use aoflagger mwaf flag files. Defaults to true if aoflagger
            flag files are submitted.
        use_cotter_flags : bool
            Being replaced by use_aoflagger_flags and will be removed in v2.4.
        remove_dig_gains : bool
            Option to divide out digital gains.
        remove_coarse_band : bool
            Option to divide out coarse band shape.
        correct_cable_len : bool
            Flag to apply cable length correction.
        correct_van_vleck : bool
            Flag to apply a van vleck correction.
        cheby_approx : bool
            Option to implement the van vleck correction with a chebyshev polynomial
            approximation. Set to False to run the integral version of the correction.
            Only used if correct_van_vleck is True.
        flag_small_auto_ants : bool
            Option to completely flag any antenna for which the autocorrelation falls
            below a threshold found by the Van Vleck correction to indicate bad data.
            Specifically, the threshold used is 0.5 * integration_time * channel_width.
            If set to False, only the times and frequencies at which the auto is below
            the threshold will be flagged for the antenna.
            Only used if correct_van_vleck is True.
        flag_small_sig_ants : bool
            Deprecated, replaced by flag_small_auto_ants and will be removed in v2.4.
        propogate_coarse_flags : bool
            Option to propogate flags for missing coarse channel integrations
            across frequency.
        flag_init: bool
            Set to True in order to do routine flagging of coarse channel edges, start
            or end integrations, or the center fine channel of each coarse
            channel. See associated keywords below.
        edge_width: float
            Only used if flag_init is True. Set to the width to flag on the edge of
            each coarse channel, in hz. Errors if not equal to integer multiple of
            channel_width. Set to 0 for no edge flagging.
        start_flag: float or str
            Only used if flag_init is True. The number of seconds to flag at the
            beginning of the observation. Set to 0 for no flagging. Default is
            'goodtime', which uses information in the metafits file to determine
            the length of time that should be flagged. Errors if input is not a
            float or 'goodtime'. Errors if float input is not equal to an
            integer multiple of the integration time.
        end_flag: floats
            Only used if flag_init is True. Set to the number of seconds to flag at the
            end of the observation. Set to 0 for no flagging. Errors if not an integer
            multiple of the integration time.
        flag_dc_offset: bool
            Only used if flag_init is True. Set to True to flag the center fine channel
            of each coarse channel.
        remove_flagged_ants : bool
            Option to perform a select to remove antennas flagged in the metafits
            file. If correct_van_vleck and flag_small_auto_ants are both True then
            antennas flagged by the Van Vleck correction are also removed.
        phase_to_pointing_center : bool
            Flag to phase to the pointing center.  Cannot be set if phase_center_radec
            is set to a value.
        data_array_dtype : numpy dtype
            Datatype to store the output data_array as. Must be either
            np.complex64 (single-precision real and imaginary) or np.complex128 (double-
            precision real and imaginary).
        nsample_array_dtype : numpy dtype
            Datatype to store the output nsample_array as. Must be either
            np.float64 (double-precision), np.float32 (single-precision), or
            np.float16 (half-precision). Half-precision is only recommended for
            cases where no sampling or averaging of baselines will occur,
            because round-off errors can be quite large (~1e-3).

        MIR
        ---
        corrchunk : int or array-like of int
            Correlator "chunk" (spectral window) to include when reading data into the
            object, where 0 corresponds to the pseudo-continuum channel.
        receivers : str or array-like of str
            The names of the receivers ("230", "240", "345", "400") to include when
            reading data into the object.
        sidebands : str or array-like of str
            The names of the sidebands ("l" for lower, "u" for upper) to include when
            reading data into the object.
        mir_select_where : tuple or list of tuples, optional
            Argument to pass to the `MirParser.select` method, which will downselect
            which data is read into the object.
        apply_flags : bool
            If set to True, apply "wideband" flags to the visibilities, which are
            recorded by the realtime system to denote when data are expected to be bad
            (e.g., antennas not on source, dewar warm). Default it true.
        apply_tsys : bool
            If set to False, data are returned as correlation coefficients (normalized
            by the auto-correlations). Default is True, which instead scales the raw
            visibilities and forward-gain of the antenna to produce values in Jy
            (uncalibrated).
        apply_dedoppler : bool
            If set to True, data will be corrected for any doppler-tracking performed
            during observations, and brought into the topocentric rest frame (default
            for UVData objects). Default is False.
        allow_flex_pol : bool
            If only one polarization per spectral window is read (and the polarization
            differs from window to window), allow for the `UVData` object to use
            "flexible polarization", which compresses the polarization-axis of various
            attributes to be of length 1, sets the `flex_spw_polarization_array`
            attribute to define the polarization per spectral window. Default is True.

        Raises
        ------
        ValueError
            If the file_type is not set and cannot be determined from the file name.
            If incompatible select keywords are set (e.g. `ant_str` with other
            antenna selectors, `times` and `time_range`) or select keywords
            exclude all data or if keywords are set to the wrong type.
            If phase_center_radec is not None and is not length 2.

        """
        uvd = cls()
        uvd.read(
            filename,
            axis=axis,
            file_type=file_type,
            read_data=read_data,
            skip_bad_files=skip_bad_files,
            background_lsts=background_lsts,
            ignore_name=ignore_name,
            use_future_array_shapes=use_future_array_shapes,
            # phasing parameters
            allow_rephase=allow_rephase,
            phase_center_radec=phase_center_radec,
            unphase_to_drift=unphase_to_drift,
            phase_frame=phase_frame,
            phase_epoch=phase_epoch,
            orig_phase_frame=orig_phase_frame,
            phase_use_ant_pos=phase_use_ant_pos,
            fix_old_proj=fix_old_proj,
            fix_use_ant_pos=fix_use_ant_pos,
            make_multi_phase=make_multi_phase,
            # select parameters
            antenna_nums=antenna_nums,
            antenna_names=antenna_names,
            ant_str=ant_str,
            bls=bls,
            catalog_names=catalog_names,
            frequencies=frequencies,
            freq_chans=freq_chans,
            times=times,
            time_range=time_range,
            lsts=lsts,
            lst_range=lst_range,
            polarizations=polarizations,
            blt_inds=blt_inds,
            phase_center_ids=phase_center_ids,
            keep_all_metadata=keep_all_metadata,
            # checking parameters
            run_check=run_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,
            # start file-type specific parameters
            # miriad
            phase_type=phase_type,
            projected=projected,
            correct_lat_lon=correct_lat_lon,
            calc_lst=calc_lst,
            # FHD
            use_model=use_model,
            # MS
            data_column=data_column,
            pol_order=pol_order,
            ignore_single_chan=ignore_single_chan,
            raise_error=raise_error,
            read_weights=read_weights,
            # MS & MIR
            allow_flex_pol=allow_flex_pol,
            # uvh5
            multidim_index=multidim_index,
            remove_flex_pol=remove_flex_pol,
            # uvh5 & mwa_corr_fits
            data_array_dtype=data_array_dtype,
            nsample_array_dtype=nsample_array_dtype,
            blt_order=blt_order,
            time_axis_faster_than_bls=time_axis_faster_than_bls,
            blts_are_rectangular=blts_are_rectangular,
            recompute_nbls=recompute_nbls,
            # mwa_corr_fits
            use_aoflagger_flags=use_aoflagger_flags,
            use_cotter_flags=use_cotter_flags,
            remove_dig_gains=remove_dig_gains,
            remove_coarse_band=remove_coarse_band,
            correct_cable_len=correct_cable_len,
            correct_van_vleck=correct_van_vleck,
            cheby_approx=cheby_approx,
            flag_small_auto_ants=flag_small_auto_ants,
            flag_small_sig_ants=flag_small_sig_ants,
            propagate_coarse_flags=propagate_coarse_flags,
            flag_init=flag_init,
            edge_width=edge_width,
            start_flag=start_flag,
            end_flag=end_flag,
            flag_dc_offset=flag_dc_offset,
            remove_flagged_ants=remove_flagged_ants,
            phase_to_pointing_center=phase_to_pointing_center,
            # MIR
            corrchunk=corrchunk,
            receivers=receivers,
            sidebands=sidebands,
            mir_select_where=mir_select_where,
            apply_tsys=apply_tsys,
            apply_flags=apply_flags,
            apply_dedoppler=apply_dedoppler,
            pseudo_cont=pseudo_cont,
            rechunk=rechunk,
        )
        return uvd

    def write_miriad(
        self,
        filepath,
        clobber=False,
        run_check=True,
        check_extra=True,
        run_check_acceptability=True,
        strict_uvw_antpos_check=False,
        no_antnums=False,
        calc_lst=False,
        check_autos=True,
        fix_autos=False,
    ):
        """
        Write the data to a miriad file.

        Parameters
        ----------
        filename : str
            The miriad root directory to write to.
        clobber : bool
            Option to overwrite the filename if the file already exists.
        run_check : bool
            Option to check for the existence and proper shapes of parameters
            after before writing the file (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 before
            writing the file (the default is True, meaning the acceptable
            range check will be done).
        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.
        no_antnums : bool
            Option to not write the antnums variable to the file.
            Should only be used for testing purposes.
        calc_lst : bool
            Recalculate the LST values upon writing the file. This is done to perform
            higher-precision accounting for the difference in MIRAD timestamps vs
            pyuvdata (the former marks the beginning of an integration, the latter
            marks the midpoint). Default is False, which instead uses a simple formula
            for correcting the LSTs, expected to be accurate to approximately 0.1 µsec
            precision.
        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.

        Raises
        ------
        ValueError
            If the frequencies are not evenly spaced or are separated by more
            than their channel width or if the UVData object is a metadata only object.
        TypeError
            If any entry in extra_keywords is not a single string or number.

        """
        if self.metadata_only:
            raise ValueError("Cannot write out metadata only objects to a miriad file.")

        miriad_obj = self._convert_to_filetype("miriad")

        # If we have a flex-pol dataset, remove it so that we can pass the data to the
        # writer without issue. We create a copy because otherwise we run the risk of
        # messing w/ the metadata of the original object, and because the overhead of
        # doing so is generally smaller than that from removing flex-pol.
        # TODO: reconsider this. The overhead is not smaller if `convert_to_flex_pol`
        # was used.
        if self.flex_spw_polarization_array is not None:
            miriad_obj = miriad_obj.copy()
            miriad_obj.remove_flex_pol()

        miriad_obj.write_miriad(
            filepath,
            clobber=clobber,
            run_check=run_check,
            check_extra=check_extra,
            run_check_acceptability=run_check_acceptability,
            strict_uvw_antpos_check=strict_uvw_antpos_check,
            no_antnums=no_antnums,
            calc_lst=calc_lst,
            check_autos=check_autos,
            fix_autos=fix_autos,
        )
        del miriad_obj

    def write_mir(self, filepath):
        """
        Write the data to a mir file.

        Parameters
        ----------
        filename : str
            The mir root directory to write to.

        Raises
        ------
        ValueError
            If the UVData object is a metadata only object.
        NotImplementedError
            Method is not fully implemented yet, and thus will raise an error

        """
        if self.metadata_only:
            raise ValueError("Cannot write out metadata only objects to a mir file.")

        mir_obj = self._convert_to_filetype("mir")
        mir_obj.write_mir(filepath)
        del mir_obj  # pragma: nocover

    def write_ms(
        self,
        filename,
        force_phase=False,
        clobber=False,
        run_check=True,
        check_extra=True,
        run_check_acceptability=True,
        strict_uvw_antpos_check=False,
        check_autos=True,
        fix_autos=False,
    ):
        """
        Write a CASA measurement set (MS).

        Parameters
        ----------
        filename : str
            The measurement set file path to write to (a measurement set is really
            a folder with many files).
        force_phase : bool
            Option to automatically phase unprojected data to zenith of the first
            timestamp.
        clobber : bool
            Option to overwrite the file if it already exists.
        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.

        Raises
        ------
        ValueError
            If the UVData object is a metadata only object.

        """
        if self.metadata_only:
            raise ValueError(
                "Cannot write out metadata only objects to a measurement set file."
            )

        ms_obj = self._convert_to_filetype("ms")
        ms_obj.write_ms(
            filename,
            force_phase=force_phase,
            clobber=clobber,
            run_check=run_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,
        )
        del ms_obj

    def write_uvfits(
        self,
        filename,
        spoof_nonessential=False,
        write_lst=True,
        force_phase=False,
        run_check=True,
        check_extra=True,
        run_check_acceptability=True,
        strict_uvw_antpos_check=False,
        check_autos=True,
        fix_autos=False,
    ):
        """
        Write the data to a uvfits file.

        If using this method to write out a data set for import into CASA, users should
        be aware that the `importuvifts` task does not currently support reading in
        data sets where the number of antennas is > 255. If writing out such a data set
        for use in CASA, we suggest using the measurement set writer (`UVData.write_ms`)
        instead.

        Parameters
        ----------
        filename : str
            The uvfits file to write to.
        spoof_nonessential : bool
            Deprecated and has no effect. Values are automatically set to their best
            known values if not previously set.
        write_lst : bool
            Option to write the LSTs to the metadata (random group parameters).
        force_phase:  : bool
            Option to automatically phase unprojected data to zenith of the first
            timestamp.
        run_check : bool
            Option to check for the existence and proper shapes of parameters
            after before writing the file (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 before
            writing the file (the default is True, meaning the acceptable
            range check will be done).
        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.

        Raises
        ------
        ValueError
            Any blts are unprojected and `force_phase` keyword is not set.
            If the frequencies are not evenly spaced or are separated by more
            than their channel width.
            The polarization values are not evenly spaced.
            If the `timesys` parameter is not set to "UTC".
            If the UVData object is a metadata only object.
        TypeError
            If any entry in extra_keywords is not a single string or number.

        """
        if self.metadata_only:
            raise ValueError("Cannot write out metadata only objects to a uvfits file.")

        uvfits_obj = self._convert_to_filetype("uvfits")

        # If we have a flex-pol dataset, remove it so that we can pass the data to the
        # writer without issue. We create a copy because otherwise we run the risk of
        # messing w/ the metadata of the original object, and because the overhead of
        # doing so is generally smaller than that from removing flex-pol.
        # TODO: reconsider this. The overhead is not smaller if `convert_to_flex_pol`
        # was used.
        if self.flex_spw_polarization_array is not None:
            uvfits_obj = uvfits_obj.copy()
            uvfits_obj.remove_flex_pol()

        uvfits_obj.write_uvfits(
            filename,
            spoof_nonessential=spoof_nonessential,
            write_lst=write_lst,
            force_phase=force_phase,
            run_check=run_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,
        )
        del uvfits_obj

    def write_uvh5(
        self,
        filename,
        clobber=False,
        chunks=True,
        data_compression=None,
        flags_compression="lzf",
        nsample_compression="lzf",
        data_write_dtype=None,
        run_check=True,
        check_extra=True,
        run_check_acceptability=True,
        strict_uvw_antpos_check=False,
        check_autos=True,
        fix_autos=False,
    ):
        """
        Write a completely 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 "lzf"
            for the LZF filter. Dataset must be chunked.
        nsample_compression : str
            HDF5 filter to apply when writing the nsample_array. Default is "lzf"
            for the LZF filter. Dataset must be chunked.
        data_write_dtype : numpy dtype
            datatype of output visibility data. If 'None', then the same datatype
            as data_array will be used. 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
            after before writing the file (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 before
            writing the file (the default is True, meaning the acceptable
            range check will be done).
        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.

        Raises
        ------
        ValueError
            If the UVData object is a metadata only object.

        """
        if self.metadata_only:
            raise ValueError(
                "Cannot write out metadata only objects to a uvh5 file. To initialize "
                "a uvh5 file for partial writing, use the `initialize_uvh5_file` "
                "method."
            )

        uvh5_obj = self._convert_to_filetype("uvh5")
        uvh5_obj.write_uvh5(
            filename,
            clobber=clobber,
            chunks=chunks,
            data_compression=data_compression,
            flags_compression=flags_compression,
            nsample_compression=nsample_compression,
            data_write_dtype=data_write_dtype,
            run_check=run_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,
        )
        del uvh5_obj

    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 with the header metadata and empty data arrays.

        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 "lzf"
            for the LZF filter. Dataset must be chunked.
        nsample_compression : str
            HDF5 filter to apply when writing the nsample_array. Default is "lzf"
            for the LZF filter. Dataset must be chunked.
        data_write_dtype : numpy dtype
            datatype of output visibility data. If 'None', then the same datatype
            as data_array will be used. 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.

        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.

        """
        uvh5_obj = self._convert_to_filetype("uvh5")
        uvh5_obj.initialize_uvh5_file(
            filename,
            clobber=clobber,
            chunks=chunks,
            data_compression=data_compression,
            flags_compression=flags_compression,
            nsample_compression=nsample_compression,
            data_write_dtype=data_write_dtype,
        )
        del uvh5_obj

    def write_uvh5_part(
        self,
        filename,
        data_array,
        flags_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,
        add_to_history=None,
        run_check_acceptability=True,
        fix_autos=False,
    ):
        """
        Write data to a UVH5 file that has already been initialized.

        Parameters
        ----------
        filename : str
            The UVH5 file to write to. It must already exist, and is assumed to
            have been initialized with initialize_uvh5_file.
        data_array : ndarray
            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.
        flags_array : ndarray
            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 : ndarray
            The nsample 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.
        check_header : bool
            Option to check that the metadata present in the header on disk
            matches that in the object.
        antenna_nums : array_like of int, optional
            The antennas numbers to include when writing data into the file
            (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 include when writing data into the file
            (antenna positions and names for the removed antennas will be retained).
            This cannot be provided if `antenna_nums` 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 include when writing data into 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, `polarizations` must be
            None.
        ant_str : str, optional
            A string containing information about what antenna numbers
            and polarizations to include writing data into the file.
            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 include when writing data into the file, each
            value passed here should exist in the freq_array.
        freq_chans : array_like of int, optional
            The frequency channel numbers to include writing data into the file.
        times : array_like of float, optional
            The times to include when writing data into the file, each value
            passed here should exist in the time_array.
        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 numbers to include when writing data into the file,
            each value passed here should exist in the polarization_array.
        blt_inds : array_like of int, optional
            The baseline-time indices to include when writing data into the file.
            This is not commonly used.
        phase_center_ids : array_like of int, optional
            Phase center IDs to include when writing data into the file (effectively
            a selection on baseline-times). Cannot be used with catalog_names.
        catalog_names : str or array-like of str, optional
            The names of the phase centers (sources) to include when writing data to
            the file, 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.
        run_check_acceptability : bool
            Option to check acceptable range of the values of parameters before
            writing the file (the default is True, meaning the acceptable
            range check will be done).
        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.
        fix_autos : bool
            Force the auto-correlations to be real-only values in data_array.
            Default is False.

        """
        if fix_autos:
            self._fix_autos()

        uvh5_obj = self._convert_to_filetype("uvh5")
        uvh5_obj.write_uvh5_part(
            filename,
            data_array,
            flags_array,
            nsample_array,
            check_header=check_header,
            antenna_nums=antenna_nums,
            antenna_names=antenna_names,
            bls=bls,
            ant_str=ant_str,
            frequencies=frequencies,
            freq_chans=freq_chans,
            times=times,
            time_range=time_range,
            lsts=lsts,
            lst_range=lst_range,
            polarizations=polarizations,
            blt_inds=blt_inds,
            phase_center_ids=phase_center_ids,
            catalog_names=catalog_names,
            add_to_history=add_to_history,
            run_check_acceptability=run_check_acceptability,
        )
        del uvh5_obj

    def normalize_by_autos(self, skip_autos=True, invert=False):
        """
        Normalize cross-correlations by auto-correlation data.

        Normalizes the cross-correlations by the geometric mean of the autocorrelations
        that make up the antenna pair for a given baseline. Useful for converting
        arbitrarily-scaled data into correlation coefficients (which can sometimes be
        more readily converted to a flux scale).

        Parameters
        ----------
        skip_autos : bool
            If set to True, the method will skip over records which correspond to the
            auto-correlations (which would otherwise simply be equal to all ones), which
            can be useful if intending to undo or redo the normalization at some point
            later. Default is True.
        invert : bool
            If set to True, will multiply by the geometric mean of the autos instead of
            dividing by it. Useful for undoing previous normalizations (which also
            requires setting `skip_autos=True` on previous calls).
        """
        # First up, let's double-check to make sure that we can actually normalize this
        # data set, and if not, bail early.
        if not np.any(self.ant_1_array == self.ant_2_array):
            raise ValueError(
                "No autos available in this data set to do normalization with."
            )

        # Now figure out how the different polarizations map to the autos
        pol_groups = []
        pol_list = list(self.polarization_array)
        for pol in pol_list:
            try:
                feed_pols = uvutils.POL_TO_FEED_DICT[uvutils.POL_NUM2STR_DICT[pol]]
                pol_groups.append(
                    [
                        pol_list.index(uvutils.POL_STR2NUM_DICT[item + item])
                        for item in feed_pols
                    ]
                )
            except KeyError:
                # If we run into a key error, it means that one of the dicts above does
                # not have a match to the given polarization, in which case assume that
                # this pol its it's own auto.
                pol_groups.append([pol_list.index(pol)] * 2)
            except ValueError as err:
                # If we have an value error, it means that the pol that _would_ be the
                # auto is not found in the data, in which case we  throw an error.
                raise ValueError(
                    "Cannot normalize {pol}, matching pols for autos not found.".format(
                        pol=uvutils.POL_NUM2STR_DICT[pol]
                    )
                ) from err

        # Each pol group contains the index positions for the "auto" polarizations,
        # so we can grab all of the auto pols by searching for the unique values
        auto_pols = list(np.unique(pol_groups))

        # Grab references to data and flags, to manipulate later
        data_arr = self.data_array
        flag_arr = self.flag_array
        if not self.future_array_shapes:
            data_arr = np.squeeze(data_arr, 1)
            flag_arr = np.squeeze(flag_arr, 1)

        # We need to match baselines in a single integration, so figure out how to
        # group the data by time.
        time_ordered = not np.any(self.time_array[:-1] > self.time_array[1:])
        blt_idx = np.arange(self.Nblts) if time_ordered else np.argsort(self.time_array)
        ordered_time = self.time_array[blt_idx if time_ordered else ...]

        # Normally time has a fixed atol, but verify that this is the case
        time_tol = self._time_array.tols[1]
        assert self._time_array.tols[0] == 0

        # Start searching through time, keeping in mind that the data are time ordered.
        time_groups = []
        start_idx = end_idx = 0
        while self.Nblts != start_idx:
            # Search sorted will find where one would insert the value of the current
            # time + the time tol, which will give us the ending index of the group.
            end_idx += np.searchsorted(
                ordered_time[start_idx:], ordered_time[start_idx] + time_tol, "right"
            )
            # Create this new grouping of blts
            time_groups.append(blt_idx[start_idx:end_idx])
            start_idx = end_idx

        # Not start the heavy lifting
        for group in time_groups:
            # Now start going through the groups
            ant_1_arr = self.ant_1_array[group]
            ant_2_arr = self.ant_2_array[group]
            norm_dict = {}
            flag_dict = {}
            for grp_idx, ant1, ant2 in zip(group, ant_1_arr, ant_2_arr):
                # Tabulate up front the normalization for each auto-correlation
                # spectrum, which will save some work downstream
                if ant1 != ant2:
                    continue
                norm_dict[ant1] = {}
                flag_dict[ant1] = {}
                for pol in auto_pols:
                    # Autos _should_ be real only, extracting them out like this will
                    # make the multipliction later a bit faster.
                    auto_data = data_arr[grp_idx, :, pol].real
                    auto_flag = flag_arr[grp_idx, :, pol] | ~(auto_data > 0)
                    norm_data = np.zeros_like(auto_data)
                    if invert:
                        norm_data = np.sqrt(auto_data, where=~auto_flag, out=norm_data)
                    else:
                        norm_data = np.reciprocal(
                            auto_data, where=~auto_flag, out=norm_data
                        )
                        norm_data = np.sqrt(norm_data, out=norm_data)
                    norm_dict[ant1][pol] = norm_data
                    flag_dict[ant1][pol] = auto_flag

            # Now that we have the autos "normalization-ready", we can get to
            # actually normalizing the crosses.
            for grp_idx, ant1, ant2 in zip(group, ant_1_arr, ant_2_arr):
                try:
                    for jdx, (pol1, pol2) in enumerate(pol_groups):
                        # Proceed pol by pol
                        if skip_autos and (ant1 == ant2) and (pol1 == pol2):
                            continue
                        data_arr[grp_idx, :, jdx] *= (
                            norm_dict[ant1][pol1] * norm_dict[ant2][pol2]
                        )
                        flag_arr[grp_idx, :, jdx] |= (
                            flag_dict[ant1][pol1] | flag_dict[ant2][pol2]
                        )
                except KeyError:
                    # If no data found for this antenna, then flag the whole blt
                    flag_arr[grp_idx] = True


# pyuvdata/uvdata/uvfits.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 uvfits files."""
import copy
import os
import warnings

import numpy as np
from astropy import constants as const
from astropy.io import fits
from astropy.time import Time

from .. import utils as uvutils
from .uvdata import UVData, _future_array_shapes_warning

__all__ = ["UVFITS"]


class UVFITS(UVData):
    """
    Defines a uvfits-specific subclass of UVData for reading and writing uvfits.

    This class should not be interacted with directly, instead use the read_uvfits
    and write_uvfits methods on the UVData class.
    """

    def _get_parameter_data(
        self, vis_hdu, read_source, run_check_acceptability, background_lsts=True
    ):
        """
        Read just the random parameters portion of the uvfits file ("metadata").

        Separated from full read so that header, metadata and data can be read
        independently.
        """
        # astropy.io fits reader scales date according to relevant PZER0 (?)
        # uvfits standard is to have 2 DATE parameters, both floats:
        # DATE (full day) and _DATE (fractional day)
        # cotter uvfits files have one DATE that is a double
        # using data.par('date') is general -- it will add them together if there are 2
        self.time_array = vis_hdu.data.par("date")

        self.Ntimes = len(np.unique(self.time_array))

        # check if lst array is saved. It's not a standard metadata item in uvfits,
        # but if the file was written with pyuvdata it may be present
        # (depending on pyuvdata version)
        proc = None
        if "LST" in vis_hdu.data.parnames:
            # angles in uvfits files are stored in degrees, so convert to radians
            self.lst_array = np.deg2rad(vis_hdu.data.par("lst"))
            if run_check_acceptability:
                (
                    latitude,
                    longitude,
                    altitude,
                ) = self.telescope_location_lat_lon_alt_degrees
                lst_array = uvutils.get_lst_for_time(
                    self.time_array, latitude, longitude, altitude
                )
                if not np.all(
                    np.isclose(
                        self.lst_array,
                        lst_array,
                        rtol=self._lst_array.tols[0],
                        atol=self._lst_array.tols[1],
                    )
                ):
                    warnings.warn(
                        "LST values stored in this file are not "
                        "self-consistent with time_array and telescope "
                        "location. Consider recomputing with "
                        "utils.get_lst_for_time."
                    )

        else:
            proc = self.set_lsts_from_time_array(background=background_lsts)

        # if antenna arrays are present, use them. otherwise use baseline array
        if "ANTENNA1" in vis_hdu.data.parnames and "ANTENNA2" in vis_hdu.data.parnames:
            # Note: we no longer subtract 1 from the antenna arrays
            # The antanna arrays are not indices but rather are numbers
            # that correspond to particular antennas
            self.ant_1_array = np.int32(vis_hdu.data.par("ANTENNA1"))
            self.ant_2_array = np.int32(vis_hdu.data.par("ANTENNA2"))
            # for instruments with a single subarray, the subarray should be set to 1s
            subarray = np.int32(vis_hdu.data.par("SUBARRAY"))
            # error on files with multiple subarrays
            if len(set(subarray)) > 1:
                raise ValueError(
                    "This file appears to have multiple subarray "
                    "values; only files with one subarray are "
                    "supported."
                )
        else:
            # cannot set this to be the baseline array because it uses the
            # 256 convention, not our 2048 convention
            bl_input_array = np.int64(vis_hdu.data.par("BASELINE"))

            # get antenna arrays based on uvfits baseline array
            self.ant_1_array, self.ant_2_array = self.baseline_to_antnums(
                bl_input_array
            )

        if read_source:
            source = vis_hdu.data.par("SOURCE")
            self.phase_center_id_array = source.astype(int)

        # get self.baseline_array using our convention
        self.baseline_array = self.antnums_to_baseline(
            self.ant_1_array, self.ant_2_array
        )
        self.Nbls = len(np.unique(self.baseline_array))

        # initialize internal variables based on the antenna lists
        self.Nants_data = int(np.union1d(self.ant_1_array, self.ant_2_array).size)

        # check for suffixes in the baseline coordinate names indicating the
        # baseline coordinate system
        if (
            "UU" in vis_hdu.data.parnames
            and "VV" in vis_hdu.data.parnames
            and "WW" in vis_hdu.data.parnames
        ):
            uvw_names = ["UU", "VV", "WW"]
        elif (
            "UU---SIN" in vis_hdu.data.parnames
            and "VV---SIN" in vis_hdu.data.parnames
            and "WW---SIN" in vis_hdu.data.parnames
        ):
            uvw_names = ["UU---SIN", "VV---SIN", "WW---SIN"]
        elif (
            "UU---NCP" in vis_hdu.data.parnames
            and "VV---NCP" in vis_hdu.data.parnames
            and "WW---NCP" in vis_hdu.data.parnames
        ):
            uvw_names = ["UU---NCP", "VV---NCP", "WW---NCP"]
            warnings.warn(
                "The baseline coordinates (uvws) in this file are specified in the "
                "---NCP coordinate system, which is does not agree with our baseline "
                "coordinate conventions. Rotating the uvws to match our convention "
                "(Note that this rotation has not been widely tested)."
            )
        else:
            raise ValueError(
                "There is no consistent set of baseline coordinates in this file. "
                "The UU, VV and WW coordinate must have no suffix or the '---SIN' or "
                "'---NCP' suffix and the suffixes must match on all three baseline "
                "coordinate parameters."
            )

        # read baseline vectors in units of seconds, return in meters
        # FITS uvw direction convention is opposite ours and Miriad's.
        # So conjugate the visibilities and flip the uvws:
        # uvfits files often have uvws in single precision rather than double precision.
        # setting the dtype below enforces double precision
        self.uvw_array = (-1) * (
            np.array(
                np.stack(
                    (
                        vis_hdu.data.par(uvw_names[0]),
                        vis_hdu.data.par(uvw_names[1]),
                        vis_hdu.data.par(uvw_names[2]),
                    )
                ),
                dtype=self._uvw_array.expected_type,
            )
            * const.c.to("m/s").value
        ).T

        if "INTTIM" in vis_hdu.data.parnames:
            self.integration_time = np.asarray(
                vis_hdu.data.par("INTTIM"), dtype=np.float64
            )
        else:
            if self.Ntimes > 1:
                # assume that all integration times in the file are the same
                int_time = self._calc_single_integration_time()
                self.integration_time = (
                    np.ones_like(self.time_array, dtype=np.float64) * int_time
                )
            else:
                warnings.warn(
                    "The integration time is not specified and only one time is "
                    "present so it cannot be calculated from the difference between "
                    "integration times. Setting to None which will cause the check to "
                    "error. Set `run_check` to False to read in the file without "
                    "checking. Then set the integration_time (to an array of length "
                    "Nblts) directly on the object to allow futher processing."
                )

        if proc is not None:
            proc.join()

    def _get_data(
        self,
        vis_hdu,
        antenna_nums,
        antenna_names,
        ant_str,
        bls,
        frequencies,
        freq_chans,
        times,
        time_range,
        lsts,
        lst_range,
        polarizations,
        blt_inds,
        phase_center_ids,
        catalog_names,
        keep_all_metadata,
        fix_old_proj,
        fix_use_ant_pos,
    ):
        """
        Read just the visibility and flag data of the uvfits file.

        Separated from full read so header and metadata can be read without data.
        """
        # figure out what data to read in
        blt_inds, freq_inds, pol_inds, history_update_string = 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,
        )

        if blt_inds is not None:
            blt_frac = len(blt_inds) / float(self.Nblts)
        else:
            blt_frac = 1

        if freq_inds is not None:
            freq_frac = len(freq_inds) * float(self.Nspws) / float(self.Nfreqs)
        else:
            freq_frac = 1

        if pol_inds is not None:
            pol_frac = len(pol_inds) / float(self.Npols)
        else:
            pol_frac = 1

        min_frac = np.min([blt_frac, freq_frac, pol_frac])

        if min_frac == 1:
            # no select, read in all the data
            if vis_hdu.header["NAXIS"] == 7:
                raw_data_array = vis_hdu.data.data[:, 0, 0, :, :, :, :]
                assert self.Nspws == raw_data_array.shape[1]

            else:
                # in many uvfits files the spw axis is left out,
                # here we put it back in so the dimensionality stays the same
                raw_data_array = vis_hdu.data.data[:, 0, 0, :, :, :]
                raw_data_array = raw_data_array[:, np.newaxis, :, :]
        else:
            # do select operations on everything except data_array, flag_array
            # and nsample_array
            self._select_by_index(
                blt_inds, freq_inds, pol_inds, history_update_string, keep_all_metadata
            )

            # just read in the right portions of the data and flag arrays
            if blt_frac == min_frac:
                if vis_hdu.header["NAXIS"] == 7:
                    raw_data_array = vis_hdu.data.data[blt_inds, :, :, :, :, :, :]
                    raw_data_array = raw_data_array[:, 0, 0, :, :, :, :]
                    assert self.Nspws == raw_data_array.shape[1]
                else:
                    # in many uvfits files the spw axis is left out,
                    # here we put it back in so the dimensionality stays the same
                    raw_data_array = vis_hdu.data.data[blt_inds, :, :, :, :, :]
                    raw_data_array = raw_data_array[:, 0, 0, :, :, :]
                    raw_data_array = raw_data_array[:, np.newaxis, :, :, :]
                if freq_frac < 1:
                    raw_data_array = raw_data_array[:, :, freq_inds, :, :]
                if pol_frac < 1:
                    raw_data_array = raw_data_array[:, :, :, pol_inds, :]
            elif freq_frac == min_frac:
                if vis_hdu.header["NAXIS"] == 7:
                    raw_data_array = vis_hdu.data.data[:, :, :, :, freq_inds, :, :]
                    raw_data_array = raw_data_array[:, 0, 0, :, :, :, :]
                    assert self.Nspws == raw_data_array.shape[1]
                else:
                    # in many uvfits files the spw axis is left out,
                    # here we put it back in so the dimensionality stays the same
                    raw_data_array = vis_hdu.data.data[:, :, :, freq_inds, :, :]
                    raw_data_array = raw_data_array[:, 0, 0, :, :, :]
                    raw_data_array = raw_data_array[:, np.newaxis, :, :, :]

                if blt_frac < 1:
                    raw_data_array = raw_data_array[blt_inds, :, :, :, :]
                if pol_frac < 1:
                    raw_data_array = raw_data_array[:, :, :, pol_inds, :]
            else:
                if vis_hdu.header["NAXIS"] == 7:
                    raw_data_array = vis_hdu.data.data[:, :, :, :, :, pol_inds, :]
                    raw_data_array = raw_data_array[:, 0, 0, :, :, :, :]
                    assert self.Nspws == raw_data_array.shape[1]
                else:
                    # in many uvfits files the spw axis is left out,
                    # here we put it back in so the dimensionality stays the same
                    raw_data_array = vis_hdu.data.data[:, :, :, :, pol_inds, :]
                    raw_data_array = raw_data_array[:, 0, 0, :, :, :]
                    raw_data_array = raw_data_array[:, np.newaxis, :, :, :]

                if blt_frac < 1:
                    raw_data_array = raw_data_array[blt_inds, :, :, :, :]
                if freq_frac < 1:
                    raw_data_array = raw_data_array[:, :, freq_inds, :, :]

        assert len(raw_data_array.shape) == 5

        # Reshape the data array to be the right size if we are working w/ multiple
        # spectral windows to be 'flex_spw' compliant
        if self.Nspws > 1:
            raw_data_array = np.reshape(
                raw_data_array,
                (self.Nblts, 1, self.Nfreqs, self.Npols, raw_data_array.shape[4]),
            )

        # FITS uvw direction convention is opposite ours and Miriad's.
        # So conjugate the visibilities and flip the uvws:
        self.data_array = (
            raw_data_array[:, :, :, :, 0] - 1j * raw_data_array[:, :, :, :, 1]
        )
        self.flag_array = raw_data_array[:, :, :, :, 2] <= 0
        self.nsample_array = np.abs(raw_data_array[:, :, :, :, 2])

        if fix_old_proj:
            self.fix_phase(use_ant_pos=fix_use_ant_pos)

    def read_uvfits(
        self,
        filename,
        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,
        keep_all_metadata=True,
        read_data=True,
        background_lsts=True,
        run_check=True,
        check_extra=True,
        run_check_acceptability=True,
        strict_uvw_antpos_check=False,
        fix_old_proj=False,
        fix_use_ant_pos=True,
        check_autos=True,
        fix_autos=True,
        use_future_array_shapes=False,
    ):
        """
        Read in header, metadata and data from a uvfits file.

        Supports reading only selected portions of the data.

        Parameters
        ----------
        filename : str
            The uvfits file to read from.
        antenna_nums : array_like of int, optional
            The antennas numbers to include when reading data into 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. Ignored if read_data is False.
        antenna_names : array_like of str, optional
            The antennas names to include when reading data into 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_nums` is also provided. Ignored if read_data 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 include when reading data into 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. Ignored if read_data is False.
        ant_str : str, optional
            A string containing information about what antenna numbers
            and polarizations to include when reading 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 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. Ignored if read_data is False.
        frequencies : array_like of float, optional
            The frequencies to include when reading data into the object, each
            value passed here should exist in the freq_array. Ignored if
            read_data is False.
        freq_chans : array_like of int, optional
            The frequency channel numbers to include when reading data into the
            object. Ignored if read_data is False.
        times : array_like of float, optional
            The times to include when reading data into the object, each value
            passed here should exist in the time_array.
        time_range : array_like of float, optional
            The time range in Julian Date to keep in the object, 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 numbers to include when reading data into the
            object, each value passed here should exist in the polarization_array.
            Ignored if read_data is False.
        blt_inds : array_like of int, optional
            The baseline-time indices to include when reading data into the
            object. This is not commonly used. Ignored if read_data is False.
        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.
        keep_all_metadata : bool
            Option to keep all the metadata associated with antennas, even those
            that do not have data associated with them after the select option.
        read_data : bool
            Read in the visibility, nsample 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.
        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 after reading in the file (the default is True,
            meaning the check will be run). Ignored if read_data is False.
        check_extra : bool
            Option to check optional parameters as well as required ones (the
            default is True, meaning the optional parameters will be checked).
            Ignored if read_data is False.
        run_check_acceptability : bool
            Option to check acceptable range of the values of parameters after
            reading in the file (the default is True, meaning the acceptable
            range check will be done). Ignored if read_data is False.
        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.
        fix_old_proj : bool
            Applies a fix to uvw-coordinates and phasing, assuming that the old `phase`
            method was used prior to writing the data, which had errors of the order of
            one part in 1e4 - 1e5. See the phasing memo for more details. Default is
            False.
        fix_use_ant_pos : bool
            If setting `fix_old_proj` to True, use the antenna positions to derive the
            correct uvw-coordinates rather than using the baseline vectors. Default is
            True.
        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 True.
        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.

        Raises
        ------
        IOError
            If filename doesn't exist.
        ValueError
            If incompatible select keywords are set (e.g. `ant_str` with other
            antenna selectors, `times` and `time_range`) or select keywords
            exclude all data or if keywords are set to the wrong type.
            If the data have multi spw with different channel widths.
            If the metadata are not internally consistent or missing.

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

        with fits.open(filename, memmap=True) as hdu_list:
            vis_hdu = hdu_list[0]  # assumes the visibilities are in the primary hdu
            vis_hdr = vis_hdu.header.copy()
            hdunames = uvutils._fits_indexhdus(hdu_list)  # find the rest of the tables

            # First get everything we can out of the header.

            # check for multi source files. NOW SUPPORTED, W00T!
            read_source = False
            if "SOURCE" in vis_hdu.data.parnames:
                if "AIPS SU" in hdunames.keys():
                    read_source = True
                else:
                    warnings.warn(
                        "UVFITS file is missing AIPS SU table, which is required when "
                        "SOURCE is one of the `random paramters` in the main binary "
                        "table. Bypassing for now, but note that this file _may_ not "
                        "work correctly in UVFITS-based programs (e.g., AIPS, CASA)."
                    )

            # check if we have an spw dimension
            if vis_hdr["NAXIS"] == 7:
                self.Nspws = vis_hdr.pop("NAXIS5")
                self.spw_array = (
                    uvutils._fits_gethduaxis(vis_hdu, 5).astype(np.int64) - 1
                )
                ra_axis = 6
                dec_axis = 7
            else:
                self.Nspws = 1
                self.spw_array = np.array([np.int64(0)])
                ra_axis = 5
                dec_axis = 6

            # get shapes
            self.Npols = vis_hdr.pop("NAXIS3")
            self.Nblts = vis_hdr.pop("GCOUNT")

            if self.Nspws > 1:
                # If this is multi-spw, use the 'flexible' spectral window setup
                self._set_flex_spw()
                uvfits_nchan = vis_hdr.pop("NAXIS4")
                self.Nfreqs = uvfits_nchan * self.Nspws
                self.flex_spw_id_array = np.transpose(
                    np.tile(np.arange(self.Nspws), (uvfits_nchan, 1))
                ).flatten()
                fq_hdu = hdu_list[hdunames["AIPS FQ"]]
                assert self.Nspws == fq_hdu.header["NO_IF"]

                # TODO: This is fine for now, although I (karto) think that this
                # is relative to the ref_freq, which can be specified as part of
                # the AIPS SU table.

                # Get rest freq value
                ref_freq = uvutils._fits_gethduaxis(vis_hdu, 4)[0]
                self.channel_width = np.transpose(
                    np.tile(abs(fq_hdu.data["CH WIDTH"]), (uvfits_nchan, 1))
                ).flatten()
                self.freq_array = np.reshape(
                    np.transpose(
                        (
                            ref_freq
                            + fq_hdu.data["IF FREQ"]
                            + np.outer(np.arange(uvfits_nchan), fq_hdu.data["CH WIDTH"])
                        )
                    ),
                    (1, -1),
                )
            else:
                self.Nfreqs = vis_hdr.pop("NAXIS4")
                self.freq_array = uvutils._fits_gethduaxis(vis_hdu, 4)
                # TODO: Spw axis to be collapsed in future release
                self.freq_array.shape = (1,) + self.freq_array.shape
                self.channel_width = vis_hdr.pop("CDELT4")

                # future proof: always set the flex_spw_id_array
                self.flex_spw_id_array = np.zeros(self.Nfreqs, dtype=int)

            self.polarization_array = np.int32(uvutils._fits_gethduaxis(vis_hdu, 3))
            # other info -- not required but frequently used
            self.telescope_name = vis_hdr.pop("TELESCOP", None)
            self.instrument = vis_hdr.pop("INSTRUME", None)
            latitude_degrees = vis_hdr.pop("LAT", None)
            longitude_degrees = vis_hdr.pop("LON", None)
            altitude = vis_hdr.pop("ALT", None)
            self.x_orientation = vis_hdr.pop("XORIENT", None)
            blt_order_str = vis_hdr.pop("BLTORDER", None)
            if blt_order_str is not None:
                self.blt_order = tuple(blt_order_str.split(", "))
                if self.blt_order == ("bda",):
                    self._blt_order.form = (1,)
            self.history = str(vis_hdr.get("HISTORY", ""))
            if not uvutils._check_history_version(
                self.history, self.pyuvdata_version_str
            ):
                self.history += self.pyuvdata_version_str

            self.vis_units = vis_hdr.pop("BUNIT", "uncalib")
            # Added here as a fix since some previous versions of UVData allowed for
            # all caps versions of UNCALIB.
            if self.vis_units == "UNCALIB":
                self.vis_units = "uncalib"

            # PHSFRAME is not a standard UVFITS keyword, but was used by older
            # versions of pyuvdata. To ensure backwards compatibility, we look
            # for it first to determine the coordinate frame for the data
            phase_center_frame = vis_hdr.pop("PHSFRAME", None)
            # If we don't find the special keyword PHSFRAME, try for the more
            # FITS-standard RADESYS
            if phase_center_frame is None:
                phase_center_frame = vis_hdr.pop("RADESYS", None)
            # If we still don't find anything, try the two 'special' variant
            # names for the coordinate frame that seem to have been documented
            if phase_center_frame is None:
                phase_center_frame = vis_hdr.pop("RADESYSA", None)
            if phase_center_frame is None:
                phase_center_frame = vis_hdr.pop("RADESYSa", None)

            if not read_source:
                # the axis number for phase center depends on if the spw exists

                phase_center_ra_degrees = float(vis_hdr.pop("CRVAL" + str(ra_axis)))
                phase_center_dec_degrees = float(vis_hdr.pop("CRVAL" + str(dec_axis)))

                phase_center_epoch = vis_hdr.pop("EPOCH", None)
                object_name = vis_hdr.pop("OBJECT", None)

                # If we _still_ didn't find the phase frame, take a guess based on the
                # value listed in the EPOCH. The behavior listed here is based off of
                # the AIPS task REGRD (http://www.aips.nrao.edu/cgi-bin/ZXHLP2.PL?REGRD)
                if phase_center_frame is None:
                    if phase_center_epoch is None:
                        phase_center_frame = "icrs"
                    else:
                        if phase_center_epoch == 1950.0:
                            phase_center_frame = "fk4"
                        else:
                            phase_center_frame = "fk5"
                cat_id = self._add_phase_center(
                    object_name,
                    cat_type="sidereal",
                    cat_lon=np.deg2rad(phase_center_ra_degrees),
                    cat_lat=np.deg2rad(phase_center_dec_degrees),
                    cat_frame=phase_center_frame,
                    cat_epoch=phase_center_epoch,
                )
                self.phase_center_id_array = np.zeros(self.Nblts, dtype=int) + cat_id

            self.extra_keywords = uvutils._get_fits_extra_keywords(
                vis_hdr,
                keywords_to_skip=[
                    "DATE-OBS",
                    "EPOCH",
                    "OBJECT",
                    "PHSFRAME",
                    "RADESYS",
                    "RADESYSA",
                    "RADESYSa",
                ],
            )

            # Next read the antenna table
            ant_hdu = hdu_list[hdunames["AIPS AN"]]

            # stuff in the header
            if self.telescope_name is None:
                self.telescope_name = ant_hdu.header["ARRNAM"]

            self.gst0 = ant_hdu.header["GSTIA0"]
            self.rdate = ant_hdu.header["RDATE"]
            self.earth_omega = ant_hdu.header["DEGPDY"]
            self.dut1 = ant_hdu.header["UT1UTC"]
            if "TIMESYS" in ant_hdu.header.keys():
                self.timesys = ant_hdu.header["TIMESYS"]
            else:
                # CASA misspells this one
                self.timesys = ant_hdu.header["TIMSYS"]

            if "FRAME" in ant_hdu.header.keys():
                xyz_telescope_frame = ant_hdu.header["FRAME"]
            else:
                warnings.warn(
                    "Required Antenna keyword 'FRAME' not set; "
                    "Assuming frame is 'ITRF'."
                )
                xyz_telescope_frame = "ITRF"
            # get telescope location and antenna positions.
            # VLA incorrectly sets ARRAYX/ARRAYY/ARRAYZ to 0, and puts array center
            # in the antenna positions themselves
            if (
                np.isclose(ant_hdu.header["ARRAYX"], 0)
                and np.isclose(ant_hdu.header["ARRAYY"], 0)
                and np.isclose(ant_hdu.header["ARRAYZ"], 0)
            ):
                x_telescope = np.mean(ant_hdu.data["STABXYZ"][:, 0])
                y_telescope = np.mean(ant_hdu.data["STABXYZ"][:, 1])
                z_telescope = np.mean(ant_hdu.data["STABXYZ"][:, 2])
                self.antenna_positions = ant_hdu.data.field("STABXYZ") - np.array(
                    [x_telescope, y_telescope, z_telescope]
                )

            else:
                x_telescope = ant_hdu.header["ARRAYX"]
                y_telescope = ant_hdu.header["ARRAYY"]
                z_telescope = ant_hdu.header["ARRAYZ"]
                # AIPS memo #117 says that antenna_positions should be relative to
                # the array center, but in a rotated ECEF frame so that the x-axis
                # goes through the local meridian.
                rot_ecef_positions = ant_hdu.data.field("STABXYZ")
                latitude, longitude, altitude = uvutils.LatLonAlt_from_XYZ(
                    np.array([x_telescope, y_telescope, z_telescope]),
                    check_acceptability=run_check_acceptability,
                )
                self.antenna_positions = uvutils.ECEF_from_rotECEF(
                    rot_ecef_positions, longitude
                )

            if xyz_telescope_frame == "ITRF":
                self.telescope_location = np.array(
                    [x_telescope, y_telescope, z_telescope]
                )
            else:
                if (
                    latitude_degrees is not None
                    and longitude_degrees is not None
                    and altitude is not None
                ):
                    self.telescope_location_lat_lon_alt_degrees = (
                        latitude_degrees,
                        longitude_degrees,
                        altitude,
                    )

            # stuff in columns
            ant_names = ant_hdu.data.field("ANNAME").tolist()
            self.antenna_names = []
            for name in ant_names:
                # Sometimes CASA writes antnames as bytes not strings.
                # If the ant name is shorter than 8 characters, the trailing
                # characters may be non-ascii.
                # This is technically a FITS violation as FITS requires ascii.
                # So we just ignore any non-ascii bytes in the decode.
                if isinstance(name, bytes):
                    ant_name_str = str(name.decode("utf-8", "ignore"))
                else:
                    ant_name_str = name
                # remove non-printing ascii characters and exclamation points
                ant_name_str = (
                    ant_name_str.replace("\x00", "")
                    .replace("\x07", "")
                    .replace("!", "")
                )
                self.antenna_names.append(ant_name_str)

            # Note: we no longer subtract one to get to 0-indexed values
            # rather than 1-indexed values. Antenna numbers are not indices
            # but are unique to each antenna.
            self.antenna_numbers = ant_hdu.data.field("NOSTA")

            self.Nants_telescope = len(self.antenna_numbers)

            if "DIAMETER" in ant_hdu.columns.names:
                self.antenna_diameters = ant_hdu.data.field("DIAMETER")

            try:
                self.set_telescope_params()
            except ValueError as ve:
                warnings.warn(str(ve))

            # Now read in the random parameter info
            self._get_parameter_data(
                vis_hdu,
                read_source,
                run_check_acceptability,
                background_lsts=background_lsts,
            )

            if read_source:
                su_hdu = hdu_list[hdunames["AIPS SU"]]
                # We should have as many entries in the AIPS SU header as we have
                # unique entries in the SOURCES random paramter (checked in the call
                # to get_parameter_data above)
                if len(su_hdu.data) != len(np.unique(self.phase_center_id_array)):
                    raise RuntimeError(
                        "The UVFITS file has a malformed AIPS SU table - number of "
                        "sources do not match the number of unique source IDs in the "
                        "primary data header."
                    )  # pragma: no cover

                # Set up these arrays so we can assign values to them
                self.phase_center_app_ra = np.zeros(self.Nblts)
                self.phase_center_app_dec = np.zeros(self.Nblts)
                self.phase_center_app_pa = np.zeros(self.Nblts)

                # Alright, we are off to the races!
                for idx in range(len(su_hdu.data)):
                    # Grab the indv source entry
                    sou_info = su_hdu.data[idx]
                    sou_id = sou_info["ID. NO."]
                    sou_name = sou_info["SOURCE"]
                    sou_ra = sou_info["RAEPO"] * (np.pi / 180.0)
                    sou_dec = sou_info["DECEPO"] * (np.pi / 180.0)
                    sou_epoch = sou_info["EPOCH"]

                    if phase_center_frame is not None:
                        sou_frame = phase_center_frame
                    else:
                        # Note that this can result in different frames for different
                        # phase centers, which we disallow on write. Only get here if
                        # the RADESYS header parameter is not in file
                        # (we always write it)
                        if sou_epoch == 1950.0:
                            sou_frame = "fk4"
                        else:
                            sou_frame = "fk5"

                    self._add_phase_center(
                        sou_name,
                        cat_id=sou_id,
                        cat_type="sidereal",
                        cat_lon=sou_ra,
                        cat_lat=sou_dec,
                        cat_frame=sou_frame,
                        cat_epoch=sou_epoch,
                        info_source="uvfits file",
                    )

            # Calculate the apparent coordinate values
            self._set_app_coords_helper()

            # fix up the uvws if in the NCP baseline coordinate frame.
            # Must be done here because it requires the phase_center_app_dec
            if "UU---NCP" in vis_hdu.data.parnames:
                self.uvw_array = uvutils._rotate_one_axis(
                    self.uvw_array[:, :, None], self.phase_center_app_dec - np.pi / 2, 0
                )[:, :, 0]

            if read_data:
                # Now read in the data
                self._get_data(
                    vis_hdu,
                    antenna_nums,
                    antenna_names,
                    ant_str,
                    bls,
                    frequencies,
                    freq_chans,
                    times,
                    time_range,
                    lsts,
                    lst_range,
                    polarizations,
                    blt_inds,
                    phase_center_ids,
                    catalog_names,
                    keep_all_metadata,
                    fix_old_proj,
                    fix_use_ant_pos,
                )
        if use_future_array_shapes:
            self.use_future_array_shapes()
        else:
            warnings.warn(_future_array_shapes_warning, DeprecationWarning)

        # check if object has all required UVParameters set
        if run_check:
            self.check(
                check_extra=check_extra,
                run_check_acceptability=run_check_acceptability,
                strict_uvw_antpos_check=strict_uvw_antpos_check,
                allow_flip_conj=True,
                check_autos=check_autos,
                fix_autos=fix_autos,
            )

    def write_uvfits(
        self,
        filename,
        spoof_nonessential=False,
        write_lst=True,
        force_phase=False,
        run_check=True,
        check_extra=True,
        run_check_acceptability=True,
        strict_uvw_antpos_check=False,
        check_autos=True,
        fix_autos=False,
    ):
        """
        Write the data to a uvfits file.

        If using this method to write out a data set for import into CASA, users should
        be aware that the `importuvifts` task does not currently support reading in
        data sets where the number of antennas is > 255. If writing out such a data set
        for use in CASA, we suggest using the measurement set writer (`UVData.write_ms`)
        instead.

        Parameters
        ----------
        filename : str
            The uvfits file to write to.
        spoof_nonessential : bool
            Deprecated and has no effect. Values are automatically set to their best
            known values if not previously set.
        write_lst : bool
            Option to write the LSTs to the metadata (random group parameters).
        force_phase : bool
            Option to automatically phase drift scan data to zenith of the first
            timestamp.
        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.

        Raises
        ------
        ValueError
            The `phase_type` of the object is "drift" and the `force_phase`
            keyword is not set.
            If the frequencies are not evenly spaced or are separated by more
            than their channel width.
            The polarization values are not evenly spaced.
            If the `timesys` parameter is set to anything other than "UTC" or None.
        TypeError
            If any entry in extra_keywords is not a single string or number.

        """
        if run_check:
            self.check(
                check_extra=check_extra,
                run_check_acceptability=run_check_acceptability,
                check_freq_spacing=True,
                strict_uvw_antpos_check=strict_uvw_antpos_check,
                check_autos=check_autos,
                fix_autos=fix_autos,
            )

        if spoof_nonessential:
            warnings.warn(
                (
                    "UVFITS-required metadata are now set automatically to their "
                    'best known values, and no longer need to be "spoofed". As such, '
                    "the spoof_nonessential parameter is deprecated, and will be "
                    "removed in version 2.4."
                ),
                DeprecationWarning,
            )

        if np.any(~self._check_for_cat_type(["sidereal"])):
            if force_phase:
                print(
                    "The data are not all phased to a sidereal source. Phasing to "
                    "zenith of the first timestamp."
                )
                phase_time = Time(self.time_array[0], format="jd")
                self.phase_to_time(phase_time)
            else:
                raise ValueError(
                    "The data are not all phased to a sidereal source. "
                    "Set force_phase to true to phase the data "
                    "to zenith of the first timestamp before "
                    "writing a uvfits file."
                )

        if self.flex_spw:
            # If we have a 'flexible' spectral window, we will need to evaluate the
            # frequency axis slightly differently.
            if self.future_array_shapes:
                freq_array_use = self.freq_array
            else:
                freq_array_use = self.freq_array[0, :]
            nchan_list = []
            start_freq_array = []
            delta_freq_array = []
            for idx in self.spw_array:
                chan_mask = self.flex_spw_id_array == idx
                nchan_list += [np.sum(chan_mask)]
                start_freq_array += [freq_array_use[chan_mask][0]]
                # Need the array direction here since channel_width is always supposed
                # to be > 0, but channels can be in decending freq order
                freq_dir = np.sign(np.median(np.diff(freq_array_use[chan_mask])))
                delta_freq_array += [
                    np.median(self.channel_width[chan_mask]) * freq_dir
                ]

            start_freq_array = np.reshape(np.array(start_freq_array), (1, -1)).astype(
                np.float64
            )

            delta_freq_array = np.reshape(np.array(delta_freq_array), (1, -1)).astype(
                np.float64
            )

            # We've constructed a couple of lists with relevant values, now time to
            # check them to make sure that the data will write correctly

            # Make sure that all the windows are of the same size
            if len(np.unique(nchan_list)) != 1:
                raise IndexError(
                    "UVFITS format cannot handle spectral windows of different sizes!"
                )

            # Make sure freq values are greater zero. Note that I think _technically
            # one could write negative frequencies into the dataset, but I am pretty
            # sure that reduction packages may balk hard.
            if np.any(start_freq_array <= 0):
                raise ValueError("Frequency values must be > 0 for UVFITS!")

            # Make sure the delta values are non-zero
            if np.any(delta_freq_array == 0):
                raise ValueError("Something is wrong, frequency values not unique!")

            # If we passed all the above checks, then it's time to fill some extra
            # array values. Note that 'ref_freq' is something of a placeholder for
            # other exciting things...
            ref_freq = start_freq_array[0, 0]
        else:
            if self.future_array_shapes:
                ref_freq = self.freq_array[0]
                # we've already run the check_freq_spacing, so channel widths are the
                # same to our tolerances
                delta_freq_array = np.array([[np.median(self.channel_width)]]).astype(
                    np.float64
                )
            else:
                ref_freq = self.freq_array[0, 0]
                delta_freq_array = np.array([[self.channel_width]]).astype(np.float64)

        if self.Npols > 1:
            pol_indexing = np.argsort(np.abs(self.polarization_array))
            polarization_array = self.polarization_array[pol_indexing]
            if not uvutils._test_array_constant_spacing(polarization_array):
                raise ValueError(
                    "The polarization values are not evenly spaced (probably "
                    "because of a select operation). The uvfits format "
                    "does not support unevenly spaced polarizations."
                )
            pol_spacing = polarization_array[1] - polarization_array[0]
        else:
            pol_indexing = np.asarray([0])
            polarization_array = self.polarization_array
            pol_spacing = 1

        # check for unflagged data with nsample = 0. Warn if any found
        wh_nsample0 = np.where(self.nsample_array == 0)
        if np.any(~self.flag_array[wh_nsample0]):
            warnings.warn(
                "Some unflagged data has nsample = 0. Flags and "
                "nsamples are combined in uvfits files such that "
                "these data will appear to be flagged."
            )

        uvfits_data_shape = (
            self.Nblts,
            1,
            1,
            self.Nspws,
            self.Nfreqs // self.Nspws,
            self.Npols,
            1,
        )

        # Reshape the arrays so that they match the uvfits conventions
        # FITS uvw direction convention is opposite ours and Miriad's.
        # So conjugate the visibilities and flip the uvws:
        data_array = np.reshape(np.conj(self.data_array), uvfits_data_shape)
        weights_array = np.reshape(
            self.nsample_array * np.where(self.flag_array, -1, 1), uvfits_data_shape
        )
        data_array = data_array[:, :, :, :, :, pol_indexing, :]
        weights_array = weights_array[:, :, :, :, :, pol_indexing, :]

        uvfits_array_data = np.concatenate(
            [data_array.real, data_array.imag, weights_array], axis=6
        )
        # convert to seconds units
        uvw_array_sec = -1 * self.uvw_array / const.c.to("m/s").value

        if self.data_array.dtype == "complex128":
            write_precision = 64
        else:
            write_precision = 32

        # uvfits convention is that there are two float32 time_arrays and the
        # float64 sum of them + relevant PZERO = actual JD
        # a common practice is to set the PZERO to the JD at midnight of the first time
        jd_midnight = np.floor(self.time_array[0] - 0.5) + 0.5
        if write_precision == 32:
            time_array1 = np.float32(self.time_array - jd_midnight)
            time_array2 = np.float32(
                self.time_array - jd_midnight - np.float64(time_array1)
            )

        else:
            time_array1 = self.time_array - jd_midnight
        int_time_array = self.integration_time

        baselines_use = self.antnums_to_baseline(
            self.ant_1_array, self.ant_2_array, attempt256=True
        )
        # Set up dictionaries for populating hdu
        # Antenna arrays are populated with actual antenna numbers,
        # that is, numbers that are unique to each antenna.
        # We no longer add 1 to the arrays as the arrays are not indices.

        group_parameter_dict = {
            "UU      ": uvw_array_sec[:, 0],
            "VV      ": uvw_array_sec[:, 1],
            "WW      ": uvw_array_sec[:, 2],
            "DATE    ": time_array1,
            "BASELINE": baselines_use,
            "SOURCE  ": None,
            "ANTENNA1": self.ant_1_array,
            "ANTENNA2": self.ant_2_array,
            "SUBARRAY": np.ones_like(self.ant_1_array),
            "INTTIM  ": int_time_array,
        }

        id_offset = int(0 in self.phase_center_catalog)
        group_parameter_dict["SOURCE  "] = self.phase_center_id_array + id_offset

        pscal_dict = {
            "UU      ": 1.0,
            "VV      ": 1.0,
            "WW      ": 1.0,
            "DATE    ": 1.0,
            "BASELINE": 1.0,
            "SOURCE  ": 1.0,
            "ANTENNA1": 1.0,
            "ANTENNA2": 1.0,
            "SUBARRAY": 1.0,
            "INTTIM  ": 1.0,
        }
        pzero_dict = {
            "UU      ": 0.0,
            "VV      ": 0.0,
            "WW      ": 0.0,
            "DATE    ": jd_midnight,
            "BASELINE": 0.0,
            "SOURCE  ": 0.0,
            "ANTENNA1": 0.0,
            "ANTENNA2": 0.0,
            "SUBARRAY": 0.0,
            "INTTIM  ": 0.0,
        }

        if write_lst:
            # lst is a non-standard entry (it's not in the AIPS memo)
            # but storing it can be useful (e.g. can avoid recalculating it on read)
            # need to store it in 2 parts to get enough accuracy
            # angles in uvfits files are stored in degrees, so first convert to degrees
            lst_array_deg = np.rad2deg(self.lst_array)
            if write_precision == 32:
                lst_array_1 = np.float32(lst_array_deg)
                lst_array_2 = np.float32(lst_array_deg - np.float64(lst_array_1))
            else:
                lst_array_1 = lst_array_deg
            group_parameter_dict["LST     "] = lst_array_1
            pscal_dict["LST     "] = 1.0
            pzero_dict["LST     "] = 0.0
            if write_precision == 32:
                pscal_dict["LST2    "] = 1.0
                pzero_dict["LST2    "] = 0.0

        # list contains arrays of [u,v,w,date,baseline];
        # each array has shape (Nblts)
        parnames_use = ["UU      ", "VV      ", "WW      ", "DATE    "]
        if write_precision == 32:
            group_parameter_dict["DATE2   "] = time_array2
            pscal_dict["DATE2   "] = 1.0
            pzero_dict["DATE2   "] = 0.0
            parnames_use.append("DATE2   ")
        if np.max(self.ant_1_array) < 255 and np.max(self.ant_2_array) < 255:
            # if the number of antennas is less than 256 then include both the
            # baseline array and the antenna arrays in the group parameters.
            # Otherwise just use the antenna arrays
            parnames_use.append("BASELINE")
        else:
            warnings.warn(
                "Found antenna numbers > 255 in this data set. This is permitted by "
                "UVFITS standards, but may cause the `importuvfits` utility within "
                "CASA to crash. If attempting to use this data set in CASA, consider "
                "using the measurement set writer method (`write_ms`) instead."
            )

        parnames_use += ["SOURCE  ", "ANTENNA1", "ANTENNA2", "SUBARRAY", "INTTIM  "]

        if write_lst:
            parnames_use.append("LST     ")

        group_parameter_list = [
            group_parameter_dict[parname] for parname in parnames_use
        ]

        if write_precision == 32:
            # add second date part
            parnames_write = copy.deepcopy(parnames_use)
            parnames_write[parnames_write.index("DATE2   ")] = "DATE    "
            if write_lst:
                # add second LST array part
                parnames_use.append("LST2    ")
                parnames_write.append("LST     ")
                group_parameter_list.append(lst_array_2)
        else:
            parnames_write = copy.deepcopy(parnames_use)

        hdu = fits.GroupData(
            uvfits_array_data,
            parnames=parnames_write,
            pardata=group_parameter_list,
            bitpix=(-1 * write_precision),
        )
        hdu = fits.GroupsHDU(hdu)

        for i, key in enumerate(parnames_use):
            hdu.header["PSCAL" + str(i + 1) + "  "] = pscal_dict[key]
            hdu.header["PZERO" + str(i + 1) + "  "] = pzero_dict[key]

        # ISO string of first time in self.time_array
        hdu.header["DATE-OBS"] = Time(self.time_array[0], scale="utc", format="jd").isot

        hdu.header["CTYPE2  "] = "COMPLEX "
        hdu.header["CRVAL2  "] = 1.0
        hdu.header["CRPIX2  "] = 1.0
        hdu.header["CDELT2  "] = 1.0

        # Note: This axis is called STOKES to comply with the AIPS memo 117
        # However, this confusing because it is NOT a true Stokes axis,
        #   it is really the polarization axis.
        hdu.header["CTYPE3  "] = "STOKES  "
        hdu.header["CRVAL3  "] = float(polarization_array[0])
        hdu.header["CRPIX3  "] = 1.0
        hdu.header["CDELT3  "] = float(pol_spacing)

        hdu.header["CTYPE4  "] = "FREQ    "
        hdu.header["CRVAL4  "] = ref_freq
        hdu.header["CRPIX4  "] = 1.0
        hdu.header["CDELT4  "] = delta_freq_array[0, 0]

        hdu.header["CTYPE5  "] = "IF      "
        hdu.header["CRVAL5  "] = 1.0
        hdu.header["CRPIX5  "] = 1.0
        hdu.header["CDELT5  "] = 1.0

        if self.Nphase > 1:
            ra_use = 0.0
            dec_use = 0.0
            name_use = "MULTI"
        else:
            phase_dict = list(self.phase_center_catalog.values())[0]
            ra_use = np.rad2deg(phase_dict["cat_lon"])
            dec_use = np.rad2deg(phase_dict["cat_lat"])
            name_use = phase_dict["cat_name"]

        hdu.header["CTYPE6  "] = "RA"
        hdu.header["CRVAL6  "] = ra_use

        hdu.header["CTYPE7  "] = "DEC"
        hdu.header["CRVAL7  "] = dec_use

        hdu.header["BUNIT   "] = self.vis_units
        hdu.header["BSCALE  "] = 1.0
        hdu.header["BZERO   "] = 0.0

        hdu.header["OBJECT  "] = name_use
        hdu.header["TELESCOP"] = self.telescope_name
        hdu.header["LAT     "] = self.telescope_location_lat_lon_alt_degrees[0]
        hdu.header["LON     "] = self.telescope_location_lat_lon_alt_degrees[1]
        hdu.header["ALT     "] = self.telescope_location_lat_lon_alt[2]
        hdu.header["INSTRUME"] = self.instrument
        if self.Nphase == 1:
            hdu.header["EPOCH   "] = float(phase_dict["cat_epoch"])
        # TODO: This is a keyword that should at some point get added for velocity
        # reference stuff, although for right now pyuvdata doesn't do any sort of
        # handling of this, so stub this out for now.
        # hdu.header["SPECSYS "] = "TOPOCENT"

        if self.Nphase == 1:
            hdu.header["RADESYS"] = phase_dict["cat_frame"]
        else:
            frames = [
                ph_dict["cat_frame"] for ph_dict in self.phase_center_catalog.values()
            ]
            unique_frames, frame_count = np.unique(np.array(frames), return_counts=True)
            if unique_frames.size == 1:
                hdu.header["RADESYS"] = unique_frames[0]
            else:
                most_common_frames = unique_frames[frame_count == np.max(frame_count)]
                if most_common_frames.size == 1:
                    hdu.header["RADESYS"] = most_common_frames[0]
                else:
                    preferred_frames = ["fk5", "icrs"] + most_common_frames.tolist()
                    # this is guaranteed to find a match because most_common_frames are
                    # in the list, but it puts more preferred frames first
                    for frame in preferred_frames:
                        if frame in most_common_frames:
                            hdu.header["RADESYS"] = frame
                            break

        if self.x_orientation is not None:
            hdu.header["XORIENT"] = self.x_orientation

        if self.blt_order is not None:
            blt_order_str = ", ".join(self.blt_order)
            hdu.header["BLTORDER"] = blt_order_str

        for line in self.history.splitlines():
            hdu.header.add_history(line)

        # 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 uvfits 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 uvfits.".format(keyword=key, keytype=type(value))
                )

            if keyword == "COMMENT":
                for line in value.splitlines():
                    hdu.header.add_comment(line)
            else:
                hdu.header[keyword] = value

        # ADD the ANTENNA table
        staxof = np.zeros(self.Nants_telescope)

        # 0 specifies alt-az, 6 would specify a phased array
        mntsta = np.zeros(self.Nants_telescope)

        # beware, X can mean just about anything
        poltya = np.full((self.Nants_telescope), "X", dtype=np.object_)
        polaa = [90.0] + np.zeros(self.Nants_telescope)
        poltyb = np.full((self.Nants_telescope), "Y", dtype=np.object_)
        polab = [0.0] + np.zeros(self.Nants_telescope)

        col1 = fits.Column(name="ANNAME", format="8A", array=self.antenna_names)
        # AIPS memo #117 says that antenna_positions should be relative to
        # the array center, but in a rotated ECEF frame so that the x-axis
        # goes through the local meridian.
        longitude = self.telescope_location_lat_lon_alt[1]
        rot_ecef_positions = uvutils.rotECEF_from_ECEF(
            self.antenna_positions, longitude
        )
        col2 = fits.Column(name="STABXYZ", format="3D", array=rot_ecef_positions)
        # col3 = fits.Column(name="ORBPARAM", format="0D", array=Norb)
        col4 = fits.Column(name="NOSTA", format="1J", array=self.antenna_numbers)
        col5 = fits.Column(name="MNTSTA", format="1J", array=mntsta)
        col6 = fits.Column(name="STAXOF", format="1E", array=staxof)
        col7 = fits.Column(name="POLTYA", format="1A", array=poltya)
        col8 = fits.Column(name="POLAA", format="1E", array=polaa)
        # col9 = fits.Column(name='POLCALA', format='0E', array=Npcal, Nspws)
        col10 = fits.Column(name="POLTYB", format="1A", array=poltyb)
        col11 = fits.Column(name="POLAB", format="1E", array=polab)
        # col12 = fits.Column(name='POLCALB', format='0E', array=Npcal, Nspws)
        col_list = [col1, col2, col4, col5, col6, col7, col8, col10, col11]
        # The commented out entires are up above to help check for consistency with the
        # UVFITS format. ORBPARAM, POLCALA, and POLCALB are all technically required,
        # but are all of zero length. Added here to help with debugging.
        if self.antenna_diameters is not None:
            col12 = fits.Column(
                name="DIAMETER", format="1E", array=self.antenna_diameters
            )
            col_list.append(col12)

        cols = fits.ColDefs(col_list)

        ant_hdu = fits.BinTableHDU.from_columns(cols)

        ant_hdu.header["EXTNAME"] = "AIPS AN"
        ant_hdu.header["EXTVER"] = 1

        # write XYZ coordinates
        ant_hdu.header["ARRAYX"] = self.telescope_location[0]
        ant_hdu.header["ARRAYY"] = self.telescope_location[1]
        ant_hdu.header["ARRAYZ"] = self.telescope_location[2]
        ant_hdu.header["FRAME"] = "ITRF"

        # TODO Karto: Do this more intelligently in the future
        if self.future_array_shapes:
            ant_hdu.header["FREQ"] = self.freq_array[0]
        else:
            ant_hdu.header["FREQ"] = self.freq_array[0, 0]

        if (self.rdate is None) or (self.rdate == ""):
            rdate_obj = Time(np.floor(self.time_array[0]), format="jd", scale="utc")
        else:
            try:
                rdate_obj = Time(self.rdate, scale="utc")
            except ValueError:
                rdate_obj = Time(np.floor(self.time_array[0]), format="jd", scale="utc")

        if self.rdate is None:
            ant_hdu.header["RDATE"] = rdate_obj.strftime("%Y-%m-%d")
        else:
            ant_hdu.header["RDATE"] = self.rdate

        if self.gst0 is None:
            ant_hdu.header["GSTIA0"] = rdate_obj.sidereal_time("apparent", "tio").deg
        else:
            ant_hdu.header["GSTIA0"] = self.gst0

        if self.dut1 is None:
            ant_hdu.header["UT1UTC"] = float(rdate_obj.delta_ut1_utc)
        else:
            ant_hdu.header["UT1UTC"] = self.dut1

        if not (self.timesys is None or self.timesys == "UTC"):
            raise ValueError(
                "This file has a time system {tsys}. Only "
                '"UTC" time system files are supported'.format(tsys=self.timesys)
            )
        ant_hdu.header["TIMESYS"] = "UTC"
        ant_hdu.header["ARRNAM"] = self.telescope_name
        ant_hdu.header["NO_IF"] = self.Nspws
        # Note the value below is basically 360 deg x num of sidereal days in a year /
        # num of soalr days in a year.
        ant_hdu.header["DEGPDY"] = (
            360.9856438593 if self.earth_omega is None else self.earth_omega
        )
        # This is just a statically defined value
        ant_hdu.header["IATUTC"] = 37.0

        # set mandatory parameters which are not supported by this object
        # (or that we just don't understand)
        ant_hdu.header["NUMORB"] = 0

        # note: Bart had this set to 3. We've set it 0 after aips 117. -jph
        ant_hdu.header["NOPCAL"] = 0

        ant_hdu.header["POLTYPE"] = "X-Y LIN"

        # note: we do not support the concept of "frequency setups"
        # -- lists of spws given in a SU table.
        # Karto: Here might be a place to address freq setup?
        ant_hdu.header["FREQID"] = 1

        # if there are offsets in images, this could be the culprit
        ant_hdu.header["POLARX"] = 0.0
        ant_hdu.header["POLARY"] = 0.0

        ant_hdu.header["DATUTC"] = 0  # ONLY UTC SUPPORTED

        # we always output right handed coordinates
        ant_hdu.header["XYZHAND"] = "RIGHT"

        # At some point, we can fill these in more completely using astropy IERS
        # utilities, since CASA/AIPS doesn't want to be told what the apparent coords
        # are, but rather wants to calculate them itself.
        # ant_hdu.header["RDATE"] = '2020-07-24T16:35:39.144087'
        # ant_hdu.header["POLARX"] = 0.0
        # ant_hdu.header["POLARY"] = 0.0

        fits_tables = [hdu, ant_hdu]
        # If needed, add the FQ table
        if self.Nspws > 1:
            fmt_d = "%iD" % self.Nspws
            fmt_e = "%iE" % self.Nspws
            fmt_j = "%iJ" % self.Nspws

            # TODO Karto: Temp implementation until we fix some other things in UVData
            if_freq = start_freq_array - ref_freq
            ch_width = delta_freq_array
            tot_bw = (self.Nfreqs // self.Nspws) * np.abs(delta_freq_array)
            sideband = np.sign(delta_freq_array) * np.ones((1, self.Nspws))

            # FRQSEL is hardcoded at the moment, could think about doing this
            # at least somewhat more intelligently...
            col_list = [
                fits.Column(name="FRQSEL", format="1J", array=[1]),
                fits.Column(name="IF FREQ", unit="HZ", format=fmt_d, array=if_freq),
                fits.Column(name="CH WIDTH", unit="HZ", format=fmt_e, array=ch_width),
                fits.Column(
                    name="TOTAL BANDWIDTH", unit="HZ", format=fmt_e, array=tot_bw
                ),
                fits.Column(name="SIDEBAND", format=fmt_j, array=sideband),
            ]

            fq_hdu = fits.BinTableHDU.from_columns(fits.ColDefs(col_list))

            fq_hdu.header["EXTNAME"] = "AIPS FQ"
            fq_hdu.header["NO_IF"] = self.Nspws
            fits_tables.append(fq_hdu)

        # Always write the SU table
        fmt_d = "%iD" % self.Nspws
        fmt_e = "%iE" % self.Nspws
        fmt_j = "%iJ" % self.Nspws

        int_zeros = np.zeros(self.Nphase, dtype=int)
        flt_zeros = np.zeros(self.Nphase, dtype=np.float64)
        zero_arr = np.zeros((self.Nphase, self.Nspws))
        sou_ids = np.zeros(self.Nphase)
        name_arr = np.array(
            [ps_dict["cat_name"] for ps_dict in self.phase_center_catalog.values()]
        )
        cal_code = ["    "] * self.Nphase
        # These are things we need to flip through on a source-by-source basis
        ra_arr = np.zeros(self.Nphase, dtype=np.float64)
        app_ra = np.zeros(self.Nphase, dtype=np.float64)
        dec_arr = np.zeros(self.Nphase, dtype=np.float64)
        app_dec = np.zeros(self.Nphase, dtype=np.float64)
        epo_arr = np.zeros(self.Nphase, dtype=np.float64)
        pm_ra = np.zeros(self.Nphase, dtype=np.float64)
        pm_dec = np.zeros(self.Nphase, dtype=np.float64)
        rest_freq = np.zeros((self.Nphase, self.Nspws), dtype=np.float64)
        for idx, cat_id in enumerate(self.phase_center_catalog):
            phase_dict = self.phase_center_catalog[cat_id]
            # This is a stub for something smarter in the future
            sou_ids[idx] = cat_id + id_offset
            rest_freq[idx][:] = np.mean(self.freq_array)
            pm_ra[idx] = 0.0
            pm_dec[idx] = 0.0
            # So here's the deal -- we need all the objects to be in the same
            # coordinate frame, although nothing in phase_center_catalog forces
            # objects to share the same frame. So we want to make sure that
            # everything lines up with the coordinate frame listed.
            ra_arr[idx], dec_arr[idx] = uvutils.transform_sidereal_coords(
                phase_dict["cat_lon"],
                phase_dict["cat_lat"],
                phase_dict["cat_frame"],
                hdu.header["RADESYS"],
                in_coord_epoch=phase_dict.get("cat_epoch"),
                out_coord_epoch=phase_dict.get("cat_epoch"),
                time_array=np.mean(self.time_array),
            )

            epo_arr[idx] = (
                phase_dict["cat_epoch"]
                if "cat_epoch" in (phase_dict.keys())
                else 2000.0
            )

            app_ra[idx] = np.median(
                self.phase_center_app_ra[self.phase_center_id_array == cat_id]
            )

            app_dec[idx] = np.median(
                self.phase_center_app_dec[self.phase_center_id_array == cat_id]
            )

        ra_arr *= 180.0 / np.pi
        dec_arr *= 180.0 / np.pi
        app_ra *= 180.0 / np.pi
        app_dec *= 180.0 / np.pi

        col_list = [
            fits.Column(name="ID. NO.", format="1J", array=sou_ids),
            fits.Column(name="SOURCE", format="20A", array=name_arr),
            fits.Column(name="QUAL", format="1J", array=int_zeros),
            fits.Column(name="CALCODE", format="4A", array=cal_code),
            fits.Column(name="IFLUX", format=fmt_e, unit="JY", array=zero_arr),
            fits.Column(name="QFLUX", format=fmt_e, unit="JY", array=zero_arr),
            fits.Column(name="UFLUX", format=fmt_e, unit="JY", array=zero_arr),
            fits.Column(name="VFLUX", format=fmt_e, unit="JY", array=zero_arr),
            fits.Column(name="FREQOFF", format=fmt_d, unit="HZ", array=zero_arr),
            fits.Column(name="BANDWIDTH", format="1D", unit="HZ", array=flt_zeros),
            fits.Column(name="RAEPO", format="1D", unit="DEGREES", array=ra_arr),
            fits.Column(name="DECEPO", format="1D", unit="DEGREES", array=dec_arr),
            fits.Column(name="EPOCH", format="1D", unit="YEARS", array=epo_arr),
            fits.Column(name="RAAPP", format="1D", unit="DEGREES", array=app_ra),
            fits.Column(name="DECAPP", format="1D", unit="DEGREES", array=app_dec),
            fits.Column(name="LSRVEL", format=fmt_d, unit="M/SEC", array=zero_arr),
            fits.Column(name="RESTFREQ", format=fmt_d, unit="HZ", array=rest_freq),
            fits.Column(name="PMRA", format="1D", unit="DEG/DAY", array=pm_ra),
            fits.Column(name="PMDEC", format="1D", unit="DEG/DAY", array=pm_dec),
        ]

        su_hdu = fits.BinTableHDU.from_columns(fits.ColDefs(col_list))
        su_hdu.header["EXTNAME"] = "AIPS SU"
        su_hdu.header["NO_IF"] = self.Nspws
        su_hdu.header["FREQID"] = 1
        su_hdu.header["VELDEF"] = "RADIO"
        # TODO: Eventually we want to not have this hardcoded, but pyuvdata at
        # present does not carry around any velocity information. As per usual,
        # I (Karto) am tipping my hand on what I might be working on next...
        su_hdu.header["VELTYP"] = "LSR"
        fits_tables.append(su_hdu)

        # write the file
        hdulist = fits.HDUList(hdus=fits_tables)
        hdulist.writeto(filename, overwrite=True)
        hdulist.close()


# pyuvdata/uvdata/uvh5.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 UVH5 files."""
from __future__ import annotations

import json
import os
import warnings
from functools import cached_property
from pathlib import Path
from typing import Any, Literal

import h5py
import numpy as np

from .. import utils as uvutils
from .uvdata import UVData, _future_array_shapes_warning, radian_tol

__all__ = ["UVH5", "FastUVH5Meta"]


# define HDF5 type for interpreting HERA correlator outputs (integers) as
# complex numbers
_hera_corr_dtype = np.dtype([("r", "<i4"), ("i", "<i4")])

hdf5plugin_present = True
try:
    import hdf5plugin  # noqa: F401
except ImportError as error:
    hdf5plugin_present = False
    hdf5plugin_error = error


def _check_uvh5_dtype(dtype):
    """
    Check that a specified custom datatype conforms to UVH5 standards.

    According to the UVH5 spec, the data type for the data array must be a
    compound datatype with an "r" field and an "i" field. Additionally, both
    datatypes must be the same (e.g., "<i4", "<r8", etc.).

    Parameters
    ----------
    dtype : numpy dtype
        A numpy dtype object with an "r" field and an "i" field.

    Returns
    -------
    None

    Raises
    ------
    ValueError
        This is raised if dtype is not a numpy dtype, if the dtype does not have
        an "r" field and an "i" field, or if the "r" field and "i" fields have
        different types.
    """
    if not isinstance(dtype, np.dtype):
        raise ValueError("dtype in a uvh5 file must be a numpy dtype")
    if "r" not in dtype.names or "i" not in dtype.names:
        raise ValueError(
            "dtype must be a compound datatype with an 'r' field and an 'i' field"
        )
    rkind = dtype["r"].kind
    ikind = dtype["i"].kind
    if rkind != ikind:
        raise ValueError(
            "dtype must have the same kind ('i4', 'r8', etc.) for both real "
            "and imaginary fields"
        )
    return


def _read_complex_astype(dset, indices, dtype_out=np.complex64):
    """
    Read the given data set of a specified type to floating point complex data.

    Parameters
    ----------
    dset : h5py dataset
        A reference to an HDF5 dataset on disk.
    indices : tuple
        The indices to extract. Should be either lists of indices or numpy
        slice objects.
    dtype_out : str or numpy dtype
        The datatype of the output array. One of (complex, np.complex64,
        np.complex128). Default is np.complex64 (single-precision real and
        imaginary floats).

    Returns
    -------
    output_array : ndarray
        The array referenced in the dataset cast to complex values.

    Raises
    ------
    ValueError
        This is raised if dtype_out is not an acceptable value.
    """
    if dtype_out not in (complex, np.complex64, np.complex128):
        raise ValueError(
            "output datatype must be one of (complex, np.complex64, np.complex128)"
        )
    dset_shape, indices = uvutils._get_dset_shape(dset, indices)
    output_array = np.empty(dset_shape, dtype=dtype_out)
    # dset is indexed in native dtype, but is upcast upon assignment

    if dtype_out == np.complex64:
        compound_dtype = [("r", "f4"), ("i", "f4")]
    else:
        compound_dtype = [("r", "f8"), ("i", "f8")]

    output_array.view(compound_dtype)[:, :] = uvutils._index_dset(dset, indices)[:, :]

    return output_array


def _write_complex_astype(data, dset, indices):
    """
    Write floating point complex data as a specified type.

    Parameters
    ----------
    data : ndarray
        The data array to write out. Should be a complex-valued array that
        supports the .real and .imag attributes for accessing real and imaginary
        components.
    dset : h5py dataset
        A reference to an HDF5 dataset on disk.
    indices : tuple
        A 3-tuple representing indices to write data to. Should be either lists
        of indices or numpy slice objects. 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
    -------
    None
    """
    # get datatype from dataset
    dtype_out = dset.dtype

    if data.dtype == np.complex64:
        compound_dtype = [("r", "f4"), ("i", "f4")]
    else:
        compound_dtype = [("r", "f8"), ("i", "f8")]

    # make doubly sure dtype is valid; should be unless user is pathological
    _check_uvh5_dtype(dtype_out)
    if len(dset.shape) == 3:
        # this is the future array shape
        dset[indices[0], indices[1], indices[2]] = data.view(compound_dtype).astype(
            dtype_out, copy=False
        )
    else:
        dset[indices[0], np.s_[:], indices[1], indices[2]] = data.view(
            compound_dtype
        ).astype(dtype_out, copy=False)

    return


def _get_compression(compression):
    """
    Get the HDF5 compression and compression options to use.

    Parameters
    ----------
    compression : str
        HDF5 compression specification or "bitshuffle".

    Returns
    -------
    compression_use : str
        HDF5 compression specification
    compression_opts : tuple
        HDF5 compression options
    """
    if compression == "bitshuffle":
        if not hdf5plugin_present:  # pragma: no cover
            raise ImportError(
                "The hdf5plugin package is not installed but is required to use "
                "bitshuffle compression."
            ) from hdf5plugin_error

        compression_use = 32008
        compression_opts = (0, 2)
    else:
        compression_use = compression
        compression_opts = None

    return compression_use, compression_opts


class FastUVH5Meta:
    """
    A fast read-only interface to UVH5 file metadata that makes some assumptions.

    This class is just a really thin wrapper over a UVH5 file that makes it easier
    to read in parts of the metadata at a time. This makes it much faster to perform
    small tasks where simple metadata is required, rather than reading in the whole
    header.

    All metadata is available as attributes, through ``__getattr__`` magic. Thus,
    accessing eg. ``obj.freq_array`` will go and get the frequencies directly from the
    file, and store them in memory. However, some attributes are made faster than the
    default, by assumptions on the data shape -- in particular, times and baselines.

    Anything that is read in is stored in memory so the second access is much faster.
    However, the memory can be released simply by deleting the attribute (it can be
    accessed again, and the data will be re-read).

    Parameters
    ----------
    filename : str or Path
        The filename to read from.
    blt_order : tuple of str or "determine", optional
        The order of the baseline-time axis. This can be determined, or read
        directly from file, however since it has been optional in the past, many
        existing files do not contain it in the metadata.
        Some reading operations are significantly faster if this is known, so providing
        it here can provide a speedup. Default is to try and read it from file,
        and if not there, just leave it as None. Set to "determine" to auto-detect
        the blt_order from the metadata (takes extra time to do so).
    blts_are_rectangular : bool, optional
        Whether the baseline-time axis is rectangular. This can be read from metadata
        in new files, but many old files do not contain it. If not provided, the
        rectangularity will be determined from the data. This is a non-negligible
        operation, so if you know it, it can be provided here to speed up reading.
    time_axis_faster_than_bls : bool, optional
        If blts are rectangular, this variable specifies whether the time axis is
        the fastest-moving virtual axis. Various reading functions benefit from knowing
        this, so if it is known, it can be provided here to speed up reading. It will
        be determined from the data if not provided.
    recompute_nbls : bool, optional
        Whether to recompute the number of unique baselines from the data. Before v1.2
        of the UVH5 spec, it was possible to have an incorrect number of baselines in
        the header without error, so this provides an opportunity to rectify it. Old
        HERA files (< March 2023) may have this issue, but in this case the correct
        number of baselines can be computed more quickly than by fully re=computing,
        and so we do this.

    Notes
    -----
    To check if a particular attribute is available, use ``hasattr(obj, attr)``.
    Many attributes will not show up dynamically in an interpreter, because they are
    gotten dynamically from the file.
    """

    _string_attrs = frozenset(
        {
            "history",
            "instrument",
            "object_name",
            "x_orientation",
            "telescope_name",
            "rdate",
            "timesys",
            "eq_coeffs_convention",
            "phase_type",
            "phase_center_frame",
            "version",
        }
    )

    _defaults = {"x_orientation": None, "flex_spw": False}

    _int_attrs = frozenset(
        {
            "Nblts",
            "Ntimes",
            "Npols",
            "Nspws",
            "Nfreqs",
            "uvplane_reference_time",
            "Nphase",
            "Nants_data",
            "Nants_telescope",
        }
    )
    _float_attrs = frozenset(
        {
            "dut1",
            "earth_omega",
            "gst0",
            "phase_center_ra",
            "phase_center_dec",
            "phase_center_epoch",
        }
    )
    _bool_attrs = frozenset(("flex_spw",))

    def __init__(
        self,
        path: str | Path | h5py.File | h5py.Group,
        blt_order: Literal["determine"] | tuple[str] | None = None,
        blts_are_rectangular: bool | None = None,
        time_axis_faster_than_bls: bool | None = None,
        recompute_nbls: bool | None = None,
    ):
        self.__file = None

        if isinstance(path, h5py.File):
            self.path = Path(path.filename)
            self.__file = path
            self.__header = path["/Header"]
            self.__datagrp = path["/Data"]
        elif isinstance(path, h5py.Group):
            self.path = Path(path.file.filename)
            self.__file = path.file
            self.__header = path
            self.__datagrp = self.__file["/Data"]
        elif isinstance(path, (str, Path)):
            self.path = Path(path)

        self.__blts_are_rectangular = blts_are_rectangular
        self.__time_first = time_axis_faster_than_bls
        self.__blt_order = blt_order
        self._recompute_nbls = recompute_nbls

    def is_open(self) -> bool:
        """Whether the file is open."""
        return bool(self.__file)

    def __del__(self):
        """Close the file when the object is deleted."""
        if self.__file:
            self.__file.close()

    def __getstate__(self):
        """Get the state of the object."""
        return {
            k: v
            for k, v in self.__dict__.items()
            if k
            not in (
                "_FastUVH5Meta__file",
                "_FastUVH5Meta__header",
                "_FastUVH5Meta__datagrp",
                "header",
                "datagrp",
            )
        }

    def __setstate__(self, state):
        """Set the state of the object."""
        self.__dict__.update(state)
        self.__file = None

    def __eq__(self, other):
        """Check equality of two FastUVH5Meta objects."""
        if not isinstance(other, FastUVH5Meta):
            return False

        return (
            self.path == other.path
            and self.__blts_are_rectangular == other.__blts_are_rectangular
            and (
                self.__time_first == other.__time_first
                or self.__time_first is None
                or other.__time_first is None
            )
            and self.__blt_order == other.__blt_order
            and self.Nbls == other.Nbls
        )

    def __hash__(self):
        """Get a unique hash for the object."""
        return hash(self.path)

    def close(self):
        """Close the file."""
        self.__header = None
        self.__datagrp = None

        try:
            del self.header  # need to refresh these
        except AttributeError:
            pass

        try:
            del self.datagrp
        except AttributeError:
            pass

        if self.__file:
            self.__file.close()
        self.__file = None

    def open(self):  # noqa: A003
        """Open the file."""
        if not self.__file:
            self.__file = h5py.File(self.path, "r")
            self.__header = self.__file["/Header"]
            self.__datagrp = self.__file["/Data"]

    @cached_property
    def header(self) -> h5py.Group:
        """Get the header group."""
        if not self.__file:
            self.open()
        return self.__header

    @cached_property
    def datagrp(self) -> h5py.Group:
        """Get the header group."""
        if not self.__file:
            self.open()
        return self.__datagrp

    def get_transactional(self, item: str, cache: bool = True) -> Any:
        """Get an attribute from the metadata but close the file object afterwards.

        Using this method is safer than direct attribute access when dealing with
        many files.

        Parameters
        ----------
        item
            The attribute to get.
        cache
            Whether to cache the attribute in the object so that the next access is
            faster.
        """
        try:
            val = getattr(self, item)
        finally:
            self.close()

            if not cache:
                if item in self.__dict__:
                    del self.__dict__[item]

        return val

    def __getattr__(self, name: str) -> Any:
        """Get attribute directly from header group."""
        try:
            x = self.header[name][()]
            if name in self._string_attrs:
                x = bytes(x).decode("utf8")
            elif name in self._int_attrs:
                x = int(x)
            elif name in self._float_attrs:
                x = float(x)

            self.__dict__[name] = x
            return x
        except KeyError:
            try:
                return self._defaults[name]
            except KeyError as e:
                raise AttributeError(f"{name} not found in {self.path}") from e

    @cached_property
    def Nbls(self) -> int:  # noqa: N802
        """The number of unique baselines."""
        if self._recompute_nbls:
            if self.__blts_are_rectangular:
                return self.Nblts // self.Ntimes
            else:
                return len(np.unique(self.baseline_array))
        else:
            nbls = int(self.header["Nbls"][()])

            if self._recompute_nbls is None:
                # Test if this is an "old" HERA file. If so, the Nbls is wrong
                # in the header, and equal to Nblts == Nbls*Ntimes.
                if (
                    self.telescope_name == "HERA"
                    and nbls == self.Nblts
                    and (self.__blts_are_rectangular or self.Nblts % self.Ntimes == 0)
                ):
                    return self.Nblts // self.Ntimes
                else:
                    return nbls
            else:
                return nbls

    def get_blt_order(self) -> tuple[str]:
        """Get the blt order from analysing metadata."""
        return uvutils.determine_blt_order(
            time_array=self.time_array,
            ant_1_array=self.ant_1_array,
            ant_2_array=self.ant_2_array,
            baseline_array=self.baseline_array,
            Nbls=self.Nbls,
            Ntimes=self.Ntimes,
        )

    @cached_property
    def blt_order(self) -> tuple[str]:
        """Tuple defining order of blts axis."""
        if self.__blt_order not in (None, "determine"):
            return self.__blt_order

        h = self.header
        if "blt_order" in h:
            blt_order_str = bytes(h["blt_order"][()]).decode("utf8")
            return tuple(blt_order_str.split(", "))
        else:
            if self.__blt_order == "determine":
                return self.get_blt_order()
            else:
                return None

    @cached_property
    def blts_are_rectangular(self) -> bool:
        """Whether blts axis is rectangular.

        That is, this is true if the blts can be reshaped to ``(Ntimes, Nbls)`` OR
        ``(Nbls, Ntimes)``. This requires each baseline to have the same number of times
        associated, AND the blt ordering to either be (time, baseline) or
        (baseline,time).
        """
        if self.__blts_are_rectangular is not None:
            return self.__blts_are_rectangular

        h = self.header
        if "blts_are_rectangular" in h:
            return bool(h["blts_are_rectangular"][()])

        if self.Nblts == self.Ntimes * self.Nbls and self.blt_order in (
            ("time", "baseline"),
            ("baseline", "time"),
        ):
            return True

        is_rect, self.__time_first = uvutils.determine_rectangularity(
            time_array=self.time_array,
            baseline_array=self.baseline_array,
            nbls=self.Nbls,
            ntimes=self.Ntimes,
        )
        return is_rect

    @cached_property
    def time_axis_faster_than_bls(self) -> bool:
        """Whether times move first in the blt axis."""
        # first hit the blts_are_rectangular property to set the time_first property
        if not self.blts_are_rectangular:
            return False
        if self.__time_first is not None:
            return self.__time_first
        if "time_axis_faster_than_bls" in self.header:
            return bool(self.header["time_axis_faster_than_bls"][()])
        if self.Ntimes == 1:
            return False
        if self.Ntimes == self.Nblts:
            return True
        return self.header["time_array"][1] != self.header["time_array"][0]

    @cached_property
    def phase_center_catalog(self) -> dict | None:
        """Dictionary of phase centers."""
        header = self.header
        if "phase_center_catalog" not in header:
            return None
        phase_center_catalog = {}
        key0 = next(iter(header["phase_center_catalog"].keys()))
        if isinstance(header["phase_center_catalog"][key0], h5py.Group):
            # This is the new, correct way
            for pc, pc_dict in header["phase_center_catalog"].items():
                pc_id = int(pc)
                phase_center_catalog[pc_id] = {}
                for key, dset in pc_dict.items():
                    if issubclass(dset.dtype.type, np.bytes_):
                        phase_center_catalog[pc_id][key] = bytes(dset[()]).decode(
                            "utf8"
                        )
                    elif dset.shape is None:
                        phase_center_catalog[pc_id][key] = None
                    else:
                        phase_center_catalog[pc_id][key] = dset[()]
        else:
            # This is the old way this was written
            for key in header["phase_center_catalog"].keys():
                pc_dict = json.loads(
                    bytes(header["phase_center_catalog"][key][()]).decode("utf8")
                )
                pc_dict["cat_name"] = key
                pc_id = pc_dict.pop("cat_id")
                phase_center_catalog[pc_id] = pc_dict

        return phase_center_catalog

    @cached_property
    def phase_type(self) -> str:
        """The phase type of the data."""
        h = self.header
        if self.phase_center_catalog is not None:
            if all(
                pc["cat_type"] == "unprojected"
                for pc in self.phase_center_catalog.values()
            ):
                return "drift"
            else:
                return "phased"

        phs = bytes(h["phase_type"][()]).decode("utf8")
        if phs in ("drift", "phased"):
            return phs
        warnings.warn(
            "Unknown phase types are no longer supported, marking this "
            "object as unprojected (unphased) by default."
        )
        return "drift"

    @cached_property
    def phase_center_id_array(self):
        """Array of phase center IDs."""
        if self.phase_center_catalog:
            return self.header["phase_center_id_array"][:]
        else:
            return None

    @cached_property
    def times(self) -> np.ndarray:
        """The unique times in the file."""
        h = self.header
        if self.blts_are_rectangular:
            if self.time_axis_faster_than_bls:
                return h["time_array"][: self.Ntimes]
            else:
                return h["time_array"][:: self.Nbls]
        else:
            return np.unique(self.time_array)

    @cached_property
    def lsts(self) -> np.ndarray:
        """The unique LSTs in the file."""
        h = self.header
        if "lst_array" in h:
            if self.blts_are_rectangular:
                if self.time_axis_faster_than_bls:
                    return h["lst_array"][: self.Ntimes]
                else:
                    return h["lst_array"][:: self.Nbls]
            else:
                return np.unique(self.lst_array)

        # If lst_array not there, compute ourselves.
        return uvutils.get_lst_for_time(
            self.times, *self.telescope_location_lat_lon_alt_degrees
        )

    @cached_property
    def lst_array(self) -> np.ndarray:
        """The LSTs corresponding to each baseline-time."""
        h = self.header
        if "lst_array" in h:
            return h["lst_array"][:]
        else:
            return uvutils.get_lst_for_time(
                self.time_array, *self.telescope_location_lat_lon_alt_degrees
            )

    @cached_property
    def channel_width(self) -> float:
        """The width of each frequency channel in Hz."""
        # Pull in the channel_width parameter as either an array or as a single float,
        # depending on whether or not the data is stored with a flexible spw.
        h = self.header
        if self.flex_spw or np.asarray(h["channel_width"]).ndim == 1:
            return h["channel_width"][:]
        else:
            return float(h["channel_width"][()])

    @cached_property
    def extra_keywords(self) -> dict:
        """The extra_keywords from the file."""
        header = self.header
        if "extra_keywords" not in header:
            raise AttributeError("No extra_keywords in this file.")

        extra_keywords = {}
        for key in header["extra_keywords"].keys():
            if header["extra_keywords"][key].dtype.type in (np.string_, np.object_):
                extra_keywords[key] = bytes(header["extra_keywords"][key][()]).decode(
                    "utf8"
                )
            else:
                # special handling for empty datasets == python `None` type
                if header["extra_keywords"][key].shape is None:
                    extra_keywords[key] = None
                else:
                    extra_keywords[key] = header["extra_keywords"][key][()]
        return extra_keywords

    def check_lsts_against_times(self):
        """Check that LSTs consistent with the time_array and telescope location."""
        lsts = uvutils.get_lst_for_time(
            self.times, *self.telescope_location_lat_lon_alt_degrees
        )

        if not np.all(np.isclose(self.lsts, lsts, rtol=0, atol=radian_tol)):
            warnings.warn(
                f"LST values stored in {self.path} are not self-consistent "
                "with time_array and telescope location. Consider "
                "recomputing with utils.get_lst_for_time."
            )

    @cached_property
    def unique_antpair_1_array(self) -> np.ndarray:
        """The unique antenna 1 indices in the file."""
        h = self.header
        if self.blts_are_rectangular:
            if self.time_axis_faster_than_bls:
                return h["ant_1_array"][:: self.Ntimes]
            else:
                return h["ant_1_array"][: self.Nbls]
        else:
            return np.array([x for x, y in self.antpairs])

    @cached_property
    def unique_antpair_2_array(self) -> np.ndarray:
        """The unique antenna 2 indices in the file."""
        h = self.header
        if self.blts_are_rectangular:
            if self.time_axis_faster_than_bls:
                return h["ant_2_array"][:: self.Ntimes]
            else:
                return h["ant_2_array"][: self.Nbls]
        else:
            return np.array([y for x, y in self.antpairs])

    @cached_property
    def unique_ants(self) -> set:
        """The unique antennas in the file."""
        return set(
            np.unique(
                np.concatenate(
                    (self.unique_antpair_1_array, self.unique_antpair_2_array)
                )
            )
        )

    @cached_property
    def baseline_array(self) -> np.ndarray:
        """The baselines in the file, as unique integers."""
        return uvutils.antnums_to_baseline(
            self.ant_1_array, self.ant_2_array, self.Nants_telescope
        )

    @cached_property
    def unique_baseline_array(self) -> np.ndarray:
        """The unique baselines in the file, as unique integers."""
        return uvutils.antnums_to_baseline(
            self.unique_antpair_1_array,
            self.unique_antpair_2_array,
            self.Nants_telescope,
        )

    @cached_property
    def antenna_names(self) -> list[str]:
        """The antenna names in the file."""
        return [bytes(name).decode("utf8") for name in self.header["antenna_names"][:]]

    @cached_property
    def antpairs(self) -> list[tuple[int, int]]:
        """Get the unique antenna pairs in the file."""
        if self.blts_are_rectangular:
            return list(zip(self.unique_antpair_1_array, self.unique_antpair_2_array))
        else:
            return list(set(zip(self.ant_1_array, self.ant_2_array)))

    def has_key(self, key: tuple[int, int] | tuple[int, int, str]) -> bool:
        """Check if the file has a given antpair or antpair-pol key."""
        if len(key) == 2 and key in self.antpairs or (key[1], key[0]) in self.antpairs:
            return True
        elif len(key) == 3 and (
            (key[:2] in self.antpairs and key[2] in self.pols)
            or ((key[1], key[0]) in self.antpairs and key[2][::-1] in self.pols)
        ):
            return True
        else:
            return False

    @cached_property
    def pols(self) -> list[str]:
        """The polarizations in the file, as standardized strings, eg. 'xx' or 'ee'."""
        return [
            uvutils.polnum2str(p, x_orientation=self.x_orientation)
            for p in self.polarization_array
        ]

    @cached_property
    def antpos_enu(self) -> np.ndarray:
        """The antenna positions in ENU coordinates, in meters."""
        return uvutils.ENU_from_ECEF(
            self.antenna_positions + self.telescope_location,
            *self.telescope_location_lat_lon_alt,
            frame="itrs",
        )

    @cached_property
    def telescope_location(self):
        """The telescope location in ECEF coordinates, in meters."""
        return uvutils.XYZ_from_LatLonAlt(*self.telescope_location_lat_lon_alt)

    @property
    def telescope_location_lat_lon_alt(self) -> tuple[float, float, float]:
        """The telescope location in latitude, longitude, and altitude, in degrees."""
        return self.latitude * np.pi / 180, self.longitude * np.pi / 180, self.altitude

    @property
    def telescope_location_lat_lon_alt_degrees(self) -> tuple[float, float, float]:
        """The telescope location in latitude, longitude, and altitude, in degrees."""
        return self.latitude, self.longitude, self.altitude

    @cached_property
    def vis_units(self) -> str:
        """The visibility units in the file, as a string."""
        # check for vis_units
        h = self.header
        if "vis_units" in h:
            vis_units = bytes(h["vis_units"][()]).decode("utf8")
            # Added here because older files allowed for both upper and lowercase
            # formats, although since the attribute is case sensitive, we want to
            # correct for this here.
            if vis_units == "UNCALIB":
                vis_units = "uncalib"
        else:
            # default to uncalibrated data
            vis_units = "uncalib"
        return vis_units

    def to_uvdata(self, check_lsts: bool = False) -> UVData:
        """Convert the file to a UVData object.

        The object will be metadata-only.
        """
        uvd = UVH5()
        uvd.read_uvh5(
            self,
            read_data=False,
            run_check_acceptability=check_lsts,
            use_future_array_shapes=True,
        )
        return uvd


class UVH5(UVData):
    """
    A class for UVH5 file objects.

    This class defines an HDF5-specific subclass of UVData for reading and
    writing UVH5 files. This class should not be interacted with directly,
    instead use the read_uvh5 and write_uvh5 methods on the UVData class.
    """

    def _read_header_with_fast_meta(
        self,
        filename: str | Path | FastUVH5Meta,
        run_check_acceptability: bool = True,
        blt_order: tuple[str] | None | Literal["determine"] = None,
        blts_are_rectangular: bool | None = None,
        time_axis_faster_than_bls: bool | None = None,
        background_lsts: bool = True,
        recompute_nbls: bool | None = None,
    ):
        if not isinstance(filename, FastUVH5Meta):
            obj = FastUVH5Meta(
                filename,
                blt_order=blt_order,
                blts_are_rectangular=blts_are_rectangular,
                time_axis_faster_than_bls=time_axis_faster_than_bls,
                recompute_nbls=recompute_nbls,
            )
        else:
            obj = filename

        # First, get the things relevant for setting LSTs, so that can be run in the
        # background if desired.
        self.time_array = obj.time_array
        self.telescope_location_lat_lon_alt_degrees = (
            obj.telescope_location_lat_lon_alt_degrees
        )

        if "lst_array" in obj.header:
            self.lst_array = obj.header["lst_array"][:]
            proc = None
        else:
            proc = self.set_lsts_from_time_array(background=background_lsts)

        # Required parameters
        for attr in [
            "instrument",
            "telescope_name",
            "history",
            "vis_units",
            "Nfreqs",
            "Npols",
            "Nspws",
            "Ntimes",
            "Nblts",
            "Nbls",
            "Nants_data",
            "Nants_telescope",
            "antenna_names",
            "antenna_numbers",
            "antenna_positions",
            "ant_1_array",
            "ant_2_array",
            "phase_center_id_array",
            "baseline_array",
            "integration_time",
            "freq_array",
            "spw_array",
            "channel_width",
            "polarization_array",
            "uvw_array",
            "channel_width",
            "phase_center_catalog",
        ]:
            try:
                setattr(self, attr, getattr(obj, attr))
            except AttributeError as e:
                raise KeyError(str(e)) from e

        # For now, only set the rectangularity parameters if they exist in the header of
        # the file. These could be set automatically later on, but for now we'll leave
        # that up to the user dealing with the UVData object.
        if "blts_are_rectangular" in obj.header:
            self.blts_are_rectangular = obj.blts_are_rectangular
        if "time_axis_faster_than_bls" in obj.header:
            self.time_axis_faster_than_bls = obj.time_axis_faster_than_bls

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

        # Optional parameters
        for attr in [
            "dut1",
            "earth_omega",
            "gst0",
            "rdate",
            "timesys",
            "x_orientation",
            "blt_order",
            "antenna_diameters",
            "uvplane_reference_time",
            "eq_coeffs",
            "eq_coeffs_convention",
            "flex_spw_id_array",
            "flex_spw_polarization_array",
            "phase_center_app_ra",
            "phase_center_app_dec",
            "phase_center_frame_pa",
            "extra_keywords",
        ]:
            try:
                setattr(self, attr, getattr(obj, attr))
            except AttributeError:
                pass

        if self.blt_order is not None:
            self._blt_order.form = (len(self.blt_order),)

        # We've added a few new keywords that did not exist before, so check to see if
        # any of them are in the header, and if not, mark the data set as being
        # "regular" (e.g., not a flexible spectral window setup, single source only).
        if obj.flex_spw:
            self._set_flex_spw()

        if self.flex_spw_id_array is None and not self.flex_spw:
            self.flex_spw_id_array = np.full(self.Nfreqs, self.spw_array[0], dtype=int)

        # Here is where we start handling phase center information.  If we have a
        # multi phase center dataset, we need to get different header items
        if self.phase_center_catalog is not None:
            self.Nphase = obj.Nphase
        else:
            cat_name = getattr(obj, "object_name", None)

            if obj.phase_type == "drift":
                if cat_name is None:
                    cat_name = "unprojected"
                cat_id = self._add_phase_center(cat_name, cat_type="unprojected")
            else:
                cat_id = self._add_phase_center(
                    cat_name,
                    cat_type="sidereal",
                    cat_lon=obj.phase_center_ra,
                    cat_lat=obj.phase_center_dec,
                    cat_frame=obj.phase_center_frame,
                    cat_epoch=obj.phase_center_epoch,
                )
            self.phase_center_id_array = np.zeros(self.Nblts, dtype=int) + cat_id
        # set telescope params
        try:
            self.set_telescope_params()
        except ValueError as ve:
            warnings.warn(str(ve))

        if run_check_acceptability:
            obj.check_lsts_against_times()

        if self.freq_array.ndim == 1:
            arr_shape_msg = (
                "The size of arrays in this file are not internally consistent, "
                "which should not happen. Please file an issue in our GitHub issue "
                "log so that we can fix it."
            )
            assert (
                np.asarray(self.channel_width).size == self.freq_array.size
            ), arr_shape_msg
            self._set_future_array_shapes()

        if proc is not None:
            proc.join()

    def _read_header(
        self, filename: str | Path | FastUVH5Meta | h5py.File | h5py.Group, **kwargs
    ):
        """
        Read header information from a UVH5 file.

        This is an internal function called by the user-space methods.
        Properties of the UVData object are updated as the file is processed.

        Parameters
        ----------
        header : h5py datagroup
            A reference to an h5py data group that contains the header
            information. Should be "/Header" for UVH5 files conforming to spec.

        Other Parameters
        ----------------
        All other parameters passed through to :func:`_read_header_with_fast_meta`.

        Returns
        -------
        None
        """
        self._read_header_with_fast_meta(filename, **kwargs)

    def _get_data(
        self,
        dgrp,
        antenna_nums,
        antenna_names,
        ant_str,
        bls,
        frequencies,
        freq_chans,
        times,
        time_range,
        lsts,
        lst_range,
        polarizations,
        blt_inds,
        phase_center_ids,
        catalog_names,
        data_array_dtype,
        keep_all_metadata,
        multidim_index,
    ):
        """
        Read the data-size arrays (data, flags, nsamples) from a file.

        This is an internal function to read just the visibility, flag, and
        nsample data of the UVH5 file. This is separated from full read so that
        header/metadata and data can be read independently. See the
        documentation of `read_uvh5` for a full description of most of the
        descriptions of parameters. Below we only include a description of args
        unique to this function.

        Parameters
        ----------
        dgrp : h5py datagroup
            The HDF5 datagroup containing the datasets. Should be "/Data" for
            UVH5 files conforming to spec.

        Returns
        -------
        None

        Raises
        ------
        ValueError
            This is raised if the data array read from the file is not a complex
            datatype (np.complex64 or np.complex128).
        """
        # check for bitshuffle data; bitshuffle filter number is 32008
        # TODO should we check for any other filters?
        if "32008" in dgrp["visdata"]._filters:
            if not hdf5plugin_present:  # pragma: no cover
                raise ImportError(
                    "hdf5plugin is not installed but is required to read this dataset"
                ) from hdf5plugin_error

        # figure out what data to read in
        blt_inds, freq_inds, pol_inds, history_update_string = 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,
        )

        # figure out which axis is the most selective
        if blt_inds is not None:
            blt_frac = len(blt_inds) / float(self.Nblts)
        else:
            blt_frac = 1

        if freq_inds is not None:
            freq_frac = len(freq_inds) / float(self.Nfreqs)
        else:
            freq_frac = 1

        if pol_inds is not None:
            pol_frac = len(pol_inds) / float(self.Npols)
        else:
            pol_frac = 1

        min_frac = np.min([blt_frac, freq_frac, pol_frac])

        arr_shape_msg = (
            "The size of arrays in this file are not internally consistent, "
            "which should not happen. Please file an issue in our GitHub issue "
            "log so that we can fix it."
        )

        if dgrp["visdata"].ndim == 3:
            assert self.freq_array.ndim == 1, arr_shape_msg
            assert self.channel_width.size == self.freq_array.size, arr_shape_msg
            self._set_future_array_shapes()

        # get the fundamental datatype of the visdata; if integers, we need to
        # cast to floats
        visdata_dtype = dgrp["visdata"].dtype
        if visdata_dtype not in ("complex64", "complex128"):
            _check_uvh5_dtype(visdata_dtype)
            if data_array_dtype not in (np.complex64, np.complex128):
                raise ValueError(
                    "data_array_dtype must be np.complex64 or np.complex128"
                )
            custom_dtype = True
        else:
            custom_dtype = False

        if min_frac == 1:
            # no select, read in all the data
            inds = (np.s_[:], np.s_[:], np.s_[:])
            if custom_dtype:
                self.data_array = _read_complex_astype(
                    dgrp["visdata"], inds, data_array_dtype
                )
            else:
                self.data_array = uvutils._index_dset(dgrp["visdata"], inds)
            self.flag_array = uvutils._index_dset(dgrp["flags"], inds)
            self.nsample_array = uvutils._index_dset(dgrp["nsamples"], inds)
        else:
            # do select operations on everything except data_array, flag_array
            # and nsample_array
            self._select_by_index(
                blt_inds, freq_inds, pol_inds, history_update_string, keep_all_metadata
            )

            # determine which axes can be sliced, rather than fancy indexed
            # max_nslice_frac of 0.1 yields slice speedup over fancy index for HERA data
            # See pyuvdata PR #805
            if blt_inds is not None:
                blt_slices, blt_sliceable = uvutils._convert_to_slices(
                    blt_inds, max_nslice_frac=0.1
                )
            else:
                blt_inds, blt_slices = np.s_[:], np.s_[:]
                blt_sliceable = True

            if freq_inds is not None:
                freq_slices, freq_sliceable = uvutils._convert_to_slices(
                    freq_inds, max_nslice_frac=0.1
                )
            else:
                freq_inds, freq_slices = np.s_[:], np.s_[:]
                freq_sliceable = True

            if pol_inds is not None:
                pol_slices, pol_sliceable = uvutils._convert_to_slices(
                    pol_inds, max_nslice_frac=0.5
                )
            else:
                pol_inds, pol_slices = np.s_[:], np.s_[:]
                pol_sliceable = True

            # open references to datasets
            visdata_dset = dgrp["visdata"]
            flags_dset = dgrp["flags"]
            nsamples_dset = dgrp["nsamples"]

            # check that multidim_index is appropriate
            if multidim_index:
                # if more than one dim is not sliceable, then not appropriate
                if sum([blt_sliceable, freq_sliceable, pol_sliceable]) < 2:
                    multidim_index = False

            # just read in the right portions of the data and flag arrays
            if blt_frac == min_frac:
                # construct inds list given simultaneous sliceability
                inds = [blt_inds, np.s_[:], np.s_[:]]
                if blt_sliceable:
                    inds[0] = blt_slices
                if multidim_index:
                    if freq_sliceable:
                        inds[1] = freq_slices
                    else:
                        inds[1] = freq_inds
                if multidim_index:
                    if pol_sliceable:
                        inds[2] = pol_slices
                    else:
                        inds[2] = pol_inds

                inds = tuple(inds)

                # index datasets
                if custom_dtype:
                    visdata = _read_complex_astype(visdata_dset, inds, data_array_dtype)
                else:
                    visdata = uvutils._index_dset(visdata_dset, inds)
                flags = uvutils._index_dset(flags_dset, inds)
                nsamples = uvutils._index_dset(nsamples_dset, inds)
                # down select on other dimensions if necessary
                # use indices not slices here: generally not the bottleneck
                if not multidim_index and freq_frac < 1:
                    if self.future_array_shapes:
                        visdata = visdata[:, freq_inds, :]
                        flags = flags[:, freq_inds, :]
                        nsamples = nsamples[:, freq_inds, :]
                    else:
                        visdata = visdata[:, :, freq_inds, :]
                        flags = flags[:, :, freq_inds, :]
                        nsamples = nsamples[:, :, freq_inds, :]
                if not multidim_index and pol_frac < 1:
                    if self.future_array_shapes:
                        visdata = visdata[:, :, pol_inds]
                        flags = flags[:, :, pol_inds]
                        nsamples = nsamples[:, :, pol_inds]
                    else:
                        visdata = visdata[:, :, :, pol_inds]
                        flags = flags[:, :, :, pol_inds]
                        nsamples = nsamples[:, :, :, pol_inds]

            elif freq_frac == min_frac:
                # construct inds list given simultaneous sliceability
                inds = [np.s_[:], freq_inds, np.s_[:]]
                if freq_sliceable:
                    inds[1] = freq_slices
                if multidim_index:
                    if blt_sliceable:
                        inds[0] = blt_slices
                    else:
                        inds[0] = blt_inds
                if multidim_index:
                    if pol_sliceable:
                        inds[2] = pol_slices
                    else:
                        inds[2] = pol_inds

                inds = tuple(inds)

                # index datasets
                if custom_dtype:
                    visdata = _read_complex_astype(visdata_dset, inds, data_array_dtype)
                else:
                    visdata = uvutils._index_dset(visdata_dset, inds)
                flags = uvutils._index_dset(flags_dset, inds)
                nsamples = uvutils._index_dset(nsamples_dset, inds)

                # down select on other dimensions if necessary
                # use indices not slices here: generally not the bottleneck
                if not multidim_index and blt_frac < 1:
                    visdata = visdata[blt_inds]
                    flags = flags[blt_inds]
                    nsamples = nsamples[blt_inds]
                if not multidim_index and pol_frac < 1:
                    if self.future_array_shapes:
                        visdata = visdata[:, :, pol_inds]
                        flags = flags[:, :, pol_inds]
                        nsamples = nsamples[:, :, pol_inds]
                    else:
                        visdata = visdata[:, :, :, pol_inds]
                        flags = flags[:, :, :, pol_inds]
                        nsamples = nsamples[:, :, :, pol_inds]

            else:
                # construct inds list given simultaneous sliceability
                inds = [np.s_[:], np.s_[:], pol_inds]
                if pol_sliceable:
                    inds[2] = pol_slices
                if multidim_index:
                    if blt_sliceable:
                        inds[0] = blt_slices
                    else:
                        inds[0] = blt_inds
                if multidim_index:
                    if freq_sliceable:
                        inds[1] = freq_slices
                    else:
                        inds[1] = freq_inds

                inds = tuple(inds)

                # index datasets
                if custom_dtype:
                    visdata = _read_complex_astype(visdata_dset, inds, data_array_dtype)
                else:
                    visdata = uvutils._index_dset(visdata_dset, inds)
                flags = uvutils._index_dset(flags_dset, inds)
                nsamples = uvutils._index_dset(nsamples_dset, inds)

                # down select on other dimensions if necessary
                # use indices not slices here: generally not the bottleneck
                if not multidim_index and blt_frac < 1:
                    visdata = visdata[blt_inds]
                    flags = flags[blt_inds]
                    nsamples = nsamples[blt_inds]
                if not multidim_index and freq_frac < 1:
                    if self.future_array_shapes:
                        visdata = visdata[:, freq_inds, :]
                        flags = flags[:, freq_inds, :]
                        nsamples = nsamples[:, freq_inds, :]
                    else:
                        visdata = visdata[:, :, freq_inds, :]
                        flags = flags[:, :, freq_inds, :]
                        nsamples = nsamples[:, :, freq_inds, :]

            # save arrays in object
            self.data_array = visdata
            self.flag_array = flags
            self.nsample_array = nsamples

        if self.data_array.ndim == 3:
            assert self.freq_array.ndim == 1, arr_shape_msg
            assert self.channel_width.size == self.freq_array.size, arr_shape_msg
            self._set_future_array_shapes()

        return

    def read_uvh5(
        self,
        filename,
        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,
        keep_all_metadata=True,
        read_data=True,
        data_array_dtype=np.complex128,
        multidim_index=False,
        remove_flex_pol=True,
        background_lsts=True,
        run_check=True,
        check_extra=True,
        run_check_acceptability=True,
        strict_uvw_antpos_check=False,
        fix_old_proj=None,
        fix_use_ant_pos=True,
        check_autos=True,
        fix_autos=True,
        use_future_array_shapes=False,
        blt_order: tuple[str] | Literal["determine"] | None = None,
        blts_are_rectangular: bool | None = None,
        time_axis_faster_than_bls: bool | None = None,
        recompute_nbls: bool | None = None,
    ):
        """
        Read in data from a UVH5 file.

        Parameters
        ----------
        filename : str
             The UVH5 file to read from.
        antenna_nums : array_like of int, optional
            The antennas numbers to include when reading data into 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. Ignored if read_data is False.
        antenna_names : array_like of str, optional
            The antennas names to include when reading data into 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_nums` is also provided. Ignored if read_data 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 include when reading data into 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. Ignored if read_data is False.
        ant_str : str, optional
            A string containing information about what antenna numbers
            and polarizations to include when reading 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 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. Ignored if read_data is False.
        frequencies : array_like of float, optional
            The frequencies to include when reading data into the object, each
            value passed here should exist in the freq_array. Ignored if
            read_data is False.
        freq_chans : array_like of int, optional
            The frequency channel numbers to include when reading data into the
            object. Ignored if read_data is False.
        times : array_like of float, optional
            The times to include when reading data into the object, each value
            passed here should exist in the time_array. Cannot be used with
            `time_range`.
        time_range : array_like of float, optional
            The time range in Julian Date to keep in the object, 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 numbers to include when reading data into the
            object, each value passed here should exist in the polarization_array.
            Ignored if read_data is False.
        blt_inds : array_like of int, optional
            The baseline-time indices to include when reading data into the
            object. This is not commonly used. Ignored if read_data is False.
        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.
        keep_all_metadata : bool
            Option to keep all the metadata associated with antennas, even those
            that do not have data associated with them after the select option.
        read_data : bool
            Read in the visibility, nsample 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.
        data_array_dtype : numpy dtype
            Datatype to store the output data_array as. Must be either
            np.complex64 (single-precision real and imaginary) or np.complex128 (double-
            precision real and imaginary). Only used if the datatype of the visibility
            data on-disk is not 'c8' or 'c16'.
        multidim_index : bool
            If True, attempt to index the HDF5 dataset
            simultaneously along all data axes. Otherwise index one axis at-a-time.
            This only works if data selection is sliceable along all but one axis.
            If indices are not well-matched to data chunks, this can be slow.
        remove_flex_pol : bool
            If True and if the file is a flex_pol file, convert back to a standard
            UVData 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 after reading in the file (the default is True,
            meaning the check will be run). Ignored if read_data is False.
        check_extra : bool
            Option to check optional parameters as well as required ones (the
            default is True, meaning the optional parameters will be checked).
            Ignored if read_data is False.
        run_check_acceptability : bool
            Option to check acceptable range of the values of parameters after
            reading in the file (the default is True, meaning the acceptable
            range check will be done). Ignored if read_data is False.
        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.
        fix_old_proj : bool
            Applies a fix to uvw-coordinates and phasing, assuming that the old `phase`
            method was used prior to writing the data, which had errors of the order of
            one part in 1e4 - 1e5. See the phasing memo for more details. Default is
            to apply the correction if the attributes `phase_center_app_ra` and
            `phase_center_app_dec` are missing (as they were introduced alongside the
            new phasing method).
        fix_use_ant_pos : bool
            If setting `fix_old_proj` to True, use the antenna positions to derive the
            correct uvw-coordinates rather than using the baseline vectors. Default is
            True.
        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 True.
        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.
        blt_order : tuple of str or "determine", optional
            The order of the baseline-time axis. This can be determined, or read
            directly from file, however since it has been optional in the past, many
            existing files do not contain it in the metadata.
            Some reading operations are significantly faster if this is known, so
            providing it here can provide a speedup. Default is to try and read it from
            file, and if not there, just leave it as None. Set to "determine" to
            auto-detect the blt_order from the metadata (takes extra time to do so).
        blts_are_rectangular : bool, optional
            Whether the baseline-time axis is rectangular. This can be read from
            metadata in new files, but many old files do not contain it. If not
            provided, the rectangularity will be determined from the data. This is a
            non-negligible operation, so if you know it, it can be provided here to
            speed up reading.
        time_axis_faster_than_bls : bool, optional
            If blts are rectangular, this variable specifies whether the time axis is
            the fastest-moving virtual axis. Various reading functions benefit from
            knowing this, so if it is known, it can be provided here to speed up
            reading. It will be determined from the data if not provided.
        recompute_nbls : bool, optional
            Whether to recompute the number of unique baselines from the data. Before
            v1.2 of the UVH5 spec, it was possible to have an incorrect number of
            baselines in the header without error, so this provides an opportunity to
            rectify it. Old HERA files (< March 2023) may have this issue, but in this
            case the correct number of baselines can be computed more quickly than by
            fully re=computing, and so we do this.

        Returns
        -------
        None

        Raises
        ------
        IOError
            If filename doesn't exist.
        ValueError
            If the data_array_dtype is not a complex dtype.
            If incompatible select keywords are set (e.g. `ant_str` with other
            antenna selectors, `times` and `time_range`) or select keywords
            exclude all data or if keywords are set to the wrong type.

        """
        if isinstance(filename, FastUVH5Meta):
            meta = filename
            filename = str(meta.path)
        else:
            meta = FastUVH5Meta(
                filename,
                blt_order=blt_order,
                blts_are_rectangular=blts_are_rectangular,
                time_axis_faster_than_bls=time_axis_faster_than_bls,
                recompute_nbls=recompute_nbls,
            )

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

        # open hdf5 file for reading
        self._read_header(
            meta,
            run_check_acceptability=run_check_acceptability,
            background_lsts=background_lsts,
        )

        if read_data:
            # Now read in the data
            self._get_data(
                meta.datagrp,
                antenna_nums,
                antenna_names,
                ant_str,
                bls,
                frequencies,
                freq_chans,
                times,
                time_range,
                lsts,
                lst_range,
                polarizations,
                blt_inds,
                phase_center_ids,
                catalog_names,
                data_array_dtype,
                keep_all_metadata,
                multidim_index,
            )

        # Finally, backfill the apparent coords if they aren't in the original datafile
        add_app_coords = (
            self.phase_center_app_ra is None
            or (self.phase_center_app_dec is None)
            or (self.phase_center_frame_pa is None)
        )
        if add_app_coords:
            self._set_app_coords_helper()

        # Default behavior for UVH5 is to fix phasing if the problem is
        # detected, since the absence of the app coord attributes is the most
        # clear indicator of the old phasing algorithm being used. Double-check
        # the multi-phase-ctr attribute just to be extra safe.
        old_phase_compatible, _ = self._old_phase_attributes_compatible()
        if np.any(~self._check_for_cat_type("unprojected")) and old_phase_compatible:
            if (fix_old_proj) or (fix_old_proj is None and add_app_coords):
                self.fix_phase(use_ant_pos=fix_use_ant_pos)
            elif add_app_coords:
                warnings.warn(
                    "This data appears to have been phased-up using the old "
                    "`phase` method, which is incompatible with the current set of "
                    "methods. Please run the `fix_phase` method (or set "
                    "`fix_old_proj=True` when loading the dataset) to address this "
                    "issue."
                )

        if remove_flex_pol:
            self.remove_flex_pol()

        if use_future_array_shapes:
            if not self.future_array_shapes:
                self.use_future_array_shapes()
        else:
            warnings.warn(_future_array_shapes_warning, DeprecationWarning)
            if self.future_array_shapes:
                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()

        # check if object has all required UVParameters set
        if run_check:
            self.check(
                check_extra=check_extra,
                run_check_acceptability=run_check_acceptability,
                strict_uvw_antpos_check=strict_uvw_antpos_check,
                allow_flip_conj=True,
                check_autos=check_autos,
                fix_autos=fix_autos,
            )

        return

    def _write_header(self, header):
        """
        Write data to the header datagroup of a UVH5 file.

        Parameters
        ----------
        header : h5py datagroup
            The datagroup to write the header information to. For a UVH5 file
            conforming to the spec, it should be "/Header"

        Returns
        -------
        None
        """
        # write out UVH5 version information
        assert_err_msg = (
            "This is a bug, please make an issue in our issue log at "
            "https://github.com/RadioAstronomySoftwareGroup/pyuvdata/issues"
        )
        assert self.future_array_shapes, assert_err_msg
        header["version"] = np.string_("1.2")

        # write out telescope and source information
        header["latitude"] = self.telescope_location_lat_lon_alt_degrees[0]
        header["longitude"] = self.telescope_location_lat_lon_alt_degrees[1]
        header["altitude"] = self.telescope_location_lat_lon_alt_degrees[2]
        header["telescope_name"] = np.string_(self.telescope_name)
        header["instrument"] = np.string_(self.instrument)

        # write out required UVParameters
        header["Nants_data"] = self.Nants_data
        header["Nants_telescope"] = self.Nants_telescope
        header["Nbls"] = self.Nbls
        header["Nblts"] = self.Nblts
        header["Nfreqs"] = self.Nfreqs
        header["Npols"] = self.Npols
        header["Nspws"] = self.Nspws
        header["Ntimes"] = self.Ntimes
        header["antenna_numbers"] = self.antenna_numbers
        header["uvw_array"] = self.uvw_array
        header["vis_units"] = np.string_(self.vis_units)
        header["channel_width"] = self.channel_width
        header["time_array"] = self.time_array
        header["freq_array"] = self.freq_array
        header["integration_time"] = self.integration_time
        header["lst_array"] = self.lst_array
        header["polarization_array"] = self.polarization_array
        header["spw_array"] = self.spw_array
        header["ant_1_array"] = self.ant_1_array
        header["ant_2_array"] = self.ant_2_array
        header["antenna_positions"] = self.antenna_positions
        header["flex_spw"] = self.flex_spw
        # handle antenna_names; works for lists or arrays
        header["antenna_names"] = np.asarray(self.antenna_names, dtype="bytes")

        # write out phasing information
        # Write out the catalog, if available
        header["phase_center_id_array"] = self.phase_center_id_array
        header["Nphase"] = self.Nphase
        # this is a dict of dicts. Top level key is the phase_center_id,
        # next level keys give details for each phase center.
        pc_group = header.create_group("phase_center_catalog")
        for pc, pc_dict in self.phase_center_catalog.items():
            this_group = pc_group.create_group(str(pc))
            for key, value in pc_dict.items():
                if isinstance(value, str):
                    this_group[key] = np.bytes_(value)
                elif value is None:
                    this_group[key] = h5py.Empty("f")
                else:
                    this_group[key] = value
        header["phase_center_app_ra"] = self.phase_center_app_ra
        header["phase_center_app_dec"] = self.phase_center_app_dec
        header["phase_center_frame_pa"] = self.phase_center_frame_pa

        # write out optional parameters
        if self.dut1 is not None:
            header["dut1"] = self.dut1
        if self.earth_omega is not None:
            header["earth_omega"] = self.earth_omega
        if self.gst0 is not None:
            header["gst0"] = self.gst0
        if self.rdate is not None:
            header["rdate"] = np.string_(self.rdate)
        if self.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/uvdata/initializers.py
"""A module defining functions for initializing UVData objects from scratch."""
from typing import Any, Literal, Sequence

import numpy as np
from astropy import units as un
from astropy.coordinates import EarthLocation, SkyCoord
from astropy.time import Time

from .. import __version__, utils

try:
    from lunarsky import MoonLocation

    hasmoon = True
    Locations = MoonLocation | EarthLocation
except ImportError:
    hasmoon = False
    Locations = EarthLocation


def new_uvdata(
    freq_array: np.ndarray,
    polarization_array: np.ndarray | list[str | int] | tuple[str | int],
    antenna_positions: np.ndarray | dict[str | int, np.ndarray],
    telescope_location: Locations,
    telescope_name: str,
    unique_times: np.ndarray | list[float] | tuple[float] | None = None,
    time_array: np.ndarray | None = None,
    unique_antpairs: Sequence[tuple[int, int]] | None = None,
    antpair_array: np.ndarray
    | list[tuple[int, int]]
    | tuple[tuple[int, int]]
    | 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,
    blts_are_rectangular: bool | None = None,
    data_array: np.ndarray | None = None,
    flag_array: np.ndarray | None = None,
    nsample_array: np.ndarray | None = None,
    flex_spw: bool = False,
    history: str = "",
    instrument: str = "",
    vis_units: Literal["Jy", "K str", "uncalib"] = "uncalib",
    antname_format: str = "{0:03d}",
    empty: bool = False,
    time_axis_faster_than_bls: bool | None = None,
    phase_center_catalog: dict[str, Any] | None = None,
    phase_center_id_array: np.ndarray | None = None,
    **kwargs,
):
    """Initialize a new UVData object from keyword arguments.

    Parameters
    ----------
    freq_array : ndarray of float
        Array of frequencies in Hz.
    polarization_array : sequence of int or str
        Array of polarization integers or strings (eg. 'xx' or 'ee')
    antenna_positions : ndarray of float or dict of ndarray of float
        Array of antenna positions in ECEF coordinates in meters.
        If a dict, keys are antenna names or numbers and values are
        antenna positions in ECEF coordinates in meters.
    telescope_location : astropy EarthLocation or MoonLocation
        Location of the telescope.
    telescope_name : str
        Name of the telescope.
    unique_times : ndarray of float, optional
        Array of unique times in Julian Date. If provided, unique_antpairs must also
        be provided, and the blt axis of the UVData object will be formed from the
        outer product of unique_times and unique_antpairs. If not provided, time_array
        must be provided.
    time_array : ndarray of float, optional
        Array of times in Julian Date. If provided, antpair_array must also be
        provided. Required if unique_times is not provided. If time_array is provided,
        it will be passed on as-is to the UVData object.
    unique_antpairs : sequence of tuple of int, optional
        Sequence of unique antenna pairs in the data. If provided, unique_times must
        also be provided. If not, antpair_array must be provided. If provided, the blt
        axis of the UVData object will be formed from the outer product of unique_times
        and unique_antpairs.
    antpair_array : ndarray of int, optional
        Array of antenna pairs in the data. If provided, time_array must also be
        provided. Required if unique_antpairs is not provided. If antpair_array is
        provided, it will be converted to baseline_array in the same order as provided,
        and this is provided to the UVData object.
    integration_time : float or ndarray of float, optional
        Integration time in seconds. If not provided, it will be derived from the
        time_array, as the difference between successive times (with the last time
        appended). If a float is provided, it will be used for all integrations.
        If an ndarray is provided, it must have the same shape as time_array.
    channel_width : float or ndarray of float, optional
        Channel width in Hz. If not provided, it will be derived from the freq_array,
        as the difference between successive frequencies (with the last frequency
        appended). If a float is provided, it will be used for all channels.
        If an ndarray is provided, it must have the same shape as freq_array.
    antenna_names : list of str, optional
        List of antenna names. If not provided, antenna numbers will be used to form
        the antenna_names, according to the antname_format. antenna_names need not be
        provided if antenna_positions is a dict with string keys.
    antenna_numbers : list of int, optional
        List of antenna numbers. If not provided, antenna names will be used to form
        the antenna_numbers, but in this case the antenna_names must be strings that
        can be converted to integers. antenna_numbers need not be provided if
        antenna_positions is a dict with integer keys.
    blts_are_rectangular : bool, optional
        Set to True if the time_array and antpair_array are rectangular, i.e. if
        they are formed from the outer product of a unique set of times/antenna pairs.
        If not provided, it will be inferred from the provided arrays.
    data_array : ndarray of complex, optional
        Array of data. If not provided, and ``empty=True`` it will be initialized to all
        zeros. Otherwise a purely metadata-only object will be created. It must have
        shape ``(Nblts, Nfreqs, Npols)``
    flag_array : ndarray of bool, optional
        Array of flags. If not provided, and ``empty=True`` it will be initialized to
        all False. Otherwise a purely metadata-only object will be created. It must have
        shape ``(Nblts, Nfreqs, Npols)``
    nsample_array : ndarray of float, optional
        Array of nsamples. If not provided, and ``empty=True`` it will be initialized to
        all zeros. Otherwise a purely metadata-only object will be created. It must have
        shape ``(Nblts, Nfreqs, Npols)``
    flex_spw : bool, optional
        Set to True if the frequency axis is not contiguous. Default is False.
    history : str, optional
        History string to be added to the object. Default is a simple string
        containing the date and time and pyuvdata version.
    instrument : str, optional
        Instrument name. Default is the ``telescope_name``.
    vis_units : str, optional
        Visibility units. Default is 'uncalib'. Must be one of 'Jy', 'K str', or
        'uncalib'.
    antname_format : str, optional
        Format string for antenna names. Default is '{0:03d}'.
    empty : bool, optional
        Set to True to create an empty (but not metadata-only) UVData object.
        Default is False.
    time_axis_faster_than_bls : bool, optional
        Set to True if the time axis is faster than the baseline axis, under the
        assumption that the blt axis is rectangular. This *sets* the blt order in the
        case that unique times and unique baselines are provided, otherwise it is
        inferred from the provided arrays.
    phase_center_catalog : dict, optional
        Dictionary of phase center information. Each key is a source id, and each
        value is a dictionary. By default, a single phase center is assumed, at
        zenith, assumed to be unprojected (i.e. drift scan).
    phase_center_id_array : ndarray of int, optional
        Array of phase center ids. If not provided, it will be initialized to the first
        id found in ``phase_center_catalog``. It must have shape ``(Nblts,)``.

    Other Parameters
    ----------------
    All other keyword parameters are set on the UVData object as attributes.

    Returns
    -------
    obj : UVData object
        A UVData object with the specified metadata.
    """
    # import here to avoid circular import
    from pyuvdata import UVData

    obj = UVData()

    antenna_positions, antenna_names, antenna_numbers = get_antenna_params(
        antenna_positions, antenna_names, antenna_numbers, antname_format
    )

    baseline_array, unique_baselines = get_baseline_params(
        antenna_numbers, antpair_array, unique_antpairs
    )

    (
        nbls,
        ntimes,
        blts_are_rectangular,
        time_axis_faster_than_bls,
        time_array,
        baseline_array,
    ) = configure_blt_rectangularity(
        time_array=time_array,
        unique_times=unique_times,
        baseline_array=baseline_array,
        unique_baselines=unique_baselines,
        blts_are_rectangular=blts_are_rectangular,
        time_axis_faster_than_bls=time_axis_faster_than_bls,
    )

    # Re-get the ant arrays because the baseline array may have changed
    ant_1_array, ant_2_array = utils.baseline_to_antnums(
        baseline_array, Nants_telescope=len(antenna_numbers)
    )

    lst_array, integration_time = get_time_params(
        telescope_location, time_array, integration_time
    )

    freq_array, channel_width = get_freq_params(freq_array, channel_width)

    polarization_array = np.array(polarization_array)
    if polarization_array.dtype.kind != "i":
        polarization_array = utils.polstr2num(polarization_array)

    if not instrument:
        instrument = telescope_name

    if vis_units not in ["Jy", "K str", "uncalib"]:
        raise ValueError("vis_units must be one of 'Jy', 'K str', or 'uncalib'.")

    phase_center_catalog, phase_center_id_array, app_ra, app_dec = get_phase_params(
        phase_center_catalog, phase_center_id_array, telescope_location, time_array
    )

    history += (
        f"Object created by new_uvdata() at {Time.now().iso} using "
        f"pyuvdata version {__version__}."
    )

    # Now set all the metadata
    obj.freq_array = freq_array
    obj.polarization_array = polarization_array
    obj.antenna_positions = antenna_positions
    obj.telescope_location = (
        [
            telescope_location.x.to_value("m"),
            telescope_location.y.to_value("m"),
            telescope_location.z.to_value("m"),
        ],
    )
    obj.telescope_name = telescope_name
    obj.baseline_array = baseline_array
    obj.ant_1_array = ant_1_array
    obj.ant_2_array = ant_2_array
    obj.time_array = time_array
    obj.lst_array = lst_array
    obj.integration_time = integration_time
    obj.channel_width = channel_width
    obj.antenna_names = antenna_names
    obj.antenna_numbers = antenna_numbers
    obj.flex_spw = flex_spw
    obj.history = history
    obj.instrument = instrument
    obj.vis_units = vis_units
    obj.Nants_data = len(set(np.concatenate([ant_1_array, ant_2_array])))
    obj.Nants_telescope = len(antenna_numbers)
    obj.Nbls = nbls
    obj.Nblts = len(baseline_array)
    obj.Nfreqs = len(freq_array)
    obj.Npols = len(polarization_array)
    obj.Ntimes = ntimes
    obj.Nspws = 1
    obj.spw_array = np.array([0])
    obj.future_array_shapes = True
    obj.phase_center_catalog = phase_center_catalog
    obj.phase_center_id_array = phase_center_id_array
    obj.phase_center_app_ra = app_ra
    obj.phase_center_app_dec = app_dec

    obj.set_uvws_from_antenna_positions(update_vis=False)

    # Set optional parameters that the user passes in
    for key, value in kwargs.items():
        if hasattr(obj, key):
            setattr(obj, key, value)
        else:
            raise ValueError(f"Keyword argument {key} is not a valid UVData attribute.")

    # Set data parameters
    shape = (obj.Nblts, obj.Nfreqs, obj.Npols)

    if empty:
        obj.data_array = np.zeros(shape, dtype=complex)
        obj.flag_array = np.zeros(shape, dtype=bool)
        obj.nsample_array = np.ones(shape, dtype=float)

    elif data_array is not None:
        if data_array.shape != shape:
            raise ValueError(
                f"Data array shape {data_array.shape} does not match "
                f"expected shape {shape}."
            )

        obj.data_array = data_array

        if flag_array is not None:
            if flag_array.shape != shape:
                raise ValueError(
                    f"Flag array shape {flag_array.shape} does not match expected "
                    f"shape {shape}."
                )
            obj.flag_array = flag_array
        else:
            obj.flag_array = np.zeros(shape, dtype=bool)

        if nsample_array is not None:
            if nsample_array.shape != shape:
                raise ValueError(
                    f"nsample array shape {nsample_array.shape} does not match expected"
                    f" shape {shape}."
                )
            obj.nsample_array = nsample_array
        else:
            obj.nsample_array = np.ones(shape, dtype=float)

    return obj


def get_antenna_params(
    antenna_positions: np.ndarray | dict[str | int, np.ndarray],
    antenna_names: list[str] | None = None,
    antenna_numbers: list[int] | None = None,
    antname_format: str = "{0:03d}",
) -> tuple[np.ndarray, list[str], list[int]]:
    """Configure antenna parameters for new UVData object."""
    # Get Antenna Parameters
    if isinstance(antenna_positions, dict):
        if isinstance(next(iter(antenna_positions.keys())), int):
            antenna_numbers = list(antenna_positions.keys())
        else:
            antenna_names = list(antenna_positions.keys())
        antenna_positions = np.array(list(antenna_positions.values()))

    if antenna_numbers is None and antenna_names is None:
        raise ValueError(
            "Either antenna_numbers or antenna_names must be provided unless "
            "antenna_positions is a dict."
        )

    if antenna_names is None:
        antenna_names = [antname_format.format(i) for i in antenna_numbers]
    elif antenna_numbers is None:
        try:
            antenna_numbers = [int(name) for name in antenna_names]
        except ValueError as e:
            raise ValueError(
                "Antenna names must be integers if antenna_numbers is not provided."
            ) from e

    if not isinstance(antenna_positions, np.ndarray):
        raise ValueError("antenna_positions must be a numpy array or a dictionary.")

    if antenna_positions.shape != (len(antenna_numbers), 3):
        raise ValueError(
            "antenna_positions must be a 2D array with shape (N_antennas, 3), "
            f"got {antenna_positions.shape}"
        )

    if len(antenna_names) != len(set(antenna_names)):
        raise ValueError("Duplicate antenna names found.")

    if len(antenna_numbers) != len(set(antenna_numbers)):
        raise ValueError("Duplicate antenna numbers found.")

    if len(antenna_numbers) != len(antenna_names):
        raise ValueError("antenna_numbers and antenna_names must have the same length.")

    return antenna_positions, antenna_names, antenna_numbers


def get_time_params(
    telescope_location: Locations,
    time_array: np.ndarray,
    integration_time: float | np.ndarray | None = None,
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
    """Configure time parameters for new UVData object."""
    if not isinstance(time_array, np.ndarray):
        raise ValueError("time_array must be a numpy array.")

    t = Time(time_array, format="jd", scale="utc", location=telescope_location)
    lst_array = t.sidereal_time("apparent").radian

    if integration_time is None:
        integration_time = (t[1:] - t[:-1]).to_value("s")
        integration_time = np.concatenate([integration_time, integration_time[-1:]])
    elif np.isscalar(integration_time):
        integration_time = np.full_like(time_array, integration_time)

    if not isinstance(integration_time, np.ndarray):
        raise ValueError("integration_time must be a numpy array or a scalar.")

    if integration_time.shape != time_array.shape:
        raise ValueError("integration_time must be the same shape as time_array.")

    return lst_array, integration_time


def get_freq_params(
    freq_array: np.ndarray, channel_width: float | np.ndarray | None = None
) -> tuple[np.ndarray, np.ndarray]:
    """Configure frequency parameters for new UVData object."""
    if not isinstance(freq_array, np.ndarray):
        raise ValueError("freq_array must be a numpy array.")

    if channel_width is None:
        channel_width = freq_array[1:] - freq_array[:-1]
        channel_width = np.concatenate([channel_width, channel_width[-1:]])
    elif np.isscalar(channel_width):
        channel_width = np.full_like(freq_array, channel_width)

    if not isinstance(channel_width, np.ndarray):
        raise ValueError("channel_width must be a numpy array or a scalar.")

    if channel_width.shape != freq_array.shape:
        raise ValueError("channel_width must be the same shape as freq_array.")

    return freq_array, channel_width


def get_baseline_params(
    antenna_positions: np.ndarray,
    antpairs: Sequence[tuple[int, int]] | None = None,
    unique_antpairs: Sequence[tuple[int, int]] | None = None,
) -> tuple[np.ndarray, np.ndarray]:
    """Configure baseline parameters for new UVData object."""
    if unique_antpairs is None and antpairs is None:
        raise ValueError("Either antpairs or unique_antpairs must be provided.")

    if unique_antpairs is None:
        ant_1_array, ant_2_array = zip(*antpairs)
        baseline_array = utils.antnums_to_baseline(
            ant_1_array, ant_2_array, len(antenna_positions)
        )
        unique_baselines = None
    else:
        ant_1_array, ant_2_array = zip(*unique_antpairs)
        unique_baselines = utils.antnums_to_baseline(
            ant_1_array, ant_2_array, len(antenna_positions)
        )
        baseline_array = None

    return baseline_array, unique_baselines


def configure_blt_rectangularity(
    time_array: np.ndarray | None = None,
    unique_times: np.ndarray | None = None,
    baseline_array: np.ndarray | None = None,
    unique_baselines: np.ndarray | None = None,
    blts_are_rectangular: bool | None = None,
    time_axis_faster_than_bls: bool | None = None,
):
    """Configure blt rectangularity parameters for new UVData object."""
    if unique_baselines is None and baseline_array is None:
        raise ValueError("Either baseline_array or unique_baselines must be provided.")

    if unique_times is None and time_array is None:
        raise ValueError("Either time_array or unique_times must be provided.")

    if unique_baselines is not None and baseline_array is not None:
        raise ValueError(
            "Only one of baseline_array or unique_baselines can be provided."
        )

    if unique_times is not None and time_array is not None:
        raise ValueError("Only one of time_array or unique_times can be provided.")

    if unique_times is not None and baseline_array is not None:
        raise ValueError(
            "If unique_times are provided, unique_baselines must be provided "
            "(not baseline_array)."
        )

    if unique_baselines is not None and time_array is not None:
        raise ValueError(
            "If unique_baselines are provided, unique_times must be provided as well "
            "(not time_array)."
        )

    if unique_times is not None and blts_are_rectangular is False:
        raise ValueError(
            "If unique_times are provided, blts_are_rectangular must be True."
        )

    if unique_times is not None:
        # We are rectangular
        blts_are_rectangular = True

        if time_axis_faster_than_bls is None:
            time_axis_faster_than_bls = True

        if time_axis_faster_than_bls:
            time_array = np.tile(unique_times, len(unique_baselines))
            baseline_array = np.repeat(unique_baselines, len(unique_times))
        else:
            baseline_array = np.tile(unique_baselines, len(unique_times))
            time_array = np.repeat(unique_times, len(unique_baselines))
    elif blts_are_rectangular:
        time_axis_faster_than_bls = np.abs(time_array[1] - time_array[0]) > 0

        unique_times = np.unique(time_array)

        if time_axis_faster_than_bls:
            unique_baselines = baseline_array[:: len(unique_times)]
        else:
            nbls = len(baseline_array) // len(unique_times)
            unique_baselines = baseline_array[:nbls]

    elif blts_are_rectangular is False:
        time_axis_faster_than_bls = None
        unique_baselines = np.unique(baseline_array)
        unique_times = np.unique(time_array)
    else:
        # We don't know if it's rectangular or not.
        # Let's try to figure it out.
        unique_baselines = np.unique(baseline_array)
        unique_times = np.unique(time_array)

        (
            blts_are_rectangular,
            time_axis_faster_than_bls,
        ) = utils.determine_rectangularity(
            time_array=time_array,
            baseline_array=baseline_array,
            nbls=len(unique_baselines),
            ntimes=len(unique_times),
        )

    return (
        len(unique_baselines),
        len(unique_times),
        blts_are_rectangular,
        time_axis_faster_than_bls,
        time_array,
        baseline_array,
    )


def get_phase_params(
    phase_center_catalog, phase_center_id_array, telescope_location, time_array
):
    """Configure phase center parameters for new UVData object."""
    if phase_center_catalog is None:
        phase_center_catalog = {
            0: {
                "cat_type": "unprojected",
                "cat_lon": 0.0,
                "cat_lat": np.pi / 2,
                "cat_name": "zenith",
                "cat_frame": "altaz",
            }
        }

    if phase_center_id_array is None:
        phase_center_id_array = np.full(
            len(time_array), next(iter(phase_center_catalog.keys())), dtype=int
        )

    app_ra = np.zeros_like(time_array)
    app_dec = np.zeros_like(time_array)
    for key, cat in phase_center_catalog.items():
        if cat["cat_frame"] == "altaz":
            these_times = time_array[phase_center_id_array == key]
            unique_times, indx = np.unique(these_times, return_inverse=True)

            t = Time(
                unique_times, format="jd", scale="utc", location=telescope_location
            )

            coords = SkyCoord(
                alt=np.ones(len(t)) * cat["cat_lat"] * un.rad,
                az=np.ones(len(t)) * cat["cat_lon"] * un.rad,
                frame="altaz",
                obstime=t,
                location=telescope_location,
            )
            ra = coords.icrs.ra.rad[indx]
            dec = coords.icrs.dec.rad[indx]

            app_ra[phase_center_id_array == key] = ra
            app_dec[phase_center_id_array == key] = dec
        elif cat["cat_frame"] == "icrs":
            app_ra[phase_center_id_array == key] = cat["cat_lon"]
            app_dec[phase_center_id_array == key] = cat["cat_lat"]
        else:
            raise NotImplementedError(f"Unrecognized cat_frame: {cat['cat_frame']}")

    return phase_center_catalog, phase_center_id_array, app_ra, app_dec
