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.
            Only used if file_type is 'mwa_corr_fits'.
        phase_to_pointing_center : bool
            Flag to phase to the pointing center. Only used if file_type is
            'mwa_corr_fits'. Cannot be set if phase_center_radec is not None.
        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).
        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.
        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.
        isource : int
            Source code for MIR dataset
        irec : int
            Receiver code for MIR dataset
        isb : int
            Sideband code for MIR dataset
        corrchunk : int
            Correlator chunk code for MIR dataset
        pseudo_cont : boolean
            Read in only pseudo-continuuum values in MIR dataset. Default is false.
        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.
        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. Only applies to MIRIAD files.
        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
            Option to make the output a multi phase center dataset, capable of holding
            data on multiple phase centers. By default, this is only done if reading
            in a file with multiple sources.
        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.

        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 the data are multi source or have multiple
            spectral windows.
            If phase_center_radec is not None and is not length 2.

        """
        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
                    basename, 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:
                    basename, 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:
                basename, 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:
                    self.read(
                        filename[file_num],
                        file_type=file_type,
                        antenna_nums=antenna_nums,
                        antenna_names=antenna_names,
                        ant_str=ant_str,
                        bls=bls,
                        frequencies=frequencies,
                        freq_chans=freq_chans,
                        times=times,
                        polarizations=polarizations,
                        blt_inds=blt_inds,
                        time_range=time_range,
                        lsts=lsts,
                        lst_range=lst_range,
                        keep_all_metadata=keep_all_metadata,
                        read_data=read_data,
                        phase_type=phase_type,
                        correct_lat_lon=correct_lat_lon,
                        use_model=use_model,
                        data_column=data_column,
                        pol_order=pol_order,
                        data_array_dtype=data_array_dtype,
                        nsample_array_dtype=nsample_array_dtype,
                        skip_bad_files=skip_bad_files,
                        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,
                        isource=None,
                        irec=irec,
                        isb=isb,
                        corrchunk=corrchunk,
                        pseudo_cont=pseudo_cont,
                        calc_lst=calc_lst,
                        fix_old_proj=fix_old_proj,
                        fix_use_ant_pos=fix_use_ant_pos,
                        make_multi_phase=make_multi_phase,
                    )
                    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 (
                allow_rephase
                and phase_center_radec is None
                and not unphase_to_drift
                and self.phase_type == "phased"
                and not self.multi_phase_center
                and not make_multi_phase
            ):
                # set the phase center to be the phase center of the first file
                phase_center_radec = [self.phase_center_ra, self.phase_center_dec]
                phase_frame = self.phase_center_frame
                phase_epoch = self.phase_center_epoch

            uv_list = []
            if len(filename) > file_num + 1:
                for f in filename[file_num + 1 :]:
                    uv2 = UVData()
                    try:
                        uv2.read(
                            f,
                            file_type=file_type,
                            phase_center_radec=phase_center_radec,
                            phase_frame=phase_frame,
                            phase_epoch=phase_epoch,
                            antenna_nums=antenna_nums,
                            antenna_names=antenna_names,
                            ant_str=ant_str,
                            bls=bls,
                            frequencies=frequencies,
                            freq_chans=freq_chans,
                            times=times,
                            polarizations=polarizations,
                            blt_inds=blt_inds,
                            time_range=time_range,
                            lsts=lsts,
                            lst_range=lst_range,
                            keep_all_metadata=keep_all_metadata,
                            read_data=read_data,
                            phase_type=phase_type,
                            correct_lat_lon=correct_lat_lon,
                            use_model=use_model,
                            data_column=data_column,
                            pol_order=pol_order,
                            data_array_dtype=data_array_dtype,
                            nsample_array_dtype=nsample_array_dtype,
                            skip_bad_files=skip_bad_files,
                            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,
                            isource=None,
                            irec=irec,
                            isb=isb,
                            corrchunk=corrchunk,
                            pseudo_cont=pseudo_cont,
                            calc_lst=calc_lst,
                            fix_old_proj=fix_old_proj,
                            fix_use_ant_pos=fix_use_ant_pos,
                            make_multi_phase=make_multi_phase,
                        )
                        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 unread is True:
                warnings.warn(
                    "########################################################\n"
                    "ALL FILES FAILED ON READ - NO READABLE FILES IN FILENAME\n"
                    "########################################################"
                )
            elif 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,
                    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=phase_use_ant_pos,
                    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,
                            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=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 time_range is not None
                    or polarizations is not None
                    or blt_inds 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_time_range = time_range
                    select_polarizations = polarizations
                    select_blt_inds = blt_inds
                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
                ):

                    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_blt_inds = blt_inds
                else:
                    select = False

            # 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,
                    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,
                )

            elif file_type == "mir":
                self.read_mir(
                    filename,
                    isource=isource,
                    irec=irec,
                    isb=isb,
                    corrchunk=corrchunk,
                    pseudo_cont=pseudo_cont,
                )
                select = False

            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,
                    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,
                )

            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=True,
                    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,
                )

            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,
                )

            elif file_type == "ms":
                self.read_ms(
                    filename,
                    data_column=data_column,
                    pol_order=pol_order,
                    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,
                )

            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,
                    read_data=read_data,
                    data_array_dtype=data_array_dtype,
                    keep_all_metadata=keep_all_metadata,
                    multidim_index=multidim_index,
                    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,
                )
                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,
                    time_range=select_time_range,
                    polarizations=select_polarizations,
                    blt_inds=select_blt_inds,
                    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,
                )

            if make_multi_phase:
                self._set_multi_phase_center(preserve_phase_center_info=True)

            if unphase_to_drift:
                if self.phase_type != "drift":
                    warnings.warn("Unphasing this UVData object to drift")
                    self.unphase_to_drift(
                        phase_frame=orig_phase_frame, use_ant_pos=phase_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 this object is not phased or is not phased close to
                # phase_center_radec, (re)phase it.
                # Close is defined using the phase_center_ra/dec tolerances.
                if self.phase_type == "drift" or (
                    not np.isclose(
                        self.phase_center_ra,
                        phase_center_radec[0],
                        rtol=self._phase_center_ra.tols[0],
                        atol=self._phase_center_ra.tols[1],
                    )
                    or not np.isclose(
                        self.phase_center_dec,
                        phase_center_radec[1],
                        rtol=self._phase_center_dec.tols[0],
                        atol=self._phase_center_dec.tols[1],
                    )
                    or (self.phase_center_frame != phase_frame)
                    or (self.phase_center_epoch != phase_epoch)
                ):
                    warnings.warn("Phasing this UVData object to phase_center_radec")
                    self.phase(
                        phase_center_radec[0],
                        phase_center_radec[1],
                        epoch=phase_epoch,
                        phase_frame=phase_frame,
                        orig_phase_frame=orig_phase_frame,
                        use_ant_pos=phase_use_ant_pos,
                        allow_rephase=True,
                    )

    @classmethod
    def from_file(
        cls,
        filename,
        axis=None,
        file_type=None,
        allow_rephase=True,
        phase_center_radec=None,
        unphase_to_drift=False,
        phase_frame="icrs",
        phase_epoch=None,
        orig_phase_frame=None,
        phase_use_ant_pos=True,
        antenna_nums=None,
        antenna_names=None,
        ant_str=None,
        bls=None,
        frequencies=None,
        freq_chans=None,
        times=None,
        polarizations=None,
        blt_inds=None,
        time_range=None,
        keep_all_metadata=True,
        read_data=True,
        phase_type=None,
        correct_lat_lon=True,
        use_model=False,
        data_column="DATA",
        pol_order="AIPS",
        data_array_dtype=np.complex128,
        nsample_array_dtype=np.float32,
        use_aoflagger_flags=None,
        use_cotter_flags=None,
        remove_dig_gains=True,
        remove_coarse_band=None,
        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,
        skip_bad_files=False,
        multidim_index=False,
        background_lsts=True,
        run_check=True,
        check_extra=True,
        run_check_acceptability=True,
        strict_uvw_antpos_check=False,
        isource=None,
        irec=None,
        isb=None,
        corrchunk=None,
        pseudo_cont=False,
        lsts=None,
        lst_range=None,
        calc_lst=True,
        fix_old_proj=None,
        fix_use_ant_pos=True,
        make_multi_phase=False,
        ignore_name=False,
    ):
        """
        Initialize a new UVData object by reading the input file.

        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', 'fhd', 'ms', 'uvh5'] 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.
        allow_rephase :  bool
            Allow rephasing of phased file data so that data from files with
            different phasing can be combined.
        phase_center_radec : array_like of float
            The phase center to phase the files to before adding the objects in
            radians (in the ICRS frame). If set to None and multiple files are
            read with different phase centers, the phase center of the first
            file will be used.
        unphase_to_drift : bool
            Unphase the data from the files before combining them.
        phase_frame : str
            The astropy frame to phase to. Either 'icrs' or 'gcrs'.
            'gcrs' accounts for precession & nutation,
            'icrs' accounts for precession, nutation & abberation.
            Only used if `phase_center_radec` is set.
        orig_phase_frame : str
            The original phase frame of the data (if it is already phased). Used
            for unphasing, only if `unphase_to_drift` or `phase_center_radec`
            are set. Defaults to using the 'phase_center_frame' attribute or
            'icrs' if that attribute is None.
        phase_use_ant_pos : bool
            If True, calculate the phased or unphased uvws directly from the
            antenna positions rather than from the existing uvws.
            Only used if `unphase_to_drift` or `phase_center_radec` are set.
        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)]) 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`,
            `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 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`.
        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.
        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 data. Only used if file_type is 'uvfits',
            'miriad' or 'uvh5'. If set to False, only the metadata will be
            read in. Setting read_data to False results in a metdata only
            object.
        phase_type : str, optional
            Option to specify the phasing status of the data. Only used if
            file_type is 'miriad'. Options are 'drift', 'phased' or None.
            'drift' means the data are zenith drift data, 'phased' means the
            data are phased to a single RA/Dec. Default is None
            meaning it will be guessed at based on the file contents.
        correct_lat_lon : bool
            Option to update the latitude and longitude from the known_telescopes
            list if the altitude is missing. Only used if file_type is 'miriad'.
        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). Only used if file_type is 'fhd'.
        data_column : str
            name of CASA data column to read into data_array. Options are:
            'DATA', 'MODEL', or 'CORRECTED_DATA'. Only used if file_type is 'ms'.
        pol_order : str
            Option to specify polarizations order convention, options are
            'CASA' or 'AIPS'. Only used if file_type is 'ms'.
        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'. Only used if file_type is 'uvh5' or
            'mwa_corr_fits'.
        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). Only used if
            file_type is 'mwa_corr_fits'.
        use_aoflagger_flags : bool
            Only used if file_type is 'mwa_corr_fits'. 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
            Only used if file_type is 'mwa_corr_fits'. Option to divide out digital
            gains.
        remove_coarse_band : bool
            Only used if file_type is 'mwa_corr_fits'. Option to divide out coarse
            band shape.
        correct_cable_len : bool
            Flag to apply cable length correction. Only used if file_type is
            'mwa_corr_fits'.
        correct_van_vleck : bool
            Flag to apply a van vleck correction. Only used if file_type is
            'mwa_corr_fits'.
        cheby_approx : bool
            Only used if file_type is 'mwa_corr_fits' and correct_van_vleck is True.
            Option to implement the van vleck correction with a chebyshev polynomial
            approximation. Set to False to run the integral version of the correction.
        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. Only used if file_type is
            'mwa_corr_fits'.
        flag_small_sig_ants : bool
            Being 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. Only used if file_type is 'mwa_corr_fits'.
        flag_init: bool
            Only used if file_type is 'mwa_corr_fits'. 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 file_type is 'mwa_corr_fits' and 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 file_type is 'mwa_corr_fits' and 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 file_type is 'mwa_corr_fits' and 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.
            Only used if file_type is 'mwa_corr_fits'.
        phase_to_pointing_center : bool
            Flag to phase to the pointing center. Only used if file_type is
            'mwa_corr_fits'. Cannot be set if phase_center_radec is not None.
        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).
        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.
        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.
        isource : int
            Source code for MIR dataset
        irec : int
            Receiver code for MIR dataset
        isb : int
            Sideband code for MIR dataset
        corrchunk : int
            Correlator chunk code for MIR dataset
        pseudo_cont : boolean
            Read in only pseudo-continuuum values in MIR dataset. Default is false.
        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.
        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. Only applies to MIRIAD files.
        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
            Option to make the output a multi phase center dataset, capable of holding
            data on multiple phase centers. By default, this is only done if reading
            in a file with multiple sources.
        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.

        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 the data are multi source or have multiple
            spectral windows.
            If phase_center_radec is not None and is not length 2.

        """
        uvd = cls()
        uvd.read(
            filename,
            axis=axis,
            file_type=file_type,
            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,
            antenna_nums=antenna_nums,
            antenna_names=antenna_names,
            ant_str=ant_str,
            bls=bls,
            frequencies=frequencies,
            freq_chans=freq_chans,
            times=times,
            polarizations=polarizations,
            blt_inds=blt_inds,
            time_range=time_range,
            keep_all_metadata=keep_all_metadata,
            read_data=read_data,
            phase_type=phase_type,
            correct_lat_lon=correct_lat_lon,
            use_model=use_model,
            data_column=data_column,
            pol_order=pol_order,
            data_array_dtype=data_array_dtype,
            nsample_array_dtype=nsample_array_dtype,
            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,
            skip_bad_files=skip_bad_files,
            multidim_index=multidim_index,
            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,
            isource=isource,
            irec=irec,
            isb=isb,
            corrchunk=corrchunk,
            pseudo_cont=pseudo_cont,
            lsts=lsts,
            lst_range=lst_range,
            calc_lst=calc_lst,
            fix_old_proj=fix_old_proj,
            fix_use_ant_pos=fix_use_ant_pos,
            make_multi_phase=make_multi_phase,
            ignore_name=ignore_name,
        )
        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,
    ):
        """
        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.

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

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

        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,
        )
        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,
    ):
        """
        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
            Option to spoof the values of optional UVParameters that are not set
            but are required for uvfits files.
        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
            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.

        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.
            Any of ['antenna_positions', 'gst0', 'rdate', 'earth_omega', 'dut1',
            'timesys'] are not set on the object and `spoof_nonessential` is False.
            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")
        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,
        )
        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,
    ):
        """
        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 meaning no filter or compression. Dataset must be chunked.
        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.

        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,
        )
        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 meaning no filter or compression. Dataset must be chunked.
        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,
        add_to_history=None,
        run_check_acceptability=True,
    ):
        """
        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.
        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.

        """
        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,
            add_to_history=add_to_history,
            run_check_acceptability=run_check_acceptability,
        )
        del uvh5_obj


# 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 os
import copy
import warnings

import numpy as np
from astropy import constants as const
from astropy.time import Time
from astropy.io import fits

from .uvdata import UVData
from .. import utils as uvutils

__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.

    Attributes
    ----------
    uvfits_required_extra : list of str
        Names of optional UVParameters that are required for uvfits.

    """

    uvfits_required_extra = [
        "antenna_positions",
        "gst0",
        "rdate",
        "earth_omega",
        "dut1",
        "timesys",
    ]

    def _get_parameter_data(
        self, vis_hdu, 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: uvfits antennas are 1 indexed,
            # need to subtract one to get to 0-indexed
            self.ant_1_array = np.int32(vis_hdu.data.par("ANTENNA1")) - 1
            self.ant_2_array = np.int32(vis_hdu.data.par("ANTENNA2")) - 1
            subarray = np.int32(vis_hdu.data.par("SUBARRAY")) - 1
            # 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
            )

        # check for multi source files. NOW SUPPORTED, W00T!
        if "SOURCE" in vis_hdu.data.parnames:
            # Preserve the source info just in case the AIPS SU table is missing, and
            # we need to revert things back.
            self._set_multi_phase_center(preserve_phase_center_info=True)
            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:
        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]),
                    )
                )
            )
            * 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,
        read_metadata,
        keep_all_metadata,
        run_check,
        check_extra,
        run_check_acceptability,
        strict_uvw_antpos_check,
        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,
        )

        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_metadata(
                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)

        # 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,
            )

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


        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.
            self._set_phased()
            # 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
                )

                # the axis number for phase center depends on if the spw exists
                self.phase_center_ra_degrees = float(vis_hdr.pop("CRVAL6"))
                self.phase_center_dec_degrees = float(vis_hdr.pop("CRVAL7"))
            else:
                self.Nspws = 1
                self.spw_array = np.array([np.int64(0)])

                # the axis number for phase center depends on if the spw exists
                self.phase_center_ra_degrees = float(vis_hdr.pop("CRVAL5"))
                self.phase_center_dec_degrees = float(vis_hdr.pop("CRVAL6"))

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

            self.polarization_array = np.int32(uvutils._fits_gethduaxis(vis_hdu, 3))
            # other info -- not required but frequently used
            self.object_name = vis_hdr.pop("OBJECT", None)
            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"

            self.phase_center_epoch = vis_hdr.pop("EPOCH", None)

            # 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
            self.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 self.phase_center_frame is None:
                self.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 self.phase_center_frame is None:
                self.phase_center_frame = vis_hdr.pop("RADESYSA", None)
            if self.phase_center_frame is None:
                self.phase_center_frame = vis_hdr.pop("RADESYSa", None)

            # If we _still_ can't find anything, 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 self.phase_center_frame is None:
                if self.phase_center_epoch is None:
                    self.phase_center_frame = "icrs"
                else:
                    frame = "fk4" if (self.phase_center_epoch == 1950.0) else "fk5"
                    self.phase_center_frame = frame

            self.extra_keywords = uvutils._get_fits_extra_keywords(
                vis_hdr, keywords_to_skip=["DATE-OBS"]
            )

            # 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 ant_ind, name in enumerate(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)

            # subtract one to get to 0-indexed values rather than 1-indexed values
            self.antenna_numbers = ant_hdu.data.field("NOSTA") - 1

            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, run_check_acceptability, background_lsts=background_lsts,
            )
            # If we find the source attribute in the FITS random paramter list,
            # the multi_phase_center attribute will be set to True, and we should also
            # expect that there must be an AIPS SU table.
            if self.multi_phase_center and "AIPS SU" not in hdunames.keys():
                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)."
                )
                name = list(self.phase_center_catalog.keys())[0]
                self.phase_center_ra = self.phase_center_catalog[name]["cat_lon"]
                self.phase_center_dec = self.phase_center_catalog[name]["cat_lat"]
                self.phase_center_frame = self.phase_center_catalog[name]["cat_frame"]
                self.phase_center_epoch = self.phase_center_catalog[name]["cat_epoch"]

                self.multi_phase_center = False
                self._phase_center_id_array.required = False
                self._Nphase.required = False
                self._phase_center_catalog.required = False
                self.object_name = name
                self.Nphase = None
                self.phase_center_catalog = None
                self.phase_center_id_array = None
            elif self.multi_phase_center:
                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

                # Reset the catalog, since it has some dummy information stored within
                # it (that was pulled off the primary table)
                self._remove_phase_center(list(self.phase_center_catalog.keys())[0])

                # 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"]
                    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 not read_data:
                # don't read in the data. This means the object is a metadata
                # only object but that may not matter for many purposes.
                return

            # 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,
                False,
                keep_all_metadata,
                run_check,
                check_extra,
                run_check_acceptability,
                strict_uvw_antpos_check,
                fix_old_proj,
                fix_use_ant_pos,
            )

    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,
    ):
        """
        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
            Option to spoof the values of optional UVParameters that are not set
            but are required for uvfits files.
        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.

        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.
            Any of ['antenna_positions', 'gst0', 'rdate', 'earth_omega', 'dut1',
            'timesys'] are not set on the object and `spoof_nonessential` is False.
            If the `timesys` parameter is not set to "UTC".
        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,
            )

        if self.phase_type == "phased":
            pass
        elif self.phase_type == "drift":
            if force_phase:
                print(
                    "The data are in drift mode and do not have a "
                    "defined phase center. 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 in drift mode. "
                    "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_spacing = np.diff(self.polarization_array)
            pol_indexing = np.argsort(np.abs(self.polarization_array))
            polarization_array = self.polarization_array[pol_indexing]
            pol_spacing = np.diff(polarization_array)
            if np.min(pol_spacing) < np.max(pol_spacing):
                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 = pol_spacing[0]
        else:
            pol_indexing = np.asarray([0])
            polarization_array = self.polarization_array
            pol_spacing = 1

        for p in self.extra():
            param = getattr(self, p)
            if param.name in self.uvfits_required_extra:
                if param.value is None:
                    if spoof_nonessential:
                        param.apply_spoof()
                        setattr(self, p, param)
                    else:
                        raise ValueError(
                            "Required attribute {attribute} "
                            "for uvfits not defined. Define or "
                            "set spoof_nonessential to True to "
                            "spoof this attribute.".format(attribute=p)
                        )

        # 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
        )

        # FITS uvw direction convention is opposite ours and Miriad's.
        # So conjugate the visibilities and flip the uvws:
        uvw_array_sec = -1 * self.uvw_array / const.c.to("m/s").value

        # 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
        time_array1 = np.float32(self.time_array - jd_midnight)
        time_array2 = np.float32(
            self.time_array - jd_midnight - np.float64(time_array1)
        )

        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
        # Note that uvfits antenna arrays are 1-indexed so we add 1
        # to our 0-indexed arrays

        group_parameter_dict = {
            "UU      ": uvw_array_sec[:, 0],
            "VV      ": uvw_array_sec[:, 1],
            "WW      ": uvw_array_sec[:, 2],
            "DATE    ": time_array1,
            "DATE2   ": time_array2,
            "BASELINE": baselines_use,
            "SOURCE  ": None,
            "FREQSEL ": np.ones_like(self.time_array, dtype=np.float32),
            "ANTENNA1": self.ant_1_array + 1,
            "ANTENNA2": self.ant_2_array + 1,
            "SUBARRAY": np.ones_like(self.ant_1_array),
            "INTTIM  ": int_time_array,
        }

        if self.multi_phase_center:
            id_offset = np.any(
                [
                    temp_dict["cat_id"] == 0
                    for temp_dict in self.phase_center_catalog.values()
                ]
            )
            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,
            "DATE2   ": 1.0,
            "BASELINE": 1.0,
            "SOURCE  ": 1.0,
            "FREQSEL ": 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,
            "DATE2   ": 0.0,
            "BASELINE": 0.0,
            "SOURCE  ": 0.0,
            "FREQSEL ": 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)
            lst_array_1 = np.float32(lst_array_deg)
            lst_array_2 = np.float32(lst_array_deg - np.float64(lst_array_1))
            group_parameter_dict["LST     "] = lst_array_1
            pscal_dict["LST     "] = 1.0
            pzero_dict["LST     "] = 0.0

        # list contains arrays of [u,v,w,date,baseline];
        # each array has shape (Nblts)
        parnames_use = ["UU      ", "VV      ", "WW      ", "DATE    ", "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 > 256 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."
            )

        if self.multi_phase_center:
            parnames_use.append("SOURCE  ")

        parnames_use += ["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_lst:
            # add second LST array part
            parnames_use.append("LST     ")
            group_parameter_list.append(lst_array_2)

        parnames_use_datefix = copy.deepcopy(parnames_use)
        parnames_use_datefix[parnames_use_datefix.index("DATE2   ")] = "DATE    "
        hdu = fits.GroupData(
            uvfits_array_data,
            parnames=parnames_use_datefix,
            pardata=group_parameter_list,
            bitpix=-32,
        )
        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

        hdu.header["CTYPE6  "] = "RA"
        hdu.header["CRVAL6  "] = self.phase_center_ra_degrees

        hdu.header["CTYPE7  "] = "DEC"
        hdu.header["CRVAL7  "] = self.phase_center_dec_degrees

        hdu.header["BUNIT   "] = self.vis_units
        hdu.header["BSCALE  "] = 1.0
        hdu.header["BZERO   "] = 0.0

        name = "MULTI" if self.multi_phase_center else self.object_name
        hdu.header["OBJECT  "] = name
        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.phase_center_epoch is not None:
            hdu.header["EPOCH   "] = float(self.phase_center_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.phase_center_frame is not None:
            # Previous versions of pyuvdata wrote this header as PHSFRAME
            hdu.header["RADESYS"] = self.phase_center_frame

        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)
        # convert to 1-indexed from 0-indexed indicies
        col4 = fits.Column(name="NOSTA", format="1J", array=self.antenna_numbers + 1)
        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"
        ant_hdu.header["GSTIA0"] = self.gst0
        # 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]
        ant_hdu.header["RDATE"] = self.rdate
        ant_hdu.header["UT1UTC"] = self.dut1

        ant_hdu.header["TIMESYS"] = self.timesys
        if 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["ARRNAM"] = self.telescope_name
        ant_hdu.header["NO_IF"] = self.Nspws
        ant_hdu.header["DEGPDY"] = 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)

        # If needed, add the SU table
        if self.multi_phase_center:
            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(list(self.phase_center_catalog.keys()))
            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, name in enumerate(name_arr):
                phase_dict = self.phase_center_catalog[name]
                # This is a stub for something smarter in the future
                sou_ids[idx] = self.phase_center_catalog[name]["cat_id"] + id_offset
                rest_freq[idx][:] = np.mean(self.freq_array)
                pm_ra[idx] = 0.0
                pm_dec[idx] = 0.0
                if phase_dict["cat_type"] == "sidereal":
                    # 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"],
                        "fk5",
                        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
                    )

                    cat_id = self.phase_center_catalog[name]["cat_id"]

                    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."""
import numpy as np
import os
import warnings
import h5py
import json

from .uvdata import UVData
from .. import utils as uvutils

__all__ = ["UVH5"]


# define HDF5 type for interpreting HERA correlator outputs (integers) as
# complex numbers
_hera_corr_dtype = np.dtype([("r", "<i4"), ("i", "<i4")])


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)
    dtype_in = dset.dtype
    # dset is indexed in native dtype, but is upcast upon assignment
    output_array.real = uvutils._index_dset(dset.astype(dtype_in)["r"], indices)
    output_array.imag = uvutils._index_dset(dset.astype(dtype_in)["i"], 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
    # 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], "r"] = data.real
        dset[indices[0], indices[1], indices[2], "i"] = data.imag
    else:
        dset[indices[0], np.s_[:], indices[1], indices[2], "r"] = data.real
        dset[indices[0], np.s_[:], indices[1], indices[2], "i"] = data.imag
    return


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(
        self, header, filename, run_check_acceptability=True, background_lsts=True
    ):
        """
        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.
        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
        """
        # get telescope information
        latitude = header["latitude"][()]
        longitude = header["longitude"][()]
        altitude = header["altitude"][()]
        self.telescope_location_lat_lon_alt_degrees = (latitude, longitude, altitude)
        self.instrument = bytes(header["instrument"][()]).decode("utf8")
        self.telescope_name = bytes(header["telescope_name"][()]).decode("utf8")

        # set history appropriately
        self.history = bytes(header["history"][()]).decode("utf8")
        if not uvutils._check_history_version(self.history, self.pyuvdata_version_str):
            self.history += self.pyuvdata_version_str

        # check for vis_units
        if "vis_units" in header:
            self.vis_units = bytes(header["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 self.vis_units == "UNCALIB":
                self.vis_units = "uncalib"
        else:
            # default to uncalibrated data
            self.vis_units = "uncalib"

        # check for optional values
        if "dut1" in header:
            self.dut1 = float(header["dut1"][()])
        if "earth_omega" in header:
            self.earth_omega = float(header["earth_omega"][()])
        if "gst0" in header:
            self.gst0 = float(header["gst0"][()])
        if "rdate" in header:
            self.rdate = bytes(header["rdate"][()]).decode("utf8")
        if "timesys" in header:
            self.timesys = bytes(header["timesys"][()]).decode("utf8")
        if "x_orientation" in header:
            self.x_orientation = bytes(header["x_orientation"][()]).decode("utf8")
        if "blt_order" in header:
            blt_order_str = bytes(header["blt_order"][()]).decode("utf8")
            self.blt_order = tuple(blt_order_str.split(", "))
            if self.blt_order == ("bda",):
                self._blt_order.form = (1,)

        if "antenna_diameters" in header:
            self.antenna_diameters = header["antenna_diameters"][()]
        if "uvplane_reference_time" in header:
            self.uvplane_reference_time = int(header["uvplane_reference_time"][()])
        if "eq_coeffs" in header:
            self.eq_coeffs = header["eq_coeffs"][()]
        if "eq_coeffs_convention" in header:
            self.eq_coeffs_convention = bytes(
                header["eq_coeffs_convention"][()]
            ).decode("utf8")

        # 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 "flex_spw" in header:
            if bool(header["flex_spw"][()]):
                self._set_flex_spw()
        if "flex_spw_id_array" in header:
            self.flex_spw_id_array = header["flex_spw_id_array"][:]
        if "multi_phase_center" in header:
            if bool(header["multi_phase_center"][()]):
                self._set_phased()
                self._set_multi_phase_center(preserve_phase_center_info=False)
        if "Nphase" in header:
            self.Nphase = int(header["Nphase"][()])

        self.phase_type = bytes(header["phase_type"][()]).decode("utf8")
        self.object_name = bytes(header["object_name"][()]).decode("utf8")
        # Here is where we start handing phase center information.  If we have a multi
        # phase center dataset, this information is going to be handled a bit
        # differently, since all of these entries are expected to be required arrays.
        if self.multi_phase_center:
            self.phase_center_id_array = header["phase_center_id_array"][:]

        # check for phasing information
        if self.phase_type == "phased":
            self.phase_center_ra = float(header["phase_center_ra"][()])
            self.phase_center_dec = float(header["phase_center_dec"][()])
            if "phase_center_epoch" in header:
                self.phase_center_epoch = float(header["phase_center_epoch"][()])
            self._set_phased()
            if "phase_center_app_ra" in header and "phase_center_app_dec" in header:
                self.phase_center_app_ra = header["phase_center_app_ra"][:]
                self.phase_center_app_dec = header["phase_center_app_dec"][:]
            if "phase_center_frame_pa" in header:
                self.phase_center_frame_pa = header["phase_center_frame_pa"][:]
        else:
            if self.phase_type != "drift":
                warnings.warn(
                    "Unknown phase types are no longer supported, marking this "
                    'object as phase_type="drift" by default. If this was a phased '
                    "object, you can set the phase type correctly by running the "
                    "_set_phased() method."
                )
            self._set_drift()

        # Here is where we collect the other optional source/phasing info
        if "phase_center_catalog" in header:
            self.phase_center_catalog = {}
            for key in header["phase_center_catalog"].keys():
                self.phase_center_catalog[key] = json.loads(
                    bytes(header["phase_center_catalog"][key][()]).decode("utf8")
                )
        if "phase_center_frame" in header:
            self.phase_center_frame = bytes(header["phase_center_frame"][()]).decode(
                "utf8"
            )

        # get antenna arrays
        # cast to native python int type
        self.Nants_data = int(header["Nants_data"][()])
        self.Nants_telescope = int(header["Nants_telescope"][()])
        self.ant_1_array = header["ant_1_array"][:]
        self.ant_2_array = header["ant_2_array"][:]
        self.antenna_names = [
            bytes(n).decode("utf8") for n in header["antenna_names"][:]
        ]
        self.antenna_numbers = header["antenna_numbers"][:]
        self.antenna_positions = header["antenna_positions"][:]

        # set telescope params
        try:
            self.set_telescope_params()
        except ValueError as ve:
            warnings.warn(str(ve))

        # get baseline array
        self.baseline_array = self.antnums_to_baseline(
            self.ant_1_array, self.ant_2_array
        )
        self.Nbls = len(np.unique(self.baseline_array))

        # get uvw array
        self.uvw_array = header["uvw_array"][:, :]

        # get time information
        self.time_array = header["time_array"][:]
        integration_time = header["integration_time"]
        self.integration_time = integration_time[:]
        proc = None
        if "lst_array" in header:
            self.lst_array = header["lst_array"][:]
            # check that lst_array in file is self-consistent
            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 {file} are not self-consistent "
                        "with time_array and telescope location. Consider "
                        "recomputing with utils.get_lst_for_time.".format(file=filename)
                    )
        else:
            # compute lst_array from time_array and telescope location
            proc = self.set_lsts_from_time_array(background=background_lsts)

        # get frequency information
        self.freq_array = header["freq_array"][:]
        self.spw_array = header["spw_array"][:]

        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(header["channel_width"]).size == self.freq_array.size
            ), arr_shape_msg
            self._set_future_array_shapes()

        # 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.
        if self.flex_spw or np.asarray(header["channel_width"]).ndim == 1:
            self.channel_width = header["channel_width"][:]
        else:
            self.channel_width = float(header["channel_width"][()])

        # get polarization information
        self.polarization_array = header["polarization_array"][:]

        # get data shapes
        self.Nfreqs = int(header["Nfreqs"][()])
        self.Npols = int(header["Npols"][()])
        self.Ntimes = int(header["Ntimes"][()])
        self.Nblts = int(header["Nblts"][()])
        self.Nspws = int(header["Nspws"][()])

        # get extra_keywords
        if "extra_keywords" in header:
            self.extra_keywords = {}
            for key in header["extra_keywords"].keys():
                if header["extra_keywords"][key].dtype.type in (np.string_, np.object_):
                    self.extra_keywords[key] = bytes(
                        header["extra_keywords"][key][()]
                    ).decode("utf8")
                else:
                    # special handling for empty datasets == python `None` type
                    if header["extra_keywords"][key].shape is None:
                        self.extra_keywords[key] = None
                    else:
                        self.extra_keywords[key] = header["extra_keywords"][key][()]

        if proc is not None:
            # if lsts are in the background wait for them to return
            proc.join()

        return

    def _get_data(
        self,
        dgrp,
        antenna_nums,
        antenna_names,
        ant_str,
        bls,
        frequencies,
        freq_chans,
        times,
        time_range,
        lsts,
        lst_range,
        polarizations,
        blt_inds,
        data_array_dtype,
        keep_all_metadata,
        multidim_index,
        run_check,
        check_extra,
        run_check_acceptability,
        strict_uvw_antpos_check,
        fix_old_proj,
        fix_use_ant_pos,
    ):
        """
        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).
        """
        # 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,
        )

        # 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_metadata(
                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()

        # Finally, backfill the apparent coords if they aren't in the original datafile
        if self.phase_type == "phased" and (
            (self.phase_center_app_ra is None)
            or (self.phase_center_app_dec is None)
            or (self.phase_center_frame_pa is None)
        ):
            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 mutli-phase-ctr
            # attribute just to be extra safe.
            if (not self.multi_phase_center) and (
                ((fix_old_proj) or (fix_old_proj is None))
            ):
                self.fix_phase(use_ant_pos=fix_use_ant_pos)
            else:
                warnings.warn(
                    "This data appears to have been phased-up using the old `phase` "
                    "method, which is incompatible with the current sent of methods. "
                    "Please run the `fix_phase` method (or set fix_old_proj=True when "
                    "loading the dataset) to address this issue."
                )

        # 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,
            )

        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,
        keep_all_metadata=True,
        read_data=True,
        data_array_dtype=np.complex128,
        multidim_index=False,
        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,
    ):
        """
        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.
        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.
        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.

        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 not os.path.exists(filename):
            raise IOError(filename + " not found")

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

        # open hdf5 file for reading
        with h5py.File(filename, "r") as f:
            # extract header information
            header = f["/Header"]
            self._read_header(
                header,
                filename,
                run_check_acceptability=run_check_acceptability,
                background_lsts=background_lsts,
            )

            if not read_data:
                # don't read in the data. This means the object is incomplete,
                # but that may not matter for many purposes.

                # run a check if desired before returning
                if run_check:
                    self.check(
                        check_extra=check_extra,
                        run_check_acceptability=run_check_acceptability,
                        strict_uvw_antpos_check=strict_uvw_antpos_check,
                    )

                return

            # Now read in the data
            dgrp = f["/Data"]
            self._get_data(
                dgrp,
                antenna_nums,
                antenna_names,
                ant_str,
                bls,
                frequencies,
                freq_chans,
                times,
                time_range,
                lsts,
                lst_range,
                polarizations,
                blt_inds,
                data_array_dtype,
                keep_all_metadata,
                multidim_index,
                run_check,
                check_extra,
                run_check_acceptability,
                strict_uvw_antpos_check,
                fix_old_proj,
                fix_use_ant_pos,
            )

        # For now, always use current shapes when data is read in, even if the file
        # has the future shapes.
        if self.future_array_shapes:
            self.use_current_array_shapes()

        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
        if self.multi_phase_center:
            # this is Version 1.1!
            header["version"] = np.string_("1.1")
        elif self.future_array_shapes:
            # this is Version 1.0
            header["version"] = np.string_("1.0")
        else:
            header["version"] = np.string_("0.1")

        # 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)
        header["object_name"] = np.string_(self.object_name)

        # 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
        header["multi_phase_center"] = self.multi_phase_center
        # handle antenna_names; works for lists or arrays
        header["antenna_names"] = np.asarray(self.antenna_names, dtype="bytes")

        # write out phasing information
        header["phase_type"] = np.string_(self.phase_type)
        if self.phase_center_ra is not None:
            header["phase_center_ra"] = self.phase_center_ra
        if self.phase_center_app_ra is not None:
            header["phase_center_app_ra"] = self.phase_center_app_ra
        if self.phase_center_dec is not None:
            header["phase_center_dec"] = self.phase_center_dec
        if self.phase_center_app_dec is not None:
            header["phase_center_app_dec"] = self.phase_center_app_dec
        if self.phase_center_frame_pa is not None:
            header["phase_center_frame_pa"] = self.phase_center_frame_pa
        if self.phase_center_epoch is not None:
            header["phase_center_epoch"] = self.phase_center_epoch
        if self.phase_center_frame is not None:
            header["phase_center_frame"] = np.string_(self.phase_center_frame)

        # 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.phase_center_id_array is not None:
            header["phase_center_id_array"] = self.phase_center_id_array
        if self.Nphase is not None:
            header["Nphase"] = self.Nphase

        # Write out the catalog, if available
        if self.phase_center_catalog:
            phase_dict = header.create_group("phase_center_catalog")
            for k in self.phase_center_catalog.keys():
                # Dictionary entries can be written out as JSON-formatted strings.
                phase_dict[k] = np.string_(json.dumps(self.phase_center_catalog[k]))

        # 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,
    ):
        """
        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). Dataset must be chunked.
        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.

        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,
            )

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

        revert_fas = False
        if self.multi_phase_center:
            # We force using future array shapes here multi_phase_center is v1.1, but
            # future_array_shapes is v1.0 (but a UVData object can have the
            # multi_phase_center enabled, but not use future_array_shapes). We capture
            # the current state so that it can be reverted later if needed.
            if not self.future_array_shapes:
                revert_fas = True
                self.use_future_array_shapes()

        # 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,
                    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,
                    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:
            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). Dataset must be chunked.
        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")

        # 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
            if self.future_array_shapes:
                data_size = (self.Nblts, self.Nfreqs, self.Npols)
            else:
                data_size = (self.Nblts, 1, 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,
            )
            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,
            )

        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,
                filename,
                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,
        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.
        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)
            )

        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,
        )

        # 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
        if self.future_array_shapes:
            proper_shape = (Nblts, Nfreqs, Npols)
        else:
            proper_shape = (Nblts, 1, Nfreqs, Npols)
        if data_array.shape != proper_shape:
            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:
                    if self.future_array_shapes:
                        visdata_dset[blt_inds, freq_inds, pol_inds] = data_array
                    else:
                        visdata_dset[blt_inds, :, freq_inds, pol_inds] = data_array
                if self.future_array_shapes:
                    flags_dset[blt_inds, freq_inds, pol_inds] = flag_array
                    nsamples_dset[blt_inds, freq_inds, pol_inds] = nsample_array
                else:
                    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)
                                if self.future_array_shapes:
                                    _write_complex_astype(
                                        data_array[:, ifreq, ipol],
                                        visdata_dset,
                                        indices,
                                    )
                                else:
                                    _write_complex_astype(
                                        data_array[:, :, ifreq, ipol],
                                        visdata_dset,
                                        indices,
                                    )
                            else:
                                if self.future_array_shapes:
                                    visdata_dset[
                                        blt_inds, freq_idx, pol_idx
                                    ] = data_array[:, ifreq, ipol]
                                else:
                                    visdata_dset[
                                        blt_inds, :, freq_idx, pol_idx
                                    ] = data_array[:, :, ifreq, ipol]
                            if self.future_array_shapes:
                                flags_dset[blt_inds, freq_idx, pol_idx] = flag_array[
                                    :, ifreq, ipol
                                ]
                                nsamples_dset[
                                    blt_inds, freq_idx, pol_idx
                                ] = nsample_array[:, ifreq, ipol]
                            else:
                                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)
                                if self.future_array_shapes:
                                    _write_complex_astype(
                                        data_array[iblt, :, ipol], visdata_dset, indices
                                    )
                                else:
                                    _write_complex_astype(
                                        data_array[iblt, :, :, ipol],
                                        visdata_dset,
                                        indices,
                                    )
                            else:
                                if self.future_array_shapes:
                                    visdata_dset[
                                        blt_idx, freq_inds, pol_idx
                                    ] = data_array[iblt, :, ipol]
                                else:
                                    visdata_dset[
                                        blt_idx, :, freq_inds, pol_idx
                                    ] = data_array[iblt, :, :, ipol]
                            if self.future_array_shapes:
                                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:
                                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)
                                if self.future_array_shapes:
                                    _write_complex_astype(
                                        data_array[iblt, ifreq, :],
                                        visdata_dset,
                                        indices,
                                    )
                                else:
                                    _write_complex_astype(
                                        data_array[iblt, :, ifreq, :],
                                        visdata_dset,
                                        indices,
                                    )
                            else:
                                if self.future_array_shapes:
                                    visdata_dset[
                                        blt_idx, freq_idx, pol_inds
                                    ] = data_array[iblt, ifreq, :]
                                else:
                                    visdata_dset[
                                        blt_idx, :, freq_idx, pol_inds
                                    ] = data_array[iblt, :, ifreq, :]
                            if self.future_array_shapes:
                                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:
                                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)
                                if self.future_array_shapes:
                                    _write_complex_astype(
                                        data_array[iblt, ifreq, ipol],
                                        visdata_dset,
                                        indices,
                                    )
                                else:
                                    _write_complex_astype(
                                        data_array[iblt, :, ifreq, ipol],
                                        visdata_dset,
                                        indices,
                                    )
                            else:
                                if self.future_array_shapes:
                                    visdata_dset[
                                        blt_idx, freq_idx, pol_idx
                                    ] = data_array[iblt, ifreq, ipol]
                                else:
                                    visdata_dset[
                                        blt_idx, :, freq_idx, pol_idx
                                    ] = data_array[iblt, :, ifreq, ipol]
                            if self.future_array_shapes:
                                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]
                            else:
                                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)

        return


# pyuvdata/uvflag/__init__.py

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

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


# pyuvdata/uvflag/uvflag.py

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

"""Primary container for radio interferometer flag manipulation."""
import numpy as np
import os
import warnings
import h5py
import pathlib

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

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


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

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

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

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

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


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

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

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

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

    tel = uvtel.get_telescope(uv.telescope_name)
    lat, lon, alt = tel.telescope_location_lat_lon_alt_degrees
    lst_array = uvutils.get_lst_for_time(uv.time_array, lat, lon, alt)
    return lst_array


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

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

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

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

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

    if isinstance(uv, UVCal):
        if keep_pol:
            waterfall = np.swapaxes(np.mean(flag_array, axis=(0, 1)), 0, 1)
        else:
            waterfall = np.mean(flag_array, axis=(0, 1, 4)).T
    else:
        if keep_pol:
            waterfall = np.zeros((uv.Ntimes, uv.Nfreqs, uv.Npols))
            for i, t in enumerate(np.unique(uv.time_array)):
                waterfall[i, :] = np.mean(
                    flag_array[uv.time_array == t, 0, :, :], axis=0
                )
        else:
            waterfall = np.zeros((uv.Ntimes, uv.Nfreqs))
            for i, t in enumerate(np.unique(uv.time_array)):
                waterfall[i, :] = np.mean(
                    flag_array[uv.time_array == t, 0, :, :], axis=(0, 2)
                )

    return waterfall


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

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

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

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


    """

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

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

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

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

        self._Ntimes = uvp.UVParameter(
            "Ntimes", description="Number of times", expected_type=int
        )
        desc = "Number of baselines. " 'Only Required for "baseline" type objects.'
        self._Nbls = uvp.UVParameter(
            "Nbls", description=desc, expected_type=int, required=False
        )
        self._Nblts = uvp.UVParameter(
            "Nblts",
            description="Number of baseline-times "
            "(i.e. number of spectra). Not necessarily "
            "equal to Nbls * Ntimes",
            expected_type=int,
        )
        self._Nspws = uvp.UVParameter(
            "Nspws",
            description="Number of spectral windows "
            "(ie non-contiguous spectral chunks). "
            "More than one spectral window is not "
            "currently supported.",
            expected_type=int,
            required=False,
        )
        self._Nfreqs = uvp.UVParameter(
            "Nfreqs", description="Number of frequency channels", expected_type=int
        )
        self._Npols = uvp.UVParameter(
            "Npols", description="Number of polarizations", expected_type=int
        )

        desc = (
            "Floating point metric information, only availble in metric mode. "
            "shape (Nblts, Nspws, Nfreq, Npols)."
        )
        self._metric_array = uvp.UVParameter(
            "metric_array",
            description=desc,
            form=("Nblts", "Nspws", "Nfreqs", "Npols"),
            expected_type=float,
            required=False,
        )

        desc = (
            "Boolean flag, True is flagged, only availble in flag mode. "
            "shape (Nblts, Nspws, Nfreq, Npols)."
        )
        self._flag_array = uvp.UVParameter(
            "flag_array",
            description=desc,
            form=("Nblts", "Nspws", "Nfreqs", "Npols"),
            expected_type=bool,
            required=False,
        )

        desc = "Floating point weight information, shape (Nblts, Nspws, Nfreq, Npols)."
        self._weights_array = uvp.UVParameter(
            "weights_array",
            description=desc,
            form=("Nblts", "Nspws", "Nfreqs", "Npols"),
            expected_type=float,
        )

        desc = (
            "Floating point weight information about sum of squares of weights"
            " when weighted data converted from baseline to waterfall  mode."
        )
        self._weights_square_array = uvp.UVParameter(
            "weights_square_array",
            description=desc,
            form=("Nblts", "Nspws", "Nfreqs", "Npols"),
            expected_type=float,
            required=False,
        )

        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=float,
            tols=1e-3 / (60.0 * 60.0 * 24.0),
        )  # 1 ms in days

        desc = "Array of lsts, center of integration, shape (Nblts), " "units radians"
        self._lst_array = uvp.UVParameter(
            "lst_array",
            description=desc,
            form=("Nblts",),
            expected_type=float,
            tols=radian_tol,
        )

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

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

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

        desc = (
            "Array of frequencies, center of the channel, "
            "shape (Nspws, Nfreqs), units Hz"
        )
        self._freq_array = uvp.UVParameter(
            "freq_array",
            description=desc,
            form=("Nspws", "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)."
        )
        self._polarization_array = uvp.UVParameter(
            "polarization_array",
            description=desc,
            expected_type=int,
            acceptable_vals=list(np.arange(-8, 0)) + list(np.arange(1, 5)),
            form=("Npols",),
        )

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

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

        # ---antenna information ---
        desc = (
            "Number of antennas in the array. "
            'Only available for "baseline" type objects. '
            "May be larger than the number of antennas with data."
        )
        self._Nants_telescope = uvp.UVParameter(
            "Nants_telescope", description=desc, expected_type=int, required=False
        )
        desc = (
            "Number of antennas with data present. "
            'Only available for "baseline" or "antenna" type objects.'
            "May be smaller than the number of antennas in the array"
        )
        self._Nants_data = uvp.UVParameter(
            "Nants_data", description=desc, expected_type=int, required=True
        )
        #  --extra information ---
        desc = (
            "Orientation of the physical dipole corresponding to what is "
            'labelled as the x polarization. Options are "east" '
            '(indicating east/west orientation) and "north" (indicating '
            "north/south orientation)"
        )
        self._x_orientation = uvp.UVParameter(
            "x_orientation",
            description=desc,
            required=False,
            expected_type=str,
            acceptable_vals=["east", "north"],
        )

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

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

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

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

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

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

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

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

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

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

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

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

        return

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

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

        return

    def _set_type_antenna(self):
        """Set the type and required propertis consistent with an antenna object."""
        self.type = "antenna"
        self._ant_array.required = True
        self._baseline_array.required = False
        self._ant_1_array.required = False
        self._ant_2_array.required = False
        self._Nants_telescope.required = False
        self._Nants_data.required = True
        self._Nbls.required = False
        self._Nspws.required = True
        self._Nblts.required = False

        desc = (
            "Floating point metric information, "
            "has shape (Nants_data, Nspws, Nfreqs, Ntimes, Npols)."
        )
        self._metric_array.desc = desc
        self._metric_array.form = ("Nants_data", "Nspws", "Nfreqs", "Ntimes", "Npols")

        desc = (
            "Boolean flag, True is flagged, "
            "has shape (Nants_data, Nspws, Nfreqs, Ntimes, Npols)."
        )
        self._flag_array.desc = desc
        self._flag_array.form = ("Nants_data", "Nspws", "Nfreqs", "Ntimes", "Npols")

        desc = (
            "Floating point weight information, "
            "has shape (Nants_data, Nspws, Nfreqs, Ntimes, Npols)."
        )
        self._weights_array.desc = desc
        self._weights_array.form = ("Nants_data", "Nspws", "Nfreqs", "Ntimes", "Npols")

        desc = (
            "Array of unique times, center of integration, shape (Ntimes), "
            "units Julian Date"
        )
        self._time_array.form = ("Ntimes",)

        desc = (
            "Array of unique lsts, center of integration, shape (Ntimes), "
            "units radians"
        )
        self._lst_array.form = ("Ntimes",)

        desc = (
            "Array of frequencies, center of the channel, "
            "shape (Nspws, Nfreqs), units Hz"
        )
        self._freq_array.form = ("Nspws", "Nfreqs")

    def _set_type_baseline(self):
        """Set the type and required propertis consistent with a baseline object."""
        self.type = "baseline"
        self._ant_array.required = False
        self._baseline_array.required = True
        self._ant_1_array.required = True
        self._ant_2_array.required = True
        self._Nants_telescope.required = True
        self._Nants_data.required = True
        self._Nbls.required = True
        self._Nblts.required = True
        self._Nspws.required = True

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

        desc = "Floating point metric information, shape (Nblts, Nspws, Nfreqs, Npols)."
        self._metric_array.desc = desc
        self._metric_array.form = ("Nblts", "Nspws", "Nfreqs", "Npols")

        desc = "Boolean flag, True is flagged, shape (Nblts, Nfreqs, Npols)"
        self._flag_array.desc = desc
        self._flag_array.form = ("Nblts", "Nspws", "Nfreqs", "Npols")

        desc = "Floating point weight information, has shape (Nblts, Nfreqs, Npols)."
        self._weights_array.desc = desc
        self._weights_array.form = ("Nblts", "Nspws", "Nfreqs", "Npols")

        desc = (
            "Array of unique times, center of integration, shape (Ntimes), "
            "units Julian Date"
        )
        self._time_array.form = ("Nblts",)

        desc = (
            "Array of unique lsts, center of integration, shape (Ntimes), "
            "units radians"
        )
        self._lst_array.form = ("Nblts",)

        desc = (
            "Array of frequencies, center of the channel, "
            "shape (Nspws, Nfreqs), units Hz"
        )
        self._freq_array.form = ("Nspws", "Nfreqs")

    def _set_type_waterfall(self):
        """Set the type and required propertis consistent with a waterfall object."""
        self.type = "waterfall"
        self._ant_array.required = False
        self._baseline_array.required = False
        self._ant_1_array.required = False
        self._ant_2_array.required = False
        self._Nants_telescope.required = False
        self._Nants_data.required = False
        self._Nbls.required = False
        self._Nspws.required = False
        self._Nblts.required = False

        desc = "Floating point metric information, shape (Ntimes, Nfreqs, Npols)."
        self._metric_array.desc = desc
        self._metric_array.form = ("Ntimes", "Nfreqs", "Npols")

        desc = "Boolean flag, True is flagged, shape (Ntimes, Nfreqs, Npols)"
        self._flag_array.desc = desc
        self._flag_array.form = ("Ntimes", "Nfreqs", "Npols")

        desc = "Floating point weight information, has shape (Ntimes, Nfreqs, Npols)."
        self._weights_array.desc = desc
        self._weights_array.form = ("Ntimes", "Nfreqs", "Npols")

        desc = (
            "Floating point weight information about sum of squares of weights"
            " when weighted data converted from baseline to waterfall  mode."
            " Has shape (Ntimes, Nfreqs, Npols)."
        )
        self._weights_square_array.desc = desc
        self._weights_square_array.form = ("Ntimes", "Nfreqs", "Npols")

        desc = (
            "Array of unique times, center of integration, shape (Ntimes), "
            "units Julian Date"
        )
        self._time_array.form = ("Ntimes",)

        desc = (
            "Array of unique lsts, center of integration, shape (Ntimes), "
            "units radians"
        )
        self._lst_array.form = ("Ntimes",)

        desc = (
            "Array of frequencies, center of the channel, " "shape (Nfreqs), units Hz"
        )
        self._freq_array.form = ("Nfreqs",)

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

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

        """
        for p in self:
            attr = getattr(self, p)
            if (
                not attr.required
                and attr.value is not None
                and attr.name != "x_orientation"
                and attr.name != "weights_square_array"
                and attr.name != "extra_keywords"
                and attr.name != "filename"
            ):
                attr.value = None
                setattr(self, p, attr)

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

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

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

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

                _h2 = other.history
                other.history = None

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

                self.history = _h1
                other.history = _h2

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        Parameters
        ----------
        method : str, {"quadmean", "absmean", "mean", "or", "and"}
            How to collapse the dimension(s).
        run_check : bool
            Option to check for the existence and proper shapes of parameters
            after collapsing polarizations.
        check_extra : bool
            Option to check optional parameters as well as required ones.
        run_check_acceptability : bool
            Option to check acceptable range of the values of parameters after
            collapsing polarizations.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        Broadcasts the flag array to all baselines.
        This function does NOT apply flags to uv.

        Parameters
        ----------
        uv : UVData or UVFlag object
            Objcet with type baseline to match.
        force_pol : bool
            If True, will use 1 pol to broadcast to any other pol.
            Otherwise, will require polarizations match.
            For example, this keyword is useful if one flags on all
            pols combined, and wants to broadcast back to individual pols.
        run_check : bool
            Option to check for the existence and proper shapes of parameters
            after converting to baseline type.
        check_extra : bool
            Option to check optional parameters as well as required ones.
        run_check_acceptability : bool
            Option to check acceptable range of the values of parameters after
            converting to baseline type.

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

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

        # Now the pol axes should match regardless of force_pol.
        if not np.array_equal(uv.polarization_array, self.polarization_array):
            if self.polarization_array.size == 1:
                raise ValueError(
                    "Polarizations do not match. Try keyword force_pol"
                    + " if you wish to broadcast to all polarizations."
                )
            else:
                raise ValueError("Polarizations could not be made to match.")
        if self.type == "waterfall":
            # Populate arrays
            if self.mode == "flag":
                if issubclass(uv.__class__, UVData) and uv.future_array_shapes:
                    arr = np.zeros_like(uv.flag_array[:, np.newaxis, :, :])
                else:
                    arr = np.zeros_like(uv.flag_array)
                sarr = self.flag_array
            elif self.mode == "metric":
                if issubclass(uv.__class__, UVData) and uv.future_array_shapes:
                    arr = np.zeros_like(
                        uv.flag_array[:, np.newaxis, :, :], dtype=np.float64
                    )
                    warr = np.zeros_like(
                        uv.flag_array[:, np.newaxis, :, :], dtype=np.float64
                    )
                else:
                    arr = np.zeros_like(uv.flag_array, dtype=np.float64)
                    warr = np.zeros_like(uv.flag_array, dtype=np.float64)
                sarr = self.metric_array
            for i, t in enumerate(np.unique(self.time_array)):
                ti = np.where(uv.time_array == t)
                arr[ti, :, :, :] = sarr[i, :, :][np.newaxis, np.newaxis, :, :]
                if self.mode == "metric":
                    warr[ti, :, :, :] = self.weights_array[i, :, :][
                        np.newaxis, np.newaxis, :, :
                    ]
            if self.mode == "flag":
                self.flag_array = arr
            elif self.mode == "metric":
                self.metric_array = arr
                self.weights_array = warr

        elif self.type == "antenna":
            if self.mode == "metric":
                raise NotImplementedError(
                    "Cannot currently convert from "
                    "antenna type, metric mode to "
                    "baseline type UVFlag object."
                )
            else:
                ants_data = np.unique(uv.ant_1_array.tolist() + uv.ant_2_array.tolist())
                new_ants = np.setdiff1d(ants_data, self.ant_array)
                if new_ants.size > 0:
                    self.ant_array = np.append(self.ant_array, new_ants).tolist()
                    # make new flags of the same shape but with first axis the
                    # size of the new ants
                    flag_shape = list(self.flag_array.shape)
                    flag_shape[0] = new_ants.size
                    new_flags = np.full(flag_shape, True, dtype=bool)
                    self.flag_array = np.append(self.flag_array, new_flags, axis=0)

                baseline_flags = np.full(
                    (uv.Nblts, self.Nspws, self.Nfreqs, self.Npols), True, dtype=bool
                )

                for t_index, bl in enumerate(uv.baseline_array):
                    uvf_t_index = np.nonzero(uv.time_array[t_index] == self.time_array)[
                        0
                    ]
                    if uvf_t_index.size > 0:
                        # if the time is found in the array
                        # input the or'ed data from each antenna
                        ant1, ant2 = uv.baseline_to_antnums(bl)
                        ant1_index = np.nonzero(np.array(self.ant_array) == ant1)
                        ant2_index = np.nonzero(np.array(self.ant_array) == ant2)
                        or_flag = np.logical_or(
                            self.flag_array[ant1_index, :, :, uvf_t_index, :],
                            self.flag_array[ant2_index, :, :, uvf_t_index, :],
                        )

                        baseline_flags[t_index, :, :, :] = or_flag.copy()

                self.flag_array = baseline_flags

        # Check the frequency array for Nspws, otherwise broadcast to 1,Nfreqs
        self.freq_array = np.atleast_2d(self.freq_array)
        self.Nspws = self.freq_array.shape[0]

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

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

        self.Nants_telescope = int(uv.Nants_telescope)
        self._set_type_baseline()
        self.clear_unused_attributes()
        self.history += 'Broadcast to type "baseline". '

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

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

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

        Broadcasts the flag array to all antennas.
        This function does NOT apply flags to uv.

        Parameters
        ----------
        uv : UVCal or UVFlag object
            object of type antenna to match.
        force_pol : bool
            If True, will use 1 pol to broadcast to any other pol.
            Otherwise, will require polarizations match.
            For example, this keyword is useful if one flags on all
            pols combined, and wants to broadcast back to individual pols.
        run_check : bool
            Option to check for the existence and proper shapes of parameters
            after converting to antenna type.
        check_extra : bool
            Option to check optional parameters as well as required ones.
        run_check_acceptability : bool
            Option to check acceptable range of the values of parameters after
            converting to antenna type.

        """
        if self.type == "antenna":
            return
        if not (
            issubclass(uv.__class__, UVCal)
            or (isinstance(uv, UVFlag) and uv.type == "antenna")
        ):
            raise ValueError(
                "Must pass in UVCal object or UVFlag object of type "
                '"antenna" to match.'
            )
        if self.type != "waterfall":
            raise ValueError(
                'Cannot convert from type "' + self.type + '" to "antenna".'
            )
        # Deal with polarization
        if issubclass(uv.__class__, UVCal):
            polarr = uv.jones_array
        else:
            polarr = uv.polarization_array
        if force_pol and self.polarization_array.size == 1:
            # Use single pol for all pols, regardless
            self.polarization_array = polarr
            # Broadcast arrays
            if self.mode == "flag":
                self.flag_array = self.flag_array.repeat(
                    self.polarization_array.size, axis=-1
                )
            else:
                self.metric_array = self.metric_array.repeat(
                    self.polarization_array.size, axis=-1
                )
                self.weights_array = self.weights_array.repeat(
                    self.polarization_array.size, axis=-1
                )
            self.Npols = len(self.polarization_array)
            self._check_pol_state()

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

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

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

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

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

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

        Parameters
        ----------
        threshold : float
            Metric value over which the corresponding flag is
            set to True. Default is np.inf, which results in flags of all False.
        run_check : bool
            Option to check for the existence and proper shapes of parameters
            after converting to flag mode.
        check_extra : bool
            Option to check optional parameters as well as required ones.
        run_check_acceptability : bool
            Option to check acceptable range of the values of parameters after
            converting to flag mode.

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

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

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

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

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

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

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

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

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

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

        Parameters
        ----------
        other : UVFlag
            object to combine with self.
        axis : str
            Axis along which to combine UVFlag objects.
        run_check : bool
            Option to check for the existence and proper shapes of parameters
            after combining two objects.
        check_extra : bool
            Option to check optional parameters as well as required ones.
        run_check_acceptability : bool
            Option to check acceptable range of the values of parameters after
            combining two objects.
        inplace : bool
            Option to perform the select directly on self or return a new UVData
            object with just the selected data.

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

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

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

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

        # Simplify axis referencing
        axis = axis.lower()
        type_nums = {"waterfall": 0, "baseline": 1, "antenna": 2}
        axis_nums = {
            "time": [0, 0, 3],
            "baseline": [None, 0, None],
            "antenna": [None, None, 0],
            "frequency": [1, 2, 2],
            "polarization": [2, 3, 4],
            "pol": [2, 3, 4],
            "jones": [2, 3, 4],
        }
        ax = axis_nums[axis][type_nums[self.type]]
        if axis == "time":
            this.time_array = np.concatenate([this.time_array, other.time_array])
            this.lst_array = np.concatenate([this.lst_array, other.lst_array])
            if this.type == "baseline":
                this.baseline_array = np.concatenate(
                    [this.baseline_array, other.baseline_array]
                )
                this.ant_1_array = np.concatenate([this.ant_1_array, other.ant_1_array])
                this.ant_2_array = np.concatenate([this.ant_2_array, other.ant_2_array])
                this.Nants_data = int(
                    np.union1d(this.ant_1_array, this.ant_2_array).size
                )

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

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

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

        elif axis == "antenna":
            if self.type != "antenna":
                raise ValueError(
                    "Flag object of type " + self.type + " cannot be "
                    "concatenated along antenna axis."
                )
            this.ant_array = np.concatenate([this.ant_array, other.ant_array])
            this.Nants_data = len(this.ant_array)
        elif axis == "frequency":
            this.freq_array = np.concatenate(
                [this.freq_array, other.freq_array], axis=-1
            )
            this.Nfreqs = np.unique(this.freq_array.flatten()).size
        elif axis in ["polarization", "pol", "jones"]:
            if this.pol_collapsed:
                raise NotImplementedError(
                    "Two UVFlag objects with their "
                    "polarizations collapsed cannot be "
                    "added along the polarization axis "
                    "at this time."
                )
            this.polarization_array = np.concatenate(
                [this.polarization_array, other.polarization_array]
            )
            this.Npols = len(this.polarization_array)

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

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

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

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

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

        Parameters
        ----------
        other : UVFlag
            object to combine with self.
        axis : str
            Axis along which to combine UVFlag objects.
        run_check : bool
            Option to check for the existence and proper shapes of parameters
            after combining two objects.
        check_extra : bool
            Option to check optional parameters as well as required ones.
        run_check_acceptability : bool
            Option to check acceptable range of the values of parameters after
            combining two objects.

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

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

        Parameters
        ----------
        other : UVFlag
            object to combine with self.
        run_check : bool
            Option to check for the existence and proper shapes of parameters
            after combining two objects.
        check_extra : bool
            Option to check optional parameters as well as required ones.
        run_check_acceptability : bool
            Option to check acceptable range of the values of parameters after
            combining two objects.
        inplace : bool
            Option to perform the select directly on self or return a new UVData
            object with just the selected data.

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

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

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

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

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

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

        Parameters
        ----------
        other : UVFlag
            object to combine with self.
        run_check : bool
            Option to check for the existence and proper shapes of parameters
            after combining two objects.
        check_extra : bool
            Option to check optional parameters as well as required ones.
        run_check_acceptability : bool
            Option to check acceptable range of the values of parameters after
            combining two objects.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        else:
            ant_blt_inds = None

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

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

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

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

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

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

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

                    history_update_string += ", times"
            else:
                history_update_string += "times"

            n_selects += 1

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

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

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

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

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

            blt_inds = sorted(set(blt_inds))

        if freq_chans is not None:
            freq_chans = uvutils._get_iterable(freq_chans)
            if np.array(freq_chans).ndim > 1:
                freq_chans = np.array(freq_chans).flatten()
            if frequencies is None:
                if self.type != "waterfall":
                    frequencies = self.freq_array[0, freq_chans]
                else:
                    frequencies = self.freq_array[freq_chans]

            else:
                frequencies = uvutils._get_iterable(frequencies)
                if self.type != "waterfall":
                    frequencies = np.sort(
                        list(set(frequencies) | set(self.freq_array[0, freq_chans]))
                    )
                else:
                    frequencies = np.sort(
                        list(set(frequencies) | set(self.freq_array[freq_chans]))
                    )

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

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

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

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

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

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

        history_update_string += " using pyuvdata."

        return blt_inds, ant_inds, freq_inds, pol_inds, history_update_string

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

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

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

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

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

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

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

        self.history = self.history + history_update_string

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

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

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

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

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

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

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

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

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

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

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

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

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

        if not inplace:
            return uv_object

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

        Parameters
        ----------
        filename : str or pathlib.Path
            The file name to read.
        history : str
            History string to append to UVFlag history attribute.
        run_check : bool
            Option to check for the existence and proper shapes of parameters
            after reading data.
        check_extra : bool
            Option to check optional parameters as well as required ones.
        run_check_acceptability : bool
            Option to check acceptable range of the values of parameters after
            reading data.

        """
        if isinstance(filename, (tuple, list)):
            self.read(filename[0])
            if len(filename) > 1:
                for f in filename[1:]:
                    f2 = UVFlag(f, history=history)
                    self += f2
                del f2

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

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

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

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

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

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

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

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

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

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

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

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

                self.history += history

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

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

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

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

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

                if self.type == "baseline":

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

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

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

                    self.ant_1_array = header["ant_1_array"][()]
                    self.ant_2_array = header["ant_2_array"][()]

                    try:
                        self.Nants_telescope = int(header["Nants_telescope"][()])
                    except KeyError:
                        warnings.warn(
                            "Nants_telescope not available in file, " "assuming < 2048."
                        )
                        self.Nants_telescope = 2047

                    if "Nants_data" in header.keys():
                        self.Nants_data = int(header["Nants_data"][()])
                    else:
                        self.Nants_data = int(
                            np.unique(
                                np.union1d(self.ant_1_array, self.ant_2_array)
                            ).size
                        )

                    if "Nspws" in header.keys():
                        self.Nspws = int(header["Nspws"][()])
                    else:
                        self.Nspws = np.shape(self.freq_array)[0]

                elif self.type == "antenna":
                    self.ant_array = header["ant_array"][()]
                    try:
                        self.Nants_data = int(header["Nants_data"][()])
                    except KeyError:
                        warnings.warn(
                            "Nants_data not available in file, "
                            "attempting to calculate from ant_array."
                        )
                        self.Nants_data = len(self.ant_array)

                    if "Nspws" in header.keys():
                        self.Nspws = int(header["Nspws"][()])
                    else:
                        self.Nspws = np.shape(self.freq_array)[0]

                dgrp = f["/Data"]
                if self.mode == "metric":
                    self.metric_array = dgrp["metric_array"][()]
                    self.weights_array = dgrp["weights_array"][()]
                    if "weights_square_array" in dgrp:
                        self.weights_square_array = dgrp["weights_square_array"][()]
                elif self.mode == "flag":
                    self.flag_array = dgrp["flag_array"][()]
            self.clear_unused_attributes()

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

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

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

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

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

            # write out metadata
            header["type"] = np.string_(self.type)
            header["mode"] = np.string_(self.mode)

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

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

            header["Npols"] = self.Npols

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

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

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

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

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

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

            if self.type == "baseline":
                header["baseline_array"] = self.baseline_array
                header["Nbls"] = self.Nbls
                header["Nblts"] = self.Nblts
                header["ant_1_array"] = self.ant_1_array
                header["ant_2_array"] = self.ant_2_array
                header["Nants_data"] = self.Nants_data
                header["Nants_telescope"] = self.Nants_telescope
                header["Nspws"] = self.Nspws

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

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

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

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

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

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

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

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

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

            self.baseline_array = indata.baseline_array
            self.Nbls = np.unique(self.baseline_array).size
            self.Nblts = len(self.baseline_array)
            self.ant_1_array = indata.ant_1_array
            self.ant_2_array = indata.ant_2_array
            self.Nants_data = indata.Nants_data

            self.time_array = indata.time_array
            self.lst_array = indata.lst_array
            self.Ntimes = np.unique(self.time_array).size

            if indata.future_array_shapes:
                self.freq_array = indata.freq_array[np.newaxis, :]
            else:
                self.freq_array = indata.freq_array
            self.Nfreqs = indata.Nfreqs
            self.Nspws = indata.Nspws

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

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

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

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

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

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

        self.clear_unused_attributes()

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

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

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

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

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

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

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

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

            self.time_array = indata.time_array
            self.lst_array = lst_from_uv(indata)
            self.Ntimes = np.unique(self.time_array).size
            self.Nblts = self.Ntimes

            self.freq_array = indata.freq_array
            self.Nspws = indata.Nspws
            self.Nfreqs = np.unique(self.freq_array).size

            self.polarization_array = indata.jones_array
            self.Npols = len(self.polarization_array)
            if copy_flags:
                self.flag_array = indata.flag_array
                self.history += (
                    " Flags copied from " + str(indata.__class__) + " object."
                )
                if self.mode == "metric":
                    warnings.warn(
                        'Copying flags to type=="antenna" ' 'results in mode=="flag".'
                    )
                    self._set_mode_flag()
            else:
                if self.mode == "flag":
                    self.flag_array = np.zeros_like(indata.flag_array)
                elif self.mode == "metric":
                    self.metric_array = np.zeros_like(indata.flag_array).astype(
                        np.float64
                    )
        if self.mode == "metric":
            self.weights_array = np.ones(self.metric_array.shape)

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

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

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

        self.clear_unused_attributes()

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


# setup.py

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

import os
import io
import sys
import glob
import platform
from setuptools import setup, Extension, find_namespace_packages

import numpy
from distutils.sysconfig import get_config_var
from distutils.version import LooseVersion
from Cython.Build import cythonize

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


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


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


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


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

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

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

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

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

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

extensions = [corr_fits_extension, utils_extension]

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

astroquery_reqs = ["astroquery>=0.4.4"]
novas_reqs = ["novas", "novas_de405"]
casa_reqs = ["python-casacore>=3.1.0"]
healpix_reqs = ["astropy_healpix"]
cst_reqs = ["pyyaml"]
test_reqs = (
    casa_reqs
    + astroquery_reqs
    + healpix_reqs
    + novas_reqs
    + cst_reqs
    + [
        "pytest>=6.2",
        "pytest-xdist",
        "pytest-cases>=3",
        "pytest-cov",
        "cython",
        "coverage",
        "pre-commit",
    ]
)
doc_reqs = ["sphinx", "pypandoc"]

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

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


# pyuvdata/__init__.py

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

"""Init file for pyuvdata."""
import warnings
from setuptools_scm import get_version
from pathlib import Path
from pkg_resources import get_distribution, DistributionNotFound

from .branch_scheme import branch_scheme


try:  # pragma: nocover
    # get accurate version for developer installs
    version_str = get_version(Path(__file__).parent.parent, local_scheme=branch_scheme)

    __version__ = version_str

except (LookupError, ImportError):
    try:
        # Set the version automatically from the package details.
        __version__ = get_distribution(__name__).version
    except DistributionNotFound:  # pragma: nocover
        # package is not installed
        pass

# Filter annoying Cython warnings that serve no good purpose. see numpy#432
# needs to be done before the imports to work properly
warnings.filterwarnings("ignore", message="numpy.dtype size changed")
warnings.filterwarnings("ignore", message="numpy.ufunc size changed")

from .uvdata import UVData  # noqa
from .telescopes import known_telescopes, Telescope, get_telescope  # noqa
from .uvcal import UVCal  # noqa
from .uvbeam import UVBeam  # noqa
from .uvflag import UVFlag  # noqa

__all__ = [
    "UVData",
    "UVCal",
    "UVFlag",
    "UVBeam",
    "Telescope",
    "known_telescopes",
    "get_telescope",
]


# adapted from https://github.com/astropy/astropy/__init__.py
# please consult astropy/__init__.py for clarification on logic details

# Cleanup the top-level namespace.
# Delete everything that is not in __all__, a magic function,
# or is a submodule of this package
from types import ModuleType as __module_type__  # noqa

for varname in dir():
    if not (
        varname in __all__
        or (varname.startswith("__") and varname.endswith("__"))
        or (
            varname[0] != "_"
            and isinstance(locals()[varname], __module_type__)
            and locals()[varname].__name__.startswith(__name__ + ".")
        )
    ):
        del locals()[varname]

del varname, __module_type__


# pyuvdata/branch_scheme.py

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

"""Define branch_scheme for versioning."""


def branch_scheme(version):  # pragma: nocover
    """
    Local version scheme that adds the branch name for absolute reproducibility.

    If and when this is added to setuptools_scm this function and file can be removed.
    """
    if version.exact or version.node is None:
        return version.format_choice("", "+d{time:{time_format}}", time_format="%Y%m%d")
    else:
        if version.branch == "main":
            return version.format_choice("+{node}", "+{node}.dirty")
        else:
            return version.format_choice("+{node}.{branch}", "+{node}.{branch}.dirty")


# pyuvdata/conftest.py

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

"""Testing environment setup and teardown for pytest."""
import os

import pytest
from astropy.utils import iers
from astropy.time import Time
from astropy.coordinates import EarthLocation
import numpy as np

from pyuvdata.data import DATA_PATH
from pyuvdata import UVData, UVCal, UVBeam
import pyuvdata.tests as uvtest
from pyuvdata.uvdata.mir import mir_parser

filenames = ["HERA_NicCST_150MHz.txt", "HERA_NicCST_123MHz.txt"]
cst_folder = "NicCSTbeams"
cst_files = [os.path.join(DATA_PATH, cst_folder, f) for f in filenames]
casa_tutorial_uvfits = os.path.join(
    DATA_PATH, "day2_TDEM0003_10s_norx_1src_1spw.uvfits"
)
paper_miriad_file = os.path.join(DATA_PATH, "zen.2456865.60537.xy.uvcRREAA")


@pytest.fixture(autouse=True, scope="session")
def setup_and_teardown_package():
    """Handle possible IERS issues."""
    # Do a calculation that requires a current IERS table. This will trigger
    # automatic downloading of the IERS table if needed, including trying the
    # mirror site in python 3 (but won't redownload if a current one exists).
    # If there's not a current IERS table and it can't be downloaded, turn off
    # auto downloading for the tests and turn it back on once all tests are
    # completed (done by extending auto_max_age).
    # Also, the check_warnings function will ignore IERS-related warnings.
    try:
        t1 = Time.now()
        t1.ut1
    except (Exception):
        iers.conf.auto_max_age = None

    # Also ensure that we're downloading the site data from astropy
    EarthLocation._get_site_registry(force_download=True)

    yield

    iers.conf.auto_max_age = 30


@pytest.fixture(scope="session")
def casa_uvfits_main():
    """Read in CASA tutorial uvfits file."""
    uv_in = UVData()
    with uvtest.check_warnings(
        UserWarning,
        [
            "Telescope EVLA is not in known_telescopes",
            "The uvw_array does not match the expected values",
        ],
    ):
        uv_in.read(casa_tutorial_uvfits)

    return uv_in


@pytest.fixture(scope="function")
def casa_uvfits(casa_uvfits_main):
    """Make function level CASA tutorial uvfits object."""
    casa_uvfits = casa_uvfits_main.copy()
    yield casa_uvfits

    # clean up when done
    del casa_uvfits

    return


@pytest.fixture(scope="session")
def paper_miriad_main():
    """Read in PAPER miriad file."""
    pytest.importorskip("pyuvdata.uvdata.aipy_extracts")
    uv_in = UVData()
    uv_in.read(paper_miriad_file)

    return uv_in


@pytest.fixture(scope="function")
def paper_miriad(paper_miriad_main):
    """Make function level PAPER miriad object."""
    uv_in = paper_miriad_main.copy()

    yield uv_in

    # cleanup
    del uv_in


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

    yield gain_object

    del gain_object


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

    yield gain_object

    del gain_object


@pytest.fixture(scope="session")
def delay_data_main():
    """Read in delay calfits file, add input flag array."""
    delay_object = UVCal()
    delayfile = os.path.join(DATA_PATH, "zen.2457698.40355.xx.delay.calfits")
    delay_object.read_calfits(delayfile)

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

    del delay_object


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

    yield delay_object

    del delay_object


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

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

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

    del delay_object


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

    yield delay_object

    del delay_object


def make_cst_beam(beam_type):
    """Make the default CST testing beam."""
    extra_keywords = {
        "software": "CST 2016",
        "sim_type": "E-farfield",
        "layout": "1 antenna",
        "port_num": 1,
    }

    beam = UVBeam()
    beam.read_cst_beam(
        cst_files,
        beam_type=beam_type,
        frequency=[150e6, 123e6],
        telescope_name="HERA",
        feed_name="Dipole",
        feed_version="1.0",
        feed_pol=["x"],
        model_name="Dipole - Rigging height 4.9 m",
        model_version="1.0",
        x_orientation="east",
        reference_impedance=100,
        history=(
            "Derived from https://github.com/Nicolas-Fagnoni/Simulations."
            "\nOnly 2 files included to keep test data volume low."
        ),
        extra_keywords=extra_keywords,
    )
    return beam


def cut_beam(beam):
    """Downselect a beam to a small sky area to speed tests up."""
    za_max = np.deg2rad(10.0)
    za_inds_use = np.nonzero(beam.axis2_array <= za_max)[0]
    beam.select(axis2_inds=za_inds_use)
    return beam


def single_freq_version(beam):
    """Make a single freq version with expected history and filename."""
    history_use = beam.history[: beam.history.find(" Combined data")]
    beam.select(freq_chans=1)
    beam.history = history_use
    beam.filename = [beam.filename[1]]
    beam._filename.form = (1,)
    return beam


@pytest.fixture(scope="session")
def cst_efield_2freq_main():
    """Make session level 2-freq efield beam."""
    return make_cst_beam("efield")


@pytest.fixture(scope="function")
def cst_efield_2freq(cst_efield_2freq_main):
    """Make function level 2-freq efield beam."""
    return cst_efield_2freq_main.copy()


@pytest.fixture(scope="session")
def cst_efield_2freq_cut_main(cst_efield_2freq_main):
    """Make session level cut down 2-freq efield beam."""
    return cut_beam(cst_efield_2freq_main.copy())


@pytest.fixture(scope="function")
def cst_efield_2freq_cut(cst_efield_2freq_cut_main):
    """Make function level cut down 2-freq efield beam."""
    return cst_efield_2freq_cut_main.copy()


@pytest.fixture(scope="session")
def cst_efield_2freq_cut_healpix_main(cst_efield_2freq_cut_main):
    """Make session level cut down HEALPix 2-freq efield beam."""
    pytest.importorskip("astropy_healpix")
    beam = cst_efield_2freq_cut_main.copy()
    beam.interpolation_function = "az_za_simple"
    beam.to_healpix()
    return beam


@pytest.fixture(scope="function")
def cst_efield_2freq_cut_healpix(cst_efield_2freq_cut_healpix_main):
    """Make function level cut down HEALPix 2-freq efield beam."""
    return cst_efield_2freq_cut_healpix_main.copy()


@pytest.fixture(scope="session")
def cst_efield_1freq_main(cst_efield_2freq_main):
    """Make session level single freq efield beam."""
    return single_freq_version(cst_efield_2freq_main.copy())


@pytest.fixture(scope="function")
def cst_efield_1freq(cst_efield_1freq_main):
    """Make function level single freq efield beam."""
    return cst_efield_1freq_main.copy()


@pytest.fixture(scope="session")
def cst_efield_1freq_cut_main(cst_efield_2freq_cut_main):
    """Make session level cut down single freq efield beam."""
    return single_freq_version(cst_efield_2freq_cut_main.copy())


@pytest.fixture(scope="function")
def cst_efield_1freq_cut(cst_efield_1freq_cut_main):
    """Make function level cut down single freq efield beam."""
    return cst_efield_1freq_cut_main.copy()


@pytest.fixture(scope="session")
def cst_efield_1freq_cut_healpix_main(cst_efield_2freq_cut_healpix_main):
    """Make session level HEALPix cut down single freq efield beam."""
    return single_freq_version(cst_efield_2freq_cut_healpix_main.copy())


@pytest.fixture(scope="function")
def cst_efield_1freq_cut_healpix(cst_efield_1freq_cut_healpix_main):
    """Make function level HEALPix cut down single freq efield beam."""
    return cst_efield_1freq_cut_healpix_main.copy()


@pytest.fixture(scope="session")
def cst_power_2freq_main():
    """Make session level 2-freq power beam."""
    return make_cst_beam("power")


@pytest.fixture(scope="function")
def cst_power_2freq(cst_power_2freq_main):
    """Make function level 2-freq efield beam."""
    return cst_power_2freq_main.copy()


@pytest.fixture(scope="session")
def cst_power_2freq_cut_main(cst_power_2freq_main):
    """Make session level cut down 2-freq power beam."""
    return cut_beam(cst_power_2freq_main.copy())


@pytest.fixture(scope="function")
def cst_power_2freq_cut(cst_power_2freq_cut_main):
    """Make function level cut down 2-freq power beam."""
    return cst_power_2freq_cut_main.copy()


@pytest.fixture(scope="session")
def cst_power_2freq_cut_healpix_main(cst_power_2freq_cut_main):
    """Make session level HEALPix cut down 2-freq power beam."""
    pytest.importorskip("astropy_healpix")
    beam = cst_power_2freq_cut_main.copy()
    beam.interpolation_function = "az_za_simple"
    beam.to_healpix()
    return beam


@pytest.fixture(scope="function")
def cst_power_2freq_cut_healpix(cst_power_2freq_cut_healpix_main):
    """Make function level HEALPix cut down 2-freq power beam."""
    return cst_power_2freq_cut_healpix_main.copy()


@pytest.fixture(scope="session")
def cst_power_1freq_main(cst_power_2freq_main):
    """Make session level single freq power beam."""
    return single_freq_version(cst_power_2freq_main.copy())


@pytest.fixture(scope="function")
def cst_power_1freq(cst_power_1freq_main):
    """Make function level single freq power beam."""
    return cst_power_1freq_main.copy()


@pytest.fixture(scope="session")
def cst_power_1freq_cut_main(cst_power_2freq_cut_main):
    """Make session level cut down single freq power beam."""
    return single_freq_version(cst_power_2freq_cut_main.copy())


@pytest.fixture(scope="function")
def cst_power_1freq_cut(cst_power_1freq_cut_main):
    """Make function level cut down single freq power beam."""
    return cst_power_1freq_cut_main.copy()


@pytest.fixture(scope="session")
def cst_power_1freq_cut_healpix_main(cst_power_2freq_cut_healpix_main):
    """Make session level HEALPix cut down single freq power beam."""
    return single_freq_version(cst_power_2freq_cut_healpix_main.copy())


@pytest.fixture(scope="function")
def cst_power_1freq_cut_healpix(cst_power_1freq_cut_healpix_main):
    """Make function level HEALPix cut down single freq power beam."""
    return cst_power_1freq_cut_healpix_main.copy()


@pytest.fixture(params=[True, False])
def mir_data_object(request):
    """Make MIR data object for tests. Param to read autocorr data."""
    has_auto = request.param
    testfile = os.path.join(DATA_PATH, "sma_test.mir")
    mir_data = mir_parser.MirParser(
        testfile, load_vis=True, load_raw=True, load_auto=True, has_auto=has_auto
    )

    yield mir_data

    # cleanup
    del mir_data


# pyuvdata/parameter.py

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

"""
Define UVParameters: data and metadata objects for interferometric data sets.

UVParameters are objects to hold specific data and metadata associated with
interferometric data sets. They are used as attributes for classes based on
UVBase. This module also includes specialized sublasses for particular types
of metadata.

"""
import builtins
import numpy as np
import astropy.units as units

from . import utils

__all__ = ["UVParameter", "AngleParameter", "LocationParameter"]


def _get_generic_type(expected_type, strict_type_check=False):
    """Return tuple of more generic types.

    Allows for more flexible type checking in the case when a Parameter's value
    changes precison or to/from a numpy dtype but still is the desired generic type.
    If a generic type cannot be found, the expected_type is returned

    Parameters
    ----------
    expected_type : Type or string
        The expected type of a Parameter object or a string of the name of a type.
    strict_type_check : bool
        If True the input expected_type is return exactly
    if strict_type_check:
        return expected_type exactly

    Returns
    -------
    Tuple of types based on input expected_type

    """
    if isinstance(expected_type, str):
        try:
            expected_type = getattr(builtins, expected_type)
        except AttributeError as err:
            raise ValueError(
                f"Input expected_type is a string with value: '{expected_type}'. "
                "When the expected_type is a string, it must be a Python builtin type."
            ) from err
    if strict_type_check:
        return expected_type

    for types in [
        (bool, np.bool_),
        (float, np.floating),
        (np.unsignedinteger),  # unexpected but just in case
        (int, np.integer),
        (complex, np.complexfloating),
    ]:
        if issubclass(expected_type, types):
            return types

    return expected_type


class UVParameter(object):
    """
    Data and metadata objects for interferometric data sets.

    Attributes
    ----------
    name : str
        The name of the attribute. Used as the associated property name in
        classes based on UVBase.
    required : bool
        Flag indicating whether this is required metadata for
        the class with this UVParameter as an attribute. Default is True.
    value
        The value of the data or metadata.
    spoof_val
        A fake value that can be assigned to a non-required UVParameter if the
        metadata is required for a particular file-type.
        This is not an attribute of required UVParameters.
    form : 'str' or tuple
        Either 'str' or a tuple giving information about the expected
        shape of the value. Elements of the tuple may be the name of other
        UVParameters that indicate data shapes.

        Form examples:
            - 'str': a string value
            - ('Nblts', 3): the value should be an array of shape:
               Nblts (another UVParameter name), 3

    description : str
        Description of the data or metadata in the object.
    expected_type
        The type that the data or metadata should be. Default is int or str if
        form is 'str'.
    acceptable_vals : list, optional
        List giving allowed values for elements of value.
    acceptable_range: 2-tuple, optional
        Tuple giving a range of allowed magnitudes for elements of value.
    tols : float or 2-tuple of float
        Tolerances for testing the equality of UVParameters. Either a single
        absolute value or a tuple of relative and absolute values to be used by
        np.isclose()
    strict_type_check : bool
        When True, the input expected_type is used exactly, otherwise a more
        generic type is found to allow changes in precicions or to/from numpy
        dtypes to not break checks.

    """

    def __init__(
        self,
        name,
        required=True,
        value=None,
        spoof_val=None,
        form=(),
        description="",
        expected_type=int,
        acceptable_vals=None,
        acceptable_range=None,
        tols=(1e-05, 1e-08),
        strict_type_check=False,
    ):
        """Init UVParameter object."""
        self.name = name
        self.required = required
        # cannot set a spoof_val for required parameters
        if not self.required:
            self.spoof_val = spoof_val
        self.value = value
        self.description = description
        self.form = form
        if self.form == "str":
            self.expected_type = str
            self.strict_type = True
        else:
            self.expected_type = _get_generic_type(
                expected_type, strict_type_check=strict_type_check,
            )
            self.strict_type = strict_type_check
        self.acceptable_vals = acceptable_vals
        self.acceptable_range = acceptable_range
        if np.size(tols) == 1:
            # Only one tolerance given, assume absolute, set relative to zero
            self.tols = (0, tols)
        else:
            # relative and absolute tolerances to be used in np.isclose
            self.tols = tols

    def __eq__(self, other):
        """Equal if classes match and values are identical."""
        if isinstance(other, self.__class__):
            if self.value is None:
                if other.value is not None:
                    print(f"{self.name} is None on left, but not right")
                    return False
                else:
                    return True
            if other.value is None:
                if self.value is not None:
                    print(f"{self.name} is None on right, but not left")
                    return False

            if isinstance(self.value, np.ndarray) and not isinstance(
                self.value.item(0), (str, np.str_)
            ):
                if not isinstance(other.value, np.ndarray):
                    print(f"{self.name} parameter value is array, but other is not")
                    return False
                if self.value.shape != other.value.shape:
                    print(f"{self.name} parameter value is array, shapes are different")
                    return False

                # check to see if strict types are used
                if self.strict_type:
                    # types must match
                    if other.strict_type:
                        # both strict, expected_type must match
                        if self.expected_type != other.expected_type:
                            print(
                                f"{self.name} parameter has incompatible types. "
                                f"Left is {self.expected_type}, right is "
                                f"{other.expected_type}"
                            )
                            return False
                    elif not isinstance(self.value.item(0), other.expected_type):
                        print(
                            f"{self.name} parameter has incompatible dtypes. Left "
                            f"requires {self.expected_type}, right is "
                            f"{other.value.dtype}"
                        )
                        return False
                elif other.strict_type:
                    # types must match in the other direction
                    if not isinstance(other.value.item(0), self.expected_type):
                        print(
                            f"{self.name} parameter has incompatible dtypes. Left is "
                            f"{self.value.dtype}, right requires {other.expected_type}"
                        )
                        return False

                elif isinstance(self.value, units.Quantity):
                    if not self.value.unit.is_equivalent(other.value.unit):
                        print(
                            f"{self.name} parameter value is an astropy Quantity, "
                            "units are not equivalent"
                        )
                        return False
                    if not isinstance(self.tols[1], units.Quantity):
                        atol_use = self.tols[1] * self.value.unit
                    else:
                        atol_use = self.tols[1]
                    if not units.quantity.allclose(
                        self.value,
                        other.value,
                        rtol=self.tols[0],
                        atol=atol_use,
                        equal_nan=True,
                    ):
                        print(
                            f"{self.name} parameter value is an astropy Quantity, "
                            "values are not close"
                        )
                        return False
                elif not np.allclose(
                    self.value,
                    other.value,
                    rtol=self.tols[0],
                    atol=self.tols[1],
                    equal_nan=True,
                ):
                    print(f"{self.name} parameter value is array, values are not close")
                    return False
            else:
                # check to see if strict types are used
                if self.strict_type:
                    # types must match
                    if not isinstance(self.value, other.expected_type):
                        print(
                            f"{self.name} parameter has incompatible types. Left "
                            f"requires {type(self.value)}, right is "
                            f"{other.expected_type}"
                        )
                        return False
                if other.strict_type:
                    # types must match in the other direction
                    if not isinstance(other.value, self.expected_type):
                        print(
                            f"{self.name} parameter has incompatible types. Left is "
                            f"{self.expected_type}, right requires {type(other.value)}"
                        )
                        return False

                str_type = False
                if isinstance(self.value, str):
                    str_type = True
                if isinstance(self.value, (list, np.ndarray, tuple)):
                    if isinstance(self.value[0], str):
                        str_type = True

                if not str_type:
                    if isinstance(other.value, np.ndarray):
                        print(
                            f"{self.name} parameter value is not an array, "
                            "but other is not"
                        )
                        return False
                    try:
                        if not np.allclose(
                            np.array(self.value),
                            np.array(other.value),
                            rtol=self.tols[0],
                            atol=self.tols[1],
                            equal_nan=True,
                        ):
                            print(
                                f"{self.name} parameter value can be cast to an array"
                                " and tested with np.allclose. The values are "
                                "not close"
                            )
                            return False
                    except (TypeError):
                        if isinstance(self.value, dict):
                            try:
                                # Try a naive comparison first
                                # this will fail if keys are the same
                                # but cases differ.
                                # so only look for exact equality
                                # then default to the long test below.
                                if self.value == other.value:
                                    return True
                            except ValueError:
                                pass
                                # this dict probably contains arrays
                                # we will need to check each item individually

                            # check to see if they are equal other than
                            # upper/lower case keys
                            self_lower = {k.lower(): v for k, v in self.value.items()}
                            other_lower = {k.lower(): v for k, v in other.value.items()}
                            message_str = f"{self.name} parameter is a dict"
                            if set(self_lower.keys()) != set(other_lower.keys()):
                                message_str += ", keys are not the same."
                                print(message_str)
                                return False
                            else:
                                # need to check if values are close,
                                # not just equal
                                values_close = True
                                for key in self_lower.keys():
                                    try:
                                        if not np.allclose(
                                            self_lower[key], other_lower[key]
                                        ):
                                            message_str += f", key {key} is not equal"
                                            values_close = False
                                    except (TypeError):
                                        # this isn't a type that can be
                                        # handled by np.isclose,
                                        # test for equality
                                        if self_lower[key] != other_lower[key]:
                                            message_str += f", key {key} is not equal"
                                            values_close = False
                                if values_close is False:
                                    print(message_str)
                                    return False
                                else:
                                    return True
                        else:
                            if self.value != other.value:
                                print(
                                    f"{self.name} parameter value is not a string "
                                    "or a dict and cannot be cast as a numpy "
                                    "array. The values are not equal."
                                )

                                return False

                else:
                    if isinstance(self.value, (list, np.ndarray, tuple)):
                        if [s.strip() for s in self.value] != [
                            s.strip() for s in other.value
                        ]:
                            print(
                                f"{self.name} parameter value is a list of strings, "
                                "values are different"
                            )
                            return False
                    else:
                        if self.value.strip() != other.value.strip():
                            if self.value.replace("\n", "").replace(
                                " ", ""
                            ) != other.value.replace("\n", "").replace(" ", ""):
                                print(
                                    f"{self.name} parameter value is a string, "
                                    "values are different"
                                )
                                return False

            return True
        else:
            print(f"{self.name} parameter classes are different")
            return False

    def __ne__(self, other):
        """Not equal."""
        return not self.__eq__(other)

    def apply_spoof(self):
        """Set value to spoof_val for non-required UVParameters."""
        self.value = self.spoof_val

    def expected_shape(self, uvbase):
        """
        Get the expected shape of the value based on the form.

        Parameters
        ----------
        uvbase : object
            Object with this UVParameter as an attribute. Needed
            because the form can refer to other UVParameters on this object.

        Returns
        -------
        tuple
            The expected shape of the value.
        """
        if self.form == "str":
            return self.form
        elif isinstance(self.form, (int, np.integer)):
            # Fixed shape, just return the form
            return (self.form,)
        else:
            # Given by other attributes, look up values
            eshape = ()
            for p in self.form:
                if isinstance(p, (int, np.integer)):
                    eshape = eshape + (p,)
                else:
                    val = getattr(uvbase, p)
                    if val is None:
                        raise ValueError(
                            f"Missing UVBase parameter {p} needed to "
                            "calculate expected shape of parameter"
                        )
                    eshape = eshape + (val,)
            return eshape

    def check_acceptability(self):
        """Check that values are acceptable."""
        if self.acceptable_vals is None and self.acceptable_range is None:
            return True, "No acceptability check"
        else:
            # either acceptable_vals or acceptable_range is set. Prefer acceptable_vals
            if self.acceptable_vals is not None:
                # acceptable_vals are a list of allowed values
                if self.expected_type is str:
                    # strings need to be converted to lower case
                    if isinstance(self.value, str):
                        value_set = {self.value.lower()}
                    else:
                        # this is a list or array of strings, make them all lower case
                        value_set = {x.lower() for x in self.value}
                    acceptable_vals = [x.lower() for x in self.acceptable_vals]
                else:
                    if isinstance(self.value, (list, np.ndarray)):
                        value_set = set(self.value)
                    else:
                        value_set = {self.value}
                    acceptable_vals = self.acceptable_vals
                for elem in value_set:
                    if elem not in acceptable_vals:
                        message = (
                            f"Value {elem}, is not in allowed values: {acceptable_vals}"
                        )
                        return False, message
                return True, "Value is acceptable"
            else:
                # acceptable_range is a tuple giving a range of allowed magnitudes
                testval = np.mean(np.abs(self.value))
                if (testval >= self.acceptable_range[0]) and (
                    testval <= self.acceptable_range[1]
                ):
                    return True, "Value is acceptable"
                else:
                    message = (
                        f"Mean of abs values, {testval}, is not in allowed range: "
                        f"{self.acceptable_range}"
                    )
                    return False, message


class AngleParameter(UVParameter):
    """
    Subclass of UVParameter for Angle type parameters.

    Adds extra methods for conversion to & from degrees (used by UVBase objects
    for _degrees properties associated with these parameters).
    """

    def degrees(self):
        """Get value in degrees."""
        if self.value is None:
            return None
        else:
            return self.value * 180.0 / np.pi

    def set_degrees(self, degree_val):
        """
        Set value in degrees.

        Parameters
        ----------
        degree_val : float
            Value in degrees to use to set the value attribute.
        """
        if degree_val is None:
            self.value = None
        else:
            self.value = degree_val * np.pi / 180.0


class LocationParameter(UVParameter):
    """
    Subclass of UVParameter for Earth location type parameters.

    Adds extra methods for conversion to & from lat/lon/alt in radians or
    degrees (used by UVBase objects for _lat_lon_alt and _lat_lon_alt_degrees
    properties associated with these parameters).

    """

    def __init__(
        self,
        name,
        required=True,
        value=None,
        spoof_val=None,
        description="",
        acceptable_range=(6.35e6, 6.39e6),
        tols=1e-3,
    ):
        super(LocationParameter, self).__init__(
            name,
            required=required,
            value=value,
            spoof_val=spoof_val,
            form=3,
            description=description,
            expected_type=float,
            acceptable_range=acceptable_range,
            tols=tols,
        )

    def lat_lon_alt(self):
        """Get value in (latitude, longitude, altitude) tuple in radians."""
        if self.value is None:
            return None
        else:
            # check defaults to False b/c exposed check kwarg exists in UVData
            return utils.LatLonAlt_from_XYZ(self.value, check_acceptability=False)

    def set_lat_lon_alt(self, lat_lon_alt):
        """
        Set value from (latitude, longitude, altitude) tuple in radians.

        Parameters
        ----------
        lat_lon_alt : 3-tuple of float
            Tuple with the latitude (radians), longitude (radians)
            and altitude (meters) to use to set the value attribute.
        """
        if lat_lon_alt is None:
            self.value = None
        else:
            self.value = utils.XYZ_from_LatLonAlt(
                lat_lon_alt[0], lat_lon_alt[1], lat_lon_alt[2]
            )

    def lat_lon_alt_degrees(self):
        """Get value in (latitude, longitude, altitude) tuple in degrees."""
        if self.value is None:
            return None
        else:
            latitude, longitude, altitude = self.lat_lon_alt()
            return latitude * 180.0 / np.pi, longitude * 180.0 / np.pi, altitude

    def set_lat_lon_alt_degrees(self, lat_lon_alt_degree):
        """
        Set value from (latitude, longitude, altitude) tuple in degrees.

        Parameters
        ----------
        lat_lon_alt : 3-tuple of float
            Tuple with the latitude (degrees), longitude (degrees)
            and altitude (meters) to use to set the value attribute.

        """
        if lat_lon_alt_degree is None:
            self.value = None
        else:
            latitude, longitude, altitude = lat_lon_alt_degree
            self.value = utils.XYZ_from_LatLonAlt(
                latitude * np.pi / 180.0, longitude * np.pi / 180.0, altitude
            )

    def check_acceptability(self):
        """Check that vector magnitudes are in range."""
        if self.acceptable_range is None:
            return True, "No acceptability check"
        else:
            # acceptable_range is a tuple giving a range of allowed vector magnitudes
            testval = np.sqrt(np.sum(np.abs(self.value) ** 2))
            if (testval >= self.acceptable_range[0]) and (
                testval <= self.acceptable_range[1]
            ):
                return True, "Value is acceptable"
            else:
                message = (
                    f"Value {testval}, is not in allowed range: {self.acceptable_range}"
                )
                return False, message


# pyuvdata/telescopes.py

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

"""Telescope information and known telescope list."""
import os
import numpy as np
from astropy.coordinates import Angle, EarthLocation

from . import uvbase
from . import parameter as uvp
from pyuvdata.data import DATA_PATH

__all__ = ["Telescope", "known_telescopes", "get_telescope"]

# We use astropy sites for telescope locations. The dict below is for
# telescopes not in astropy sites, or to include extra information for a telescope.

# The center_xyz is the location of the telescope in ITRF (earth-centered frame)

# Antenna positions can be specified via a csv file with the following columns:
# "name" -- antenna name, "number" -- antenna number, "x", "y", "z" -- ECEF coordinates
# relative to the telescope location.
KNOWN_TELESCOPES = {
    "PAPER": {
        "center_xyz": None,
        "latitude": Angle("-30d43m17.5s").radian,
        "longitude": Angle("21d25m41.9s").radian,
        "altitude": 1073.0,
        "citation": (
            "value taken from capo/cals/hsa7458_v000.py, "
            "comment reads KAT/SA  (GPS), altitude from elevationmap.net"
        ),
    },
    "HERA": {
        "center_xyz": None,
        "latitude": Angle("-30.72152612068925d").radian,
        "longitude": Angle("21.42830382686301d").radian,
        "altitude": 1051.69,
        "diameters": 14.0,
        "antenna_positions_file": "hera_ant_pos.csv",
        "citation": (
            "value taken from hera_mc geo.py script "
            "(using hera_cm_db_updates under the hood.)"
        ),
    },
    "SMA": {
        "center_xyz": None,
        "latitude": Angle("19d49m27.13895s").radian,
        "longitude": Angle("-155d28m39.08279s").radian,
        "altitude": 4083.948144,
        "citation": "Ho, P. T. P., Moran, J. M., & Lo, K. Y. 2004, ApJL, 616, L1",
    },
    "SZA": {
        "center_xyz": None,
        "latitude": Angle("37d16m49.3698s").radian,
        "longitude": Angle("-118d08m29.9126s").radian,
        "altitude": 2400.0,
        "citation": "Unknown",
    },
    "OVRO-LWA": {
        "center_xyz": None,
        "latitude": Angle("37.239777271d").radian,
        "longitude": Angle("-118.281666695d").radian,
        "altitude": 1183.48,
        "citation": "OVRO Sharepoint Documentation",
    },
}


class Telescope(uvbase.UVBase):
    """
    A class for defining a telescope for use with UVData objects.

    Attributes
    ----------
    citation : str
        text giving source of telescope information
    telescope_name : UVParameter of str
        name of the telescope
    telescope_location : UVParameter of array_like
        telescope location xyz coordinates in ITRF (earth-centered frame).
    antenna_diameters : UVParameter of float
        Optional, antenna diameters in meters. Used by CASA to construct a
        default beam if no beam is supplied.
    """

    def __init__(self):
        """Create a new Telescope object."""
        # add the UVParameters to the class
        # use the same names as in UVData so they can be automatically set
        self.citation = None

        self._telescope_name = uvp.UVParameter(
            "telescope_name", description="name of telescope " "(string)", form="str"
        )
        desc = (
            "telescope location: xyz in ITRF (earth-centered frame). "
            "Can also be set using telescope_location_lat_lon_alt or "
            "telescope_location_lat_lon_alt_degrees properties"
        )
        self._telescope_location = uvp.LocationParameter(
            "telescope_location",
            description=desc,
            acceptable_range=(6.35e6, 6.39e6),
            tols=1e-3,
        )
        desc = (
            "Antenna diameters in meters. Used by CASA to "
            "construct a default beam if no beam is supplied."
        )
        self._antenna_diameters = uvp.UVParameter(
            "antenna_diameters",
            required=False,
            description=desc,
            expected_type=float,
            tols=1e-3,  # 1 mm
        )

        desc = "Number of antennas in the array."
        self._Nants_telescope = uvp.UVParameter(
            "Nants_telescope", required=False, description=desc, expected_type=int
        )

        desc = (
            "List of antenna names, shape (Nants_telescope), "
            "with numbers given by antenna_numbers."
        )
        self._antenna_names = uvp.UVParameter(
            "antenna_names",
            required=False,
            description=desc,
            form=("Nants_telescope",),
            expected_type=str,
        )

        desc = (
            "List of integer antenna numbers corresponding to antenna_names, "
            "shape (Nants_telescope)."
        )
        self._antenna_numbers = uvp.UVParameter(
            "antenna_numbers",
            required=False,
            description=desc,
            form=("Nants_telescope",),
            expected_type=int,
        )

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

        super(Telescope, self).__init__()


def known_telescopes():
    """
    Get list of known telescopes.

    Returns
    -------
    list of str
        List of known telescope names.
    """
    astropy_sites = EarthLocation.get_site_names()
    while "" in astropy_sites:
        astropy_sites.remove("")
    known_telescopes = list(set(astropy_sites + list(KNOWN_TELESCOPES.keys())))
    return known_telescopes


def _parse_antpos_file(antenna_positions_file):
    """
    Interpret the antenna positions file.

    Parameters
    ----------
    antenna_positions_file : str
        Name of the antenna_positions_file, which is assumed to be in DATA_PATH.
        Should contain antenna names, numbers and ECEF positions relative to the
        telescope location.

    Returns
    -------
    antenna_names : array of str
        Antenna names.
    antenna_names : array of int
        Antenna numbers.
    antenna_positions : array of float
        Antenna positions in ECEF relative to the telescope location.

    """
    columns = ["name", "number", "x", "y", "z"]
    formats = ["U10", "i8", np.longdouble, np.longdouble, np.longdouble]

    dt = np.format_parser(formats, columns, [])
    ant_array = np.genfromtxt(
        antenna_positions_file,
        delimiter=",",
        autostrip=True,
        skip_header=1,
        dtype=dt.dtype,
    )
    antenna_names = ant_array["name"]
    antenna_numbers = ant_array["number"]
    antenna_positions = np.stack((ant_array["x"], ant_array["y"], ant_array["z"])).T

    return antenna_names, antenna_numbers, antenna_positions.astype("float")


def get_telescope(telescope_name, telescope_dict_in=None):
    """
    Get Telescope object for a telescope in telescope_dict.

    Parameters
    ----------
    telescope_name : str
        Name of a telescope
    telescope_dict_in: dict
        telescope info dict. Default is None, meaning use KNOWN_TELESCOPES
        (other values are only used for testing)

    Returns
    -------
    Telescope object
        The Telescope object associated with telescope_name.
    """
    if telescope_dict_in is None:
        telescope_dict_in = KNOWN_TELESCOPES

    astropy_sites = EarthLocation.get_site_names()
    telescope_keys = list(telescope_dict_in.keys())
    telescope_list = [tel.lower() for tel in telescope_keys]
    if telescope_name in astropy_sites:
        telescope_loc = EarthLocation.of_site(telescope_name)

        obj = Telescope()
        obj.telescope_name = telescope_name
        obj.citation = "astropy sites"
        obj.telescope_location = np.array(
            [telescope_loc.x.value, telescope_loc.y.value, telescope_loc.z.value]
        )

    elif telescope_name.lower() in telescope_list:
        telescope_index = telescope_list.index(telescope_name.lower())
        telescope_dict = telescope_dict_in[telescope_keys[telescope_index]]
        obj = Telescope()
        obj.citation = telescope_dict["citation"]
        obj.telescope_name = telescope_keys[telescope_index]
        if telescope_dict["center_xyz"] is not None:
            obj.telescope_location = telescope_dict["center_xyz"]
        else:
            if (
                telescope_dict["latitude"] is None
                or telescope_dict["longitude"] is None
                or telescope_dict["altitude"] is None
            ):
                raise ValueError(
                    "either the center_xyz or the "
                    "latitude, longitude and altitude of the "
                    "telescope must be specified"
                )
            obj.telescope_location_lat_lon_alt = (
                telescope_dict["latitude"],
                telescope_dict["longitude"],
                telescope_dict["altitude"],
            )
    else:
        # no telescope matching this name
        return False

    # check for extra info
    if telescope_name.lower() in telescope_list:
        telescope_index = telescope_list.index(telescope_name.lower())
        telescope_dict = telescope_dict_in[telescope_keys[telescope_index]]
        if "diameters" in telescope_dict.keys():
            obj.antenna_diameters = telescope_dict["diameters"]

        if "antenna_positions_file" in telescope_dict.keys():
            antpos_file = os.path.join(
                DATA_PATH, telescope_dict["antenna_positions_file"]
            )
            antenna_names, antenna_numbers, antenna_positions = _parse_antpos_file(
                antpos_file
            )
            obj.Nants_telescope = antenna_names.size
            obj.antenna_names = antenna_names
            obj.antenna_numbers = antenna_numbers
            obj.antenna_positions = antenna_positions

    obj.check(run_check_acceptability=True)

    return obj


# pyuvdata/utils.py

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

"""Commonly used utility functions."""
import re
import copy
import warnings
from collections.abc import Iterable
from copy import deepcopy

import numpy as np
from scipy.spatial.distance import cdist
from astropy.time import Time
from astropy.coordinates import Angle
from astropy.utils import iers
from astropy.coordinates import SkyCoord, Distance, EarthLocation
from astropy import units
import erfa

from . import _utils


__all__ = [
    "POL_STR2NUM_DICT",
    "POL_NUM2STR_DICT",
    "CONJ_POL_DICT",
    "JONES_STR2NUM_DICT",
    "JONES_NUM2STR_DICT",
    "LatLonAlt_from_XYZ",
    "XYZ_from_LatLonAlt",
    "rotECEF_from_ECEF",
    "ECEF_from_rotECEF",
    "ENU_from_ECEF",
    "ECEF_from_ENU",
    "phase_uvw",
    "unphase_uvw",
    "uvcalibrate",
    "apply_uvflag",
    "get_lst_for_time",
    "polstr2num",
    "polnum2str",
    "jstr2num",
    "jnum2str",
    "parse_polstr",
    "parse_jpolstr",
    "conj_pol",
    "reorder_conj_pols",
    "baseline_to_antnums",
    "antnums_to_baseline",
    "baseline_index_flip",
    "get_baseline_redundancies",
    "get_antenna_redundancies",
    "collapse",
    "mean_collapse",
    "absmean_collapse",
    "quadmean_collapse",
    "or_collapse",
    "and_collapse",
]

# fmt: off
# polarization constants
# maps polarization strings to polarization integers
POL_STR2NUM_DICT = {"pI": 1, "pQ": 2, "pU": 3, "pV": 4,
                    "I": 1, "Q": 2, "U": 3, "V": 4,  # support straight stokes names
                    "rr": -1, "ll": -2, "rl": -3, "lr": -4,
                    "xx": -5, "yy": -6, "xy": -7, "yx": -8}
# maps polarization integers to polarization strings
POL_NUM2STR_DICT = {1: "pI", 2: "pQ", 3: "pU", 4: "pV",
                    -1: "rr", -2: "ll", -3: "rl", -4: "lr",
                    -5: "xx", -6: "yy", -7: "xy", -8: "yx"}

# maps how polarizations change when antennas are swapped
CONJ_POL_DICT = {"xx": "xx", "yy": "yy", "xy": "yx", "yx": "xy",
                 "ee": "ee", "nn": "nn", "en": "ne", "ne": "en",
                 "rr": "rr", "ll": "ll", "rl": "lr", "lr": "rl",
                 "I": "I", "Q": "Q", "U": "U", "V": "V",
                 "pI": "pI", "pQ": "pQ", "pU": "pU", "pV": "pV"}

# maps jones matrix element strings to jones integers
# Add entries that don't start with "J" to allow shorthand versions
JONES_STR2NUM_DICT = {"Jxx": -5, "Jyy": -6, "Jxy": -7, "Jyx": -8,
                      "xx": -5, "x": -5, "yy": -6, "y": -6, "xy": -7, "yx": -8,
                      "Jrr": -1, "Jll": -2, "Jrl": -3, "Jlr": -4,
                      "rr": -1, "r": -1, "ll": -2, "l": -2, "rl": -3, "lr": -4}
# maps jones integers to jones matrix element strings
JONES_NUM2STR_DICT = {-1: "Jrr", -2: "Jll", -3: "Jrl", -4: "Jlr",
                      -5: "Jxx", -6: "Jyy", -7: "Jxy", -8: "Jyx"}

# maps uvdata pols to input feed polarizations
POL_TO_FEED_DICT = {"xx": ["x", "x"], "yy": ["y", "y"],
                    "xy": ["x", "y"], "yx": ["y", "x"],
                    "ee": ["e", "e"], "nn": ["n", "n"],
                    "en": ["e", "n"], "ne": ["n", "e"],
                    "rr": ["r", "r"], "ll": ["l", "l"],
                    "rl": ["r", "l"], "lr": ["l", "r"]}

# fmt: on


def _get_iterable(x):
    """Return iterable version of input."""
    if isinstance(x, Iterable):
        return x
    else:
        return (x,)


def _fits_gethduaxis(hdu, axis):
    """
    Make axis arrays for fits files.

    Parameters
    ----------
    hdu : astropy.io.fits HDU object
        The HDU to make an axis array for.
    axis : int
        The axis number of interest (1-based).

    Returns
    -------
    ndarray of float
        Array of values for the specified axis.

    """
    ax = str(axis)
    axis_num = hdu.header["NAXIS" + ax]
    val = hdu.header["CRVAL" + ax]
    delta = hdu.header["CDELT" + ax]
    index = hdu.header["CRPIX" + ax] - 1

    return delta * (np.arange(axis_num) - index) + val


def _fits_indexhdus(hdulist):
    """
    Get a dict of table names and HDU numbers from a FITS HDU list.

    Parameters
    ----------
    hdulist : list of astropy.io.fits HDU objects
        List of HDUs to get names for

    Returns
    -------
    dict
        dictionary with table names as keys and HDU number as values.

    """
    tablenames = {}
    for i in range(len(hdulist)):
        try:
            tablenames[hdulist[i].header["EXTNAME"]] = i
        except (KeyError):
            continue
    return tablenames


def _get_fits_extra_keywords(header, keywords_to_skip=None):
    """
    Get any extra keywords and return as dict.

    Parameters
    ----------
    header : FITS header object
        header object to get extra_keywords from.
    keywords_to_skip : list of str
        list of keywords to not include in extra keywords in addition to standard
        FITS keywords.

    Returns
    -------
    dict
        dict of extra keywords.
    """
    # List standard FITS header items that are still should not be included in
    # extra_keywords
    # These are the beginnings of FITS keywords to ignore, the actual keywords
    # often include integers following these names (e.g. NAXIS1, CTYPE3)
    std_fits_substrings = [
        "HISTORY",
        "SIMPLE",
        "BITPIX",
        "EXTEND",
        "BLOCKED",
        "GROUPS",
        "PCOUNT",
        "BSCALE",
        "BZERO",
        "NAXIS",
        "PTYPE",
        "PSCAL",
        "PZERO",
        "CTYPE",
        "CRVAL",
        "CRPIX",
        "CDELT",
        "CROTA",
        "CUNIT",
    ]

    if keywords_to_skip is not None:
        std_fits_substrings.extend(keywords_to_skip)

    extra_keywords = {}
    # find all the other header items and keep them as extra_keywords
    for key in header:
        # check if key contains any of the standard FITS substrings
        if np.any([sub in key for sub in std_fits_substrings]):
            continue
        if key == "COMMENT":
            extra_keywords[key] = str(header.get(key))
        elif key != "":
            extra_keywords[key] = header.get(key)

    return extra_keywords


def _check_history_version(history, version_string):
    """Check if version_string is present in history string."""
    if version_string.replace(" ", "") in history.replace("\n", "").replace(" ", ""):
        return True
    else:
        return False


def _check_histories(history1, history2):
    """Check if two histories are the same."""
    if history1.replace("\n", "").replace(" ", "") == history2.replace(
        "\n", ""
    ).replace(" ", ""):
        return True
    else:
        return False


def _combine_history_addition(history1, history2):
    """
    Find extra history to add to have minimal repeats.

    Parameters
    ----------
    history1 : str
        First history.
    history2 : str
        Second history

    Returns
    -------
    str
        Extra history to add to first history.

    """
    # first check if they're the same to avoid more complicated processing.
    if _check_histories(history1, history2):
        return None

    hist2_words = history2.split(" ")
    add_hist = ""
    test_hist1 = " " + history1 + " "
    for i, word in enumerate(hist2_words):
        if " " + word + " " not in test_hist1:
            add_hist += " " + word
            keep_going = i + 1 < len(hist2_words)
            while keep_going:
                if (hist2_words[i + 1] == " ") or (
                    " " + hist2_words[i + 1] + " " not in test_hist1
                ):
                    add_hist += " " + hist2_words[i + 1]
                    del hist2_words[i + 1]
                    keep_going = i + 1 < len(hist2_words)
                else:
                    keep_going = False

    if add_hist == "":
        add_hist = None
    return add_hist


def _check_flex_spw_contiguous(spw_array, flex_spw_id_array):
    """
    Check if the spectral windows are contiguous for flex_spw datasets.

    This checks the flex_spw_id_array to make sure that all channels for each
    spectral window are together in one block, versus being interspersed (e.g.,
    channel #1 and #3 is in spw #1, channels #2 and #4 are in spw #2). In theory,
    UVH5 and UVData objects can handle this, but MIRIAD, MIR, UVFITS, and MS file
    formats cannot, so we just consider it forbidden.

    Parameters
    ----------
    spw_array : array of integers
        Array of spectral window numbers, shape (Nspws,).
    flex_spw_id_array : array of integers
        Array of spectral window numbers per frequency channel, shape (Nfreqs,).

    """
    exp_spw_ids = np.unique(spw_array)
    # This is an internal consistency check to make sure that the indexes match
    # up as expected -- this shouldn't error unless someone is mucking with
    # settings they shouldn't be.
    assert np.all(np.unique(flex_spw_id_array) == exp_spw_ids), (
        "There are some entries in flex_spw_id_array that are not in spw_array. "
        "This is a bug, please report it in an issue."
    )

    n_breaks = np.sum(flex_spw_id_array[1:] != flex_spw_id_array[:-1])
    if (n_breaks + 1) != spw_array.size:
        raise ValueError(
            "Channels from different spectral windows are interspersed with "
            "one another, rather than being grouped together along the "
            "frequency axis. Most file formats do not support such "
            "non-grouping of data."
        )
    return True


def _check_freq_spacing(
    freq_array,
    freq_tols,
    channel_width,
    channel_width_tols,
    flex_spw,
    future_array_shapes,
    spw_array,
    flex_spw_id_array,
    raise_errors=True,
):
    """
    Check if frequencies are evenly spaced and separated by their channel width.

    This is a requirement for writing uvfits & miriad files.

    Parameters
    ----------
    freq_array : array of float
        Array of frequencies, shape (1, Nfreqs) or (Nfreqs,) if future_array_shapes=True
    freq_tols : tuple of float
        freq_array tolerances (from uvobj._freq_array.tols).
    channel_width : float or array of float
        Channel widths, either a scalar or an array of shape (Nfreqs,) if flex_spw=True
        and/or future_array_shapes=True.
    channel_width_tols : tuple of float
        channel_width tolerances (from uvobj._channel_width.tols).
    future_array_shapes : bool
        Indicates that parameters have future shapes.
    flex_spw :  bool
        Indicates there are flexible spectral windows.
    spw_array : array of integers or None
        Array of spectral window numbers, shape (Nspws,). Required if flex_spw is True.
    flex_spw_id_array : array of integers or None
        Array of spectral window numbers per frequency channel, shape (Nfreqs,).
        Required if flex_spw is True.
    raise_errors : bool
        Option to raise errors if the various checks do not pass.

    Returns
    -------
    spacing_error : bool
        Flag that channel spacings or channel widths are not equal.
    chanwidth_error : bool
        Flag that channel spacing does not match channel width.

    """
    spacing_error = False
    chanwidth_error = False
    Nfreqs = freq_array.size
    if future_array_shapes:
        freq_spacing = np.diff(freq_array)
        freq_array_use = freq_array
    else:
        freq_spacing = np.diff(freq_array[0])
        freq_array_use = freq_array[0]

    if Nfreqs == 1:
        # Skip all of this if there is only 1 channel
        pass
    elif flex_spw:
        # Check to make sure that the flexible spectral window has indicies set up
        # correctly (grouped together) for this check
        _check_flex_spw_contiguous(spw_array, flex_spw_id_array)
        diff_chanwidth = np.diff(channel_width)
        freq_dir = []
        # We want to grab unique spw IDs, in the order that they appear in the data
        select_mask = np.append((np.diff(flex_spw_id_array) != 0), True)
        for idx in flex_spw_id_array[select_mask]:
            chan_mask = flex_spw_id_array == idx
            diffs = np.diff(freq_array_use[chan_mask])
            if diffs.size > 0:
                freq_dir += [np.sign(np.mean(diffs))] * np.sum(chan_mask)
            else:
                freq_dir += [1.0]

        # Pop off the first entry, since the above arrays are diff'd
        # (and thus one element shorter)
        freq_dir = np.array(freq_dir[1:])
        # Ignore cases where looking at the boundaries of spectral windows
        bypass_check = flex_spw_id_array[1:] != flex_spw_id_array[:-1]
        if not np.all(
            np.logical_or(
                bypass_check,
                np.isclose(diff_chanwidth, 0.0, rtol=freq_tols[0], atol=freq_tols[1]),
            )
        ):
            spacing_error = True
        if not np.all(
            np.logical_or(
                bypass_check,
                np.isclose(
                    freq_spacing,
                    channel_width[1:] * freq_dir,
                    rtol=freq_tols[0],
                    atol=freq_tols[1],
                ),
            )
        ):
            chanwidth_error = True
    else:
        freq_dir = np.sign(np.mean(freq_spacing))
        if not np.isclose(
            np.min(freq_spacing),
            np.max(freq_spacing),
            rtol=freq_tols[0],
            atol=freq_tols[1],
        ):
            spacing_error = True
        if future_array_shapes:
            if not np.isclose(
                np.min(channel_width),
                np.max(channel_width),
                rtol=freq_tols[0],
                atol=freq_tols[1],
            ):
                spacing_error = True
            else:
                if not np.isclose(
                    np.mean(freq_spacing),
                    np.mean(channel_width) * freq_dir,
                    rtol=channel_width_tols[0],
                    atol=channel_width_tols[1],
                ):
                    chanwidth_error = True
        else:
            if not np.isclose(
                np.mean(freq_spacing),
                channel_width * freq_dir,
                rtol=channel_width_tols[0],
                atol=channel_width_tols[1],
            ):
                chanwidth_error = True
    if raise_errors and spacing_error:
        raise ValueError(
            "The frequencies are not evenly spaced (probably "
            "because of a select operation) or has differing "
            "values of channel widths. Some file formats "
            "(e.g. uvfits, miriad) and methods (frequency_average) "
            "do not support unevenly spaced frequencies."
        )
    if raise_errors and chanwidth_error:
        raise ValueError(
            "The frequencies are separated by more than their "
            "channel width (probably because of a select operation). "
            "Some file formats (e.g. uvfits, miriad) and "
            "methods (frequency_average) do not support "
            "frequencies that are spaced by more than their "
            "channel width."
        )

    return spacing_error, chanwidth_error


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

    Parameters
    ----------
    baseline : int or array_like of ints
        baseline number
    Nants_telescope : int
        number of antennas

    Returns
    -------
    int or array_like of int
        first antenna number(s)
    int or array_like of int
        second antenna number(s)

    """
    if Nants_telescope > 2048:
        raise Exception(
            "error Nants={Nants}>2048 not supported".format(Nants=Nants_telescope)
        )

    return_array = isinstance(baseline, (np.ndarray, list, tuple))
    ant1, ant2 = _utils.baseline_to_antnums(
        np.ascontiguousarray(baseline, dtype=np.int64)
    )
    if return_array:
        return ant1, ant2
    else:
        return ant1.item(0), ant2.item(0)


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

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

    Returns
    -------
    int or array of int
        baseline number corresponding to the two antenna numbers.

    """
    if Nants_telescope is not None and Nants_telescope > 2048:
        raise Exception(
            "cannot convert ant1, ant2 to a baseline index "
            "with Nants={Nants}>2048.".format(Nants=Nants_telescope)
        )

    return_array = isinstance(ant1, (np.ndarray, list, tuple))
    baseline = _utils.antnums_to_baseline(
        np.ascontiguousarray(ant1, dtype=np.int64),
        np.ascontiguousarray(ant2, dtype=np.int64),
        attempt256=attempt256,
    )
    if return_array:
        return baseline
    else:
        return baseline.item(0)


def baseline_index_flip(baseline, Nants_telescope):
    """Change baseline number to reverse antenna order."""
    ant1, ant2 = baseline_to_antnums(baseline, Nants_telescope)
    return antnums_to_baseline(ant2, ant1, Nants_telescope)


def _x_orientation_rep_dict(x_orientation):
    """Create replacement dict based on x_orientation."""
    if x_orientation.lower() == "east" or x_orientation.lower() == "e":
        return {"x": "e", "y": "n"}
    elif x_orientation.lower() == "north" or x_orientation.lower() == "n":
        return {"x": "n", "y": "e"}
    else:
        raise ValueError("x_orientation not recognized.")


def polstr2num(pol, x_orientation=None):
    """
    Convert polarization str to number according to AIPS Memo 117.

    Prefer 'pI', 'pQ', 'pU' and 'pV' to make it clear that these are pseudo-Stokes,
    not true Stokes, but also supports 'I', 'Q', 'U', 'V'.

    Parameters
    ----------
    pol : str
        polarization string
    x_orientation : str, optional
        Orientation of the physical dipole corresponding to what is
        labelled as the x polarization ("east" or "north") to allow for
        converting from E/N strings. See corresonding parameter on UVData
        for more details.

    Returns
    -------
    int
        Number corresponding to string

    Raises
    ------
    ValueError
        If the pol string cannot be converted to a polarization number.

    Warns
    -----
    UserWarning
        If the x_orientation not recognized.

    """
    dict_use = copy.deepcopy(POL_STR2NUM_DICT)
    if x_orientation is not None:
        try:
            rep_dict = _x_orientation_rep_dict(x_orientation)
            for key, value in POL_STR2NUM_DICT.items():
                new_key = key.replace("x", rep_dict["x"]).replace("y", rep_dict["y"])
                dict_use[new_key] = value
        except ValueError:
            warnings.warn("x_orientation not recognized.")

    poldict = {k.lower(): v for k, v in dict_use.items()}
    if isinstance(pol, str):
        out = poldict[pol.lower()]
    elif isinstance(pol, Iterable):
        out = [poldict[key.lower()] for key in pol]
    else:
        raise ValueError(
            "Polarization {p} cannot be converted to a polarization number.".format(
                p=pol
            )
        )
    return out


def polnum2str(num, x_orientation=None):
    """
    Convert polarization number to str according to AIPS Memo 117.

    Uses 'pI', 'pQ', 'pU' and 'pV' to make it clear that these are pseudo-Stokes,
    not true Stokes

    Parameters
    ----------
    num : int
        polarization number
    x_orientation : str, optional
        Orientation of the physical dipole corresponding to what is
        labelled as the x polarization ("east" or "north") to convert to
        E/N strings. See corresonding parameter on UVData for more details.

    Returns
    -------
    str
        String corresponding to polarization number

    Raises
    ------
    ValueError
        If the polarization number cannot be converted to a polarization string.

    Warns
    -----
    UserWarning
        If the x_orientation not recognized.

    """
    dict_use = copy.deepcopy(POL_NUM2STR_DICT)
    if x_orientation is not None:
        try:
            rep_dict = _x_orientation_rep_dict(x_orientation)
            for key, value in POL_NUM2STR_DICT.items():
                new_val = value.replace("x", rep_dict["x"]).replace("y", rep_dict["y"])
                dict_use[key] = new_val
        except ValueError:
            warnings.warn("x_orientation not recognized.")

    if isinstance(num, (int, np.int32, np.int64)):
        out = dict_use[num]
    elif isinstance(num, Iterable):
        out = [dict_use[i] for i in num]
    else:
        raise ValueError(
            "Polarization {p} cannot be converted to string.".format(p=num)
        )
    return out


def jstr2num(jstr, x_orientation=None):
    """
    Convert jones polarization str to number according to calfits memo.

    Parameters
    ----------
    jstr : str
        antenna (jones) polarization string
    x_orientation : str, optional
        Orientation of the physical dipole corresponding to what is
        labelled as the x polarization ("east" or "north") to allow for
        converting from E/N strings. See corresonding parameter on UVData
        for more details.

    Returns
    -------
    int
        antenna (jones) polarization number corresponding to string

    Raises
    ------
    ValueError
        If the jones string cannot be converted to a polarization number.

    Warns
    -----
    UserWarning
        If the x_orientation not recognized.

    """
    dict_use = copy.deepcopy(JONES_STR2NUM_DICT)
    if x_orientation is not None:
        try:
            rep_dict = _x_orientation_rep_dict(x_orientation)
            for key, value in JONES_STR2NUM_DICT.items():
                new_key = key.replace("x", rep_dict["x"]).replace("y", rep_dict["y"])
                dict_use[new_key] = value
        except ValueError:
            warnings.warn("x_orientation not recognized.")

    jdict = {k.lower(): v for k, v in dict_use.items()}
    if isinstance(jstr, str):
        out = jdict[jstr.lower()]
    elif isinstance(jstr, Iterable):
        out = [jdict[key.lower()] for key in jstr]
    else:
        raise ValueError(
            "Jones polarization {j} cannot be converted to index.".format(j=jstr)
        )
    return out


def jnum2str(jnum, x_orientation=None):
    """
    Convert jones polarization number to str according to calfits memo.

    Parameters
    ----------
    num : int
        antenna (jones) polarization number
    x_orientation : str, optional
        Orientation of the physical dipole corresponding to what is
        labelled as the x polarization ("east" or "north") to convert to
        E/N strings. See corresonding parameter on UVData for more details.

    Returns
    -------
    str
        antenna (jones) polarization string corresponding to number

    Raises
    ------
    ValueError
        If the jones polarization number cannot be converted to a jones
        polarization string.

    Warns
    -----
    UserWarning
        If the x_orientation not recognized.

    """
    dict_use = copy.deepcopy(JONES_NUM2STR_DICT)
    if x_orientation is not None:
        try:
            rep_dict = _x_orientation_rep_dict(x_orientation)
            for key, value in JONES_NUM2STR_DICT.items():
                new_val = value.replace("x", rep_dict["x"]).replace("y", rep_dict["y"])
                dict_use[key] = new_val
        except ValueError:
            warnings.warn("x_orientation not recognized.")

    if isinstance(jnum, (int, np.int32, np.int64)):
        out = dict_use[jnum]
    elif isinstance(jnum, Iterable):
        out = [dict_use[i] for i in jnum]
    else:
        raise ValueError(
            "Jones polarization {j} cannot be converted to string.".format(j=jnum)
        )
    return out


def parse_polstr(polstr, x_orientation=None):
    """
    Parse a polarization string and return pyuvdata standard polarization string.

    See utils.POL_STR2NUM_DICT for options.

    Parameters
    ----------
    polstr : str
        polarization string
    x_orientation : str, optional
        Orientation of the physical dipole corresponding to what is
        labelled as the x polarization ("east" or "north") to allow for
        converting from E/N strings. See corresonding parameter on UVData
        for more details.

    Returns
    -------
    str
        AIPS Memo 117 standard string

    Raises
    ------
    ValueError
        If the pol string cannot be converted to a polarization number.

    Warns
    -----
    UserWarning
        If the x_orientation not recognized.

    """
    return polnum2str(
        polstr2num(polstr, x_orientation=x_orientation), x_orientation=x_orientation
    )


def parse_jpolstr(jpolstr, x_orientation=None):
    """
    Parse a Jones polarization string and return pyuvdata standard jones string.

    See utils.JONES_STR2NUM_DICT for options.

    Parameters
    ----------
    jpolstr : str
        Jones polarization string

    Returns
    -------
    str
        calfits memo standard string

    Raises
    ------
    ValueError
        If the jones string cannot be converted to a polarization number.

    Warns
    -----
    UserWarning
        If the x_orientation not recognized.

    """
    return jnum2str(
        jstr2num(jpolstr, x_orientation=x_orientation), x_orientation=x_orientation
    )


def conj_pol(pol):
    """
    Return the polarization for the conjugate baseline.

    For example, (1, 2, 'xy') = conj(2, 1, 'yx').
    The returned polarization is determined by assuming the antenna pair is
    reversed in the data, and finding the correct polarization correlation
    which will yield the requested baseline when conjugated. Note this means
    changing the polarization for linear cross-pols, but keeping auto-pol
    (e.g. xx) and Stokes the same.

    Parameters
    ----------
    pol : str or int
        Polarization string or integer.

    Returns
    -------
    cpol : str or int
        Polarization as if antennas are swapped (type matches input)

    """
    cpol_dict = {k.lower(): v for k, v in CONJ_POL_DICT.items()}

    if isinstance(pol, str):
        cpol = cpol_dict[pol.lower()]
    elif isinstance(pol, Iterable):
        cpol = [conj_pol(p) for p in pol]
    elif isinstance(pol, (int, np.int32, np.int64)):
        cpol = polstr2num(cpol_dict[polnum2str(pol).lower()])
    else:
        raise ValueError("Polarization not recognized, cannot be conjugated.")
    return cpol


def reorder_conj_pols(pols):
    """
    Reorder multiple pols, swapping pols that are conjugates of one another.

    For example ('xx', 'xy', 'yx', 'yy') -> ('xx', 'yx', 'xy', 'yy')
    This is useful for the _key2inds function in the case where an antenna
    pair is specified but the conjugate pair exists in the data. The conjugated
    data should be returned in the order of the polarization axis, so after
    conjugating the data, the pols need to be reordered.
    For example, if a file contains antpair (0, 1) and pols 'xy' and 'yx', but
    the user requests antpair (1, 0), they should get:
    [(1x, 0y), (1y, 0x)] = [conj(0y, 1x), conj(0x, 1y)]

    Parameters
    ----------
    pols : array_like of str or int
        Polarization array (strings or ints).

    Returns
    -------
    conj_order : ndarray of int
        Indices to reorder polarization array.
    """
    if not isinstance(pols, Iterable):
        raise ValueError("reorder_conj_pols must be given an array of polarizations.")
    cpols = np.array([conj_pol(p) for p in pols])  # Array needed for np.where
    conj_order = [np.where(cpols == p)[0][0] if p in cpols else -1 for p in pols]
    if -1 in conj_order:
        raise ValueError(
            "Not all conjugate pols exist in the polarization array provided."
        )
    return conj_order


def LatLonAlt_from_XYZ(xyz, check_acceptability=True):
    """
    Calculate lat/lon/alt from ECEF x,y,z.

    Parameters
    ----------
    xyz : ndarray of float
        numpy array, shape (Npts, 3), with ECEF x,y,z coordinates.
    check_acceptability : bool
        Flag to check XYZ coordinates are reasonable.

    Returns
    -------
    latitude :  ndarray or float
        latitude, numpy array (if Npts > 1) or value (if Npts = 1) in radians
    longitude :  ndarray or float
        longitude, numpy array (if Npts > 1) or value (if Npts = 1) in radians
    altitude :  ndarray or float
        altitude, numpy array (if Npts > 1) or value (if Npts = 1) in meters

    """
    # convert to a numpy array
    xyz = np.asarray(xyz)
    if xyz.ndim > 1 and xyz.shape[1] != 3:
        raise ValueError("The expected shape of ECEF xyz array is (Npts, 3).")

    squeeze = xyz.ndim == 1

    if squeeze:
        xyz = xyz[np.newaxis, :]

    xyz = np.ascontiguousarray(xyz.T, dtype=np.float64)

    # checking for acceptable values
    if check_acceptability:
        norms = np.linalg.norm(xyz, axis=0)
        if not all(np.logical_and(norms >= 6.35e6, norms <= 6.39e6)):
            raise ValueError("xyz values should be ECEF x, y, z coordinates in meters")
    # this helper function returns one 2D array because it is less overhead for cython
    lla = _utils._lla_from_xyz(xyz)

    if squeeze:
        return lla[0, 0], lla[1, 0], lla[2, 0]
    return lla[0], lla[1], lla[2]


def XYZ_from_LatLonAlt(latitude, longitude, altitude):
    """
    Calculate ECEF x,y,z from lat/lon/alt values.

    Parameters
    ----------
    latitude :  ndarray or float
        latitude, numpy array (if Npts > 1) or value (if Npts = 1) in radians
    longitude :  ndarray or float
        longitude, numpy array (if Npts > 1) or value (if Npts = 1) in radians
    altitude :  ndarray or float
        altitude, numpy array (if Npts > 1) or value (if Npts = 1) in meters

    Returns
    -------
    xyz : ndarray of float
        numpy array, shape (Npts, 3), with ECEF x,y,z coordinates.

    """
    latitude = np.ascontiguousarray(latitude, dtype=np.float64)
    longitude = np.ascontiguousarray(longitude, dtype=np.float64)
    altitude = np.ascontiguousarray(altitude, dtype=np.float64)

    n_pts = latitude.size

    if longitude.size != n_pts:
        raise ValueError(
            "latitude, longitude and altitude must all have the same length"
        )
    if altitude.size != n_pts:
        raise ValueError(
            "latitude, longitude and altitude must all have the same length"
        )

    xyz = _utils._xyz_from_latlonalt(latitude, longitude, altitude)
    xyz = xyz.T
    if n_pts == 1:
        return xyz[0]

    return xyz


def rotECEF_from_ECEF(xyz, longitude):
    """
    Get rotated ECEF positions such that the x-axis goes through the longitude.

    Miriad and uvfits expect antenna positions in this frame
    (with longitude of the array center/telescope location)

    Parameters
    ----------
    xyz : ndarray of float
        numpy array, shape (Npts, 3), with ECEF x,y,z coordinates.
    longitude : float
        longitude in radians to rotate coordinates to
        (usually the array center/telescope location).

    Returns
    -------
    ndarray of float
        Rotated ECEF coordinates, shape (Npts, 3).

    """
    angle = -1 * longitude
    rot_matrix = np.array(
        [
            [np.cos(angle), -1 * np.sin(angle), 0],
            [np.sin(angle), np.cos(angle), 0],
            [0, 0, 1],
        ]
    )
    return rot_matrix.dot(xyz.T).T


def ECEF_from_rotECEF(xyz, longitude):
    """
    Calculate ECEF from a rotated ECEF (Inverse of rotECEF_from_ECEF).

    Parameters
    ----------
    xyz : ndarray of float
        numpy array, shape (Npts, 3), with rotated ECEF x,y,z coordinates.
    longitude : float
        longitude in radians giving the x direction of the rotated coordinates
        (usually the array center/telescope location).

    Returns
    -------
    ndarray of float
        ECEF coordinates, shape (Npts, 3).

    """
    angle = longitude
    rot_matrix = np.array(
        [
            [np.cos(angle), -1 * np.sin(angle), 0],
            [np.sin(angle), np.cos(angle), 0],
            [0, 0, 1],
        ]
    )
    return rot_matrix.dot(xyz.T).T


def ENU_from_ECEF(xyz, latitude, longitude, altitude):
    """
    Calculate local ENU (east, north, up) coordinates from ECEF coordinates.

    Parameters
    ----------
    xyz : ndarray of float
        numpy array, shape (Npts, 3), with ECEF x,y,z coordinates.
    latitude : float
        Latitude of center of ENU coordinates in radians.
    longitude : float
        Longitude of center of ENU coordinates in radians.
    altitude : float
        Altitude of center of ENU coordinates in radians.

    Returns
    -------
    ndarray of float
        numpy array, shape (Npts, 3), with local ENU coordinates

    """
    xyz = np.asarray(xyz)
    if xyz.ndim > 1 and xyz.shape[1] != 3:
        raise ValueError("The expected shape of ECEF xyz array is (Npts, 3).")

    squeeze = False
    if xyz.ndim == 1:
        squeeze = True
        xyz = xyz[np.newaxis, :]
    xyz = np.ascontiguousarray(xyz.T, dtype=np.float64)

    # check that these are sensible ECEF values -- their magnitudes need to be
    # on the order of Earth's radius
    ecef_magnitudes = np.linalg.norm(xyz, axis=0)
    sensible_radius_range = (6.35e6, 6.39e6)
    if np.any(ecef_magnitudes <= sensible_radius_range[0]) or np.any(
        ecef_magnitudes >= sensible_radius_range[1]
    ):
        raise ValueError(
            "ECEF vector magnitudes must be on the order of the radius of the earth"
        )

    # the cython utility expects (3, Npts) for faster manipulation
    # transpose after we get the array back to match the expected shape
    enu = _utils._ENU_from_ECEF(
        xyz,
        np.ascontiguousarray(latitude, dtype=np.float64),
        np.ascontiguousarray(longitude, dtype=np.float64),
        np.ascontiguousarray(altitude, dtype=np.float64),
    )

    enu = enu.T
    if squeeze:
        enu = np.squeeze(enu)

    return enu


def ECEF_from_ENU(enu, latitude, longitude, altitude):
    """
    Calculate ECEF coordinates from local ENU (east, north, up) coordinates.

    Parameters
    ----------
    enu : ndarray of float
        numpy array, shape (Npts, 3), with local ENU coordinates.
    latitude : float
        Latitude of center of ENU coordinates in radians.
    longitude : float
        Longitude of center of ENU coordinates in radians.
    altitude : float
        Altitude of center of ENU coordinates in radians.


    Returns
    -------
    xyz : ndarray of float
        numpy array, shape (Npts, 3), with ECEF x,y,z coordinates.

    """
    enu = np.asarray(enu)
    if enu.ndim > 1 and enu.shape[1] != 3:
        raise ValueError("The expected shape of the ENU array is (Npts, 3).")
    squeeze = False

    if enu.ndim == 1:
        squeeze = True
        enu = enu[np.newaxis, :]
    enu = np.ascontiguousarray(enu.T, dtype=np.float64)

    # the cython utility expects (3, Npts) for faster manipulation
    # transpose after we get the array back to match the expected shape
    xyz = _utils._ECEF_from_ENU(
        enu,
        np.ascontiguousarray(latitude, dtype=np.float64),
        np.ascontiguousarray(longitude, dtype=np.float64),
        np.ascontiguousarray(altitude, dtype=np.float64),
    )
    xyz = xyz.T
    if squeeze:
        xyz = np.squeeze(xyz)

    return xyz


def phase_uvw(ra, dec, initial_uvw):
    """
    Calculate phased uvws/positions from unphased ones in an icrs or gcrs frame.

    This code expects input uvws or positions relative to the telescope
    location in the same frame that ra/dec are in (e.g. icrs or gcrs) and
    returns phased ones in the same frame.

    Note that this code is nearly identical to ENU_from_ECEF, except that it
    uses an arbitrary phasing center rather than a coordinate center.

    Parameters
    ----------
    ra : float
        Right ascension of phase center.
    dec : float
        Declination of phase center.
    initial_uvw : ndarray of float
        Unphased uvws or positions relative to the array center,
        shape (Nlocs, 3).

    Returns
    -------
    uvw : ndarray of float
        uvw array in the same frame as initial_uvws, ra and dec.

    """
    if initial_uvw.ndim == 1:
        initial_uvw = initial_uvw[np.newaxis, :]

    return _utils._phase_uvw(
        np.float64(ra),
        np.float64(dec),
        np.ascontiguousarray(initial_uvw.T, dtype=np.float64),
    ).T


def unphase_uvw(ra, dec, uvw):
    """
    Calculate unphased uvws/positions from phased ones in an icrs or gcrs frame.

    This code expects phased uvws or positions in the same frame that ra/dec
    are in (e.g. icrs or gcrs) and returns unphased ones in the same frame.

    Parameters
    ----------
    ra : float
        Right ascension of phase center.
    dec : float
        Declination of phase center.
    uvw : ndarray of float
        Phased uvws or positions relative to the array center,
        shape (Nlocs, 3).

    Returns
    -------
    unphased_uvws : ndarray of float
        Unphased uvws or positions relative to the array center,
        shape (Nlocs, 3).

    """
    if uvw.ndim == 1:
        uvw = uvw[np.newaxis, :]

    return _utils._unphase_uvw(
        np.float64(ra), np.float64(dec), np.ascontiguousarray(uvw.T, dtype=np.float64),
    ).T


def polar2_to_cart3(lon_array, lat_array):
    """
    Convert 2D polar coordinates into 3D cartesian coordinates.

    This is a simple routine for converting a set of spherical angular coordinates
    into a 3D cartesian vectors, where the x-direction is set by the position (0, 0).

    Parameters
    ----------
    lon_array : float or ndarray
        Longitude coordinates, which increases in the counter-clockwise direction.
        Units of radians. Can either be a float or ndarray -- if the latter, must have
        the same shape as lat_array.
    lat_array : float or ndarray
        Latitude coordinates, where 0 falls on the equator of the sphere.  Units of
        radians. Can either be a float or ndarray -- if the latter, must have the same
        shape as lat_array.

    Returns
    -------
    xyz_array : ndarray of float
        Cartesian coordinates of the given longitude and latitude on a unit sphere.
        Shape is (3, coord_shape), where coord_shape is the shape of lon_array and
        lat_array if they were provided as type ndarray, otherwise (3,).
    """
    # Check to make sure that we are not playing with mixed types
    if type(lon_array) is not type(lat_array):
        raise ValueError(
            "lon_array and lat_array must either both be floats or ndarrays."
        )
    if isinstance(lon_array, np.ndarray):
        if lon_array.shape != lat_array.shape:
            raise ValueError("lon_array and lat_array must have the same shape.")

    # Once we know that lon_array and lat_array are of the same shape,
    # time to create our 3D set of vectors!
    xyz_array = np.array(
        [
            np.cos(lon_array) * np.cos(lat_array),
            np.sin(lon_array) * np.cos(lat_array),
            np.sin(lat_array),
        ],
        dtype=float,
    )

    return xyz_array


def cart3_to_polar2(xyz_array):
    """
    Convert 3D cartesian coordinates into 2D polar coordinates.

    This is a simple routine for converting a set of 3D cartesian vectors into
    spherical coordinates, where the position (0, 0) lies along the x-direction.

    Parameters
    ----------
    xyz_array : ndarray of float
        Cartesian coordinates, need not be of unit vector length. Shape is
        (3, coord_shape).

    Returns
    -------
    lon_array : ndarray of float
        Longitude coordinates, which increases in the counter-clockwise direction.
        Units of radians, shape is (coord_shape,).
    lat_array : ndarray of float
        Latitude coordinates, where 0 falls on the equator of the sphere.  Units of
        radians, shape is (coord_shape,).
    """
    if not isinstance(xyz_array, np.ndarray):
        raise ValueError("xyz_array must be an ndarray.")
    if xyz_array.ndim == 0:
        raise ValueError("xyz_array must have ndim > 0")
    if xyz_array.shape[0] != 3:
        raise ValueError("xyz_array must be length 3 across the zeroth axis.")

    # The longitude coord is relatively easy to calculate, just take the X and Y
    # components and find the arctac of the pair.
    lon_array = np.mod(np.arctan2(xyz_array[1], xyz_array[0]), 2.0 * np.pi, dtype=float)

    # If we _knew_ that xyz_array was always of length 1, then this call could be a much
    # simpler one to arcsin. But to make this generic, we'll use the length of the XY
    # component along with arctan2.
    lat_array = np.arctan2(
        xyz_array[2], np.sqrt((xyz_array[0:2] ** 2.0).sum(axis=0)), dtype=float
    )

    # Return the two arrays
    return lon_array, lat_array


def _rotate_matmul_wrapper(xyz_array, rot_matrix, n_rot):
    """
    Apply a rotation matrix to a series of vectors.

    This is a simple convenience function which wraps numpy's matmul function for use
    with various vector rotation functions in this module. This code could, in
    principle, be replaced by a cythonized piece of code, although the matmul function
    is _pretty_ well optimized already. This function is not meant to be called by
    users, but is instead used by multiple higher-level utility functions (namely those
    that perform rotations).

    Parameters
    ----------
    xyz_array : ndarray of floats
        Array of vectors to be rotated. When nrot > 1, shape may be (n_rot, 3, n_vec)
        or (1, 3, n_vec), the latter is useful for when performing multiple rotations
        on a fixed set of vectors. If nrot = 1, shape may be (1, 3, n_vec), (3, n_vec),
        or (3,).
    rot_matrix : ndarray of floats
        Series of rotation matricies to be applied to the stack of vectors. Must be
        of shape (n_rot, 3, 3)
    n_rot : int
        Number of individual rotation matricies to be applied.

    Returns
    -------
    rotated_xyz : ndarray of floats
        Array of vectors that have been rotated, of shape (n_rot, 3, n_vectors,).
    """
    # Do a quick check to make sure that things look sensible
    if rot_matrix.shape != (n_rot, 3, 3):
        raise ValueError(
            "rot_matrix must be of shape (n_rot, 3, 3), where n_rot=%i." % n_rot
        )
    if (xyz_array.ndim == 3) and (
        (xyz_array.shape[0] not in [1, n_rot]) or (xyz_array.shape[-2] != 3)
    ):
        raise ValueError("Misshaped xyz_array - expected shape (n_rot, 3, n_vectors).")
    if (xyz_array.ndim < 3) and (xyz_array.shape[0] != 3):
        raise ValueError("Misshaped xyz_array - expected shape (3, n_vectors) or (3,).")
    rotated_xyz = np.matmul(rot_matrix, xyz_array)

    return rotated_xyz


def _rotate_one_axis(xyz_array, rot_amount, rot_axis):
    """
    Rotate an array of 3D positions around the a single axis (x, y, or z).

    This function performs a basic rotation of 3D vectors about one of the priciple
    axes -- the x-axis, the y-axis, or the z-axis.

    Note that the rotations here obey the right-hand rule -- that is to say, from the
    perspective of the positive side of the axis of rotation, a positive rotation will
    cause points on the plane intersecting this axis to move in a counter-clockwise
    fashion.

    Parameters
    ----------
    xyz_array : ndarray of float
        Set of 3-dimensional vectors be rotated, in typical right-handed cartesian
        order, e.g. (x, y, z). Shape is (Nrot, 3, Nvectors).
    rot_amount : float or ndarray of float
        Amount (in radians) to rotate the given set of coordinates. Can either be a
        single float (or ndarray of shape (1,)) if rotating all vectors by the same
        amount, otherwise expected to be shape (Nrot,).
    rot_axis : int
        Axis around which the rotation is applied. 0 is the x-axis, 1 is the y-axis,
        and 2 is the z-axis.

    Returns
    -------
    rotated_xyz : ndarray of float
        Set of rotated 3-dimensional vectors, shape (Nrot, 3, Nvector).
    """
    # If rot_amount is None or all zeros, then this is just one big old no-op.
    if (rot_amount is None) or np.all(rot_amount == 0.0):
        if np.ndim(xyz_array) == 1:
            return deepcopy(xyz_array[np.newaxis, :, np.newaxis])
        elif np.ndim(xyz_array) == 2:
            return deepcopy(xyz_array[np.newaxis, :, :])
        else:
            return deepcopy(xyz_array)

    # Check and see how big of a rotation matrix we need
    n_rot = 1 if (not isinstance(rot_amount, np.ndarray)) else (rot_amount.shape[0])
    n_vec = xyz_array.shape[-1]

    # The promotion of values to float64 is to suppress numerical precision issues,
    # since the matrix math can - in limited circumstances - introduce precision errors
    # of order 10x the limiting numerical precision of the float. For a float32/single,
    # thats a part in 1e6 (~arcsec-level errors), but for a float64 it translates to
    # a part in 1e15.
    rot_matrix = np.zeros((3, 3, n_rot), dtype=np.float64)

    # Figure out which pieces of the matrix we need to update
    temp_jdx = (rot_axis + 1) % 3
    temp_idx = (rot_axis + 2) % 3

    # Fill in the rotation matricies accordingly
    rot_matrix[rot_axis, rot_axis] = 1
    rot_matrix[temp_idx, temp_idx] = np.cos(rot_amount, dtype=np.float64)
    rot_matrix[temp_jdx, temp_jdx] = rot_matrix[temp_idx, temp_idx]
    rot_matrix[temp_idx, temp_jdx] = np.sin(rot_amount, dtype=np.float64)
    rot_matrix[temp_jdx, temp_idx] = -rot_matrix[temp_idx, temp_jdx]

    # The rot matrix was shape (3, 3, n_rot) to help speed up filling in the elements
    # of each matrix, but now we want to flip it into its proper shape of (n_rot, 3, 3)
    rot_matrix = np.transpose(rot_matrix, axes=[2, 0, 1])

    if (n_rot == 1) and (n_vec == 1) and (xyz_array.ndim == 3):
        # This is a special case where we allow the rotation axis to "expand" along
        # the 0th axis of the rot_amount arrays. For xyz_array, if n_vectors = 1
        # but n_rot !=1, then it's a lot faster (by about 10x) to "switch it up" and
        # swap the n_vector and  n_rot axes, and then swap them back once everything
        # else is done.
        return np.transpose(
            _rotate_matmul_wrapper(
                np.transpose(xyz_array, axes=[2, 1, 0]), rot_matrix, n_rot,
            ),
            axes=[2, 1, 0],
        )
    else:
        return _rotate_matmul_wrapper(xyz_array, rot_matrix, n_rot)


def _rotate_two_axis(xyz_array, rot_amount1, rot_amount2, rot_axis1, rot_axis2):
    """
    Rotate an array of 3D positions sequentially around a pair of axes (x, y, or z).

    This function performs a sequential pair of basic rotations of 3D vectors about
    the priciple axes -- the x-axis, the y-axis, or the z-axis.

    Note that the rotations here obey the right-hand rule -- that is to say, from the
    perspective of the positive side of the axis of rotation, a positive rotation will
    cause points on the plane intersecting this axis to move in a counter-clockwise
    fashion.

    Parameters
    ----------
    xyz_array : ndarray of float
        Set of 3-dimensional vectors be rotated, in typical right-handed cartesian
        order, e.g. (x, y, z). Shape is (Nrot, 3, Nvectors).
    rot_amount1 : float or ndarray of float
        Amount (in radians) of rotatation to apply during the first rotation of the
        sequence, to the given set of coordinates. Can either be a single float (or
        ndarray of shape (1,)) if rotating all vectors by the same amount, otherwise
        expected to be shape (Nrot,).
    rot_amount2 : float or ndarray of float
        Amount (in radians) of rotatation to apply during the second rotation of the
        sequence, to the given set of coordinates. Can either be a single float (or
        ndarray of shape (1,)) if rotating all vectors by the same amount, otherwise
        expected to be shape (Nrot,).
    rot_axis1 : int
        Axis around which the first rotation is applied. 0 is the x-axis, 1 is the
        y-axis, and 2 is the z-axis.
    rot_axis2 : int
        Axis around which the second rotation is applied. 0 is the x-axis, 1 is the
        y-axis, and 2 is the z-axis.

    Returns
    -------
    rotated_xyz : ndarray of float
        Set of rotated 3-dimensional vectors, shape (Nrot, 3, Nvector).

    """
    # Capture some special cases upfront, where we can save ourselves a bit of work
    no_rot1 = (rot_amount1 is None) or np.all(rot_amount1 == 0.0)
    no_rot2 = (rot_amount2 is None) or np.all(rot_amount2 == 0.0)
    if no_rot1 and no_rot2:
        # If rot_amount is None, then this is just one big old no-op.
        return deepcopy(xyz_array)
    elif no_rot1:
        # If rot_amount1 is None, then ignore it and just work w/ the 2nd rotation
        return _rotate_one_axis(xyz_array, rot_amount2, rot_axis2)
    elif no_rot2:
        # If rot_amount2 is None, then ignore it and just work w/ the 1st rotation
        return _rotate_one_axis(xyz_array, rot_amount1, rot_axis1)
    elif rot_axis1 == rot_axis2:
        # Capture the case where someone wants to do a sequence of rotations on the same
        # axis. Also known as just rotating a single axis.
        return _rotate_one_axis(xyz_array, rot_amount1 + rot_amount2, rot_axis1)

    # Figure out how many individual rotation matricies we need, accounting for the
    # fact that these can either be floats or ndarrays.
    n_rot = max(
        rot_amount1.shape[0] if isinstance(rot_amount1, np.ndarray) else 1,
        rot_amount2.shape[0] if isinstance(rot_amount2, np.ndarray) else 1,
    )
    n_vec = xyz_array.shape[-1]

    # The promotion of values to float64 is to suppress numerical precision issues,
    # since the matrix math can - in limited circumstances - introduce precision errors
    # of order 10x the limiting numerical precision of the float. For a float32/single,
    # thats a part in 1e6 (~arcsec-level errors), but for a float64 it translates to
    # a part in 1e15.
    rot_matrix = np.empty((3, 3, n_rot), dtype=np.float64)

    # There are two permulations per pair of axes -- when the pair is right-hand
    # oriented vs left-hand oriented. Check here which one it is. For example,
    # rotating first on the x-axis, second on the y-axis is considered a
    # "right-handed" pair, whereas z-axis first, then y-axis would be considered
    # a "left-handed" pair.
    lhd_order = np.mod(rot_axis2 - rot_axis1, 3) != 1

    temp_idx = [
        np.mod(rot_axis1 - lhd_order, 3),
        np.mod(rot_axis1 + 1 - lhd_order, 3),
        np.mod(rot_axis1 + 2 - lhd_order, 3),
    ]

    # We're using lots of sin and cos calculations -- doing them once upfront saves
    # quite a bit of time by eliminating redundant calculations
    sin_lo = np.sin(rot_amount2 if lhd_order else rot_amount1, dtype=np.float64)
    cos_lo = np.cos(rot_amount2 if lhd_order else rot_amount1, dtype=np.float64)
    sin_hi = np.sin(rot_amount1 if lhd_order else rot_amount2, dtype=np.float64)
    cos_hi = np.cos(rot_amount1 if lhd_order else rot_amount2, dtype=np.float64)

    # Take care of the diagonal terms first, since they aren't actually affected by the
    # order of rotational opertations
    rot_matrix[temp_idx[0], temp_idx[0]] = cos_hi
    rot_matrix[temp_idx[1], temp_idx[1]] = cos_lo
    rot_matrix[temp_idx[2], temp_idx[2]] = cos_lo * cos_hi

    # Now time for the off-diagonal terms, as a set of 3 pairs. The rotation matrix
    # for a left-hand oriented pair of rotation axes (e.g., x-rot, then y-rot) is just
    # a transpose of the right-hand orientation of the same pair (e.g., y-rot, then
    # x-rot).
    rot_matrix[temp_idx[0 + lhd_order], temp_idx[1 - lhd_order]] = sin_lo * sin_hi
    rot_matrix[temp_idx[0 - lhd_order], temp_idx[lhd_order - 1]] = (
        cos_lo * sin_hi * ((-1.0) ** lhd_order)
    )

    rot_matrix[temp_idx[1 - lhd_order], temp_idx[0 + lhd_order]] = 0.0
    rot_matrix[temp_idx[1 + lhd_order], temp_idx[2 - lhd_order]] = sin_lo * (
        (-1.0) ** (1 + lhd_order)
    )

    rot_matrix[temp_idx[lhd_order - 1], temp_idx[0 - lhd_order]] = sin_hi * (
        (-1.0) ** (1 + lhd_order)
    )
    rot_matrix[temp_idx[2 - lhd_order], temp_idx[1 + lhd_order]] = (
        sin_lo * cos_hi * ((-1.0) ** (lhd_order))
    )

    # The rot matrix was shape (3, 3, n_rot) to help speed up filling in the elements
    # of each matrix, but now we want to flip it into its proper shape of (n_rot, 3, 3)
    rot_matrix = np.transpose(rot_matrix, axes=[2, 0, 1])

    if (n_rot == 1) and (n_vec == 1) and (xyz_array.ndim == 3):
        # This is a special case where we allow the rotation axis to "expand" along
        # the 0th axis of the rot_amount arrays. For xyz_array, if n_vectors = 1
        # but n_rot !=1, then it's a lot faster (by about 10x) to "switch it up" and
        # swap the n_vector and  n_rot axes, and then swap them back once everything
        # else is done.
        return np.transpose(
            _rotate_matmul_wrapper(
                np.transpose(xyz_array, axes=[2, 1, 0]), rot_matrix, n_rot,
            ),
            axes=[2, 1, 0],
        )
    else:
        return _rotate_matmul_wrapper(xyz_array, rot_matrix, n_rot)


def calc_uvw(
    app_ra=None,
    app_dec=None,
    frame_pa=None,
    lst_array=None,
    use_ant_pos=True,
    uvw_array=None,
    antenna_positions=None,
    antenna_numbers=None,
    ant_1_array=None,
    ant_2_array=None,
    old_app_ra=None,
    old_app_dec=None,
    old_frame_pa=None,
    telescope_lat=None,
    telescope_lon=None,
    from_enu=False,
    to_enu=False,
):
    """
    Calculate an array of baseline coordinates, in either uvw or ENU.

    This routine is meant as a convenience function for producing baseline coordinates
    based under a few different circumstances:

    1) Calculating ENU coordinates using antenna positions
    2) Calculating uwv coordinates at a given sky position using antenna positions
    3) Converting from ENU coordinates to uvw coordinates
    4) Converting from uvw coordinate to ENU coordinates
    5) Converting from uvw coordinates at one sky position to another sky position

    Different conversion pathways have different parameters that are required.

    Parameters
    ----------
    app_ra : ndarray of float
        Apparent RA of the target phase center, required if calculating baseline
        coordinates in uvw-space (vs ENU-space). Shape is (Nblts,), units are
        radians.
    app_dec : ndarray of float
        Apparent declination of the target phase center, required if calculating
        baseline coordinates in uvw-space (vs ENU-space). Shape is (Nblts,),
        units are radians.
    frame_pa : ndarray of float
        Position angle between the great circle of declination in the apparent frame
        versus that of the reference frame, used for making sure that "North" on
        the derived maps points towards a particular celestial pole (not just the
        topocentric one). Required if not deriving baseline coordinates from antenna
        positions, from_enu=False, and a value for old_frame_pa is given. Shape is
        (Nblts,), units are radians.
    old_app_ra : ndarray of float
        Apparent RA of the previous phase center, required if not deriving baseline
        coordinates from antenna positions and from_enu=False. Shape is (Nblts,),
        units are radians.
    old_app_dec : ndarray of float
        Apparent declination of the previous phase center, required if not deriving
        baseline coordinates from antenna positions and from_enu=False. Shape is
        (Nblts,), units are radians.
    old_frame_pa : ndarray of float
        Frame position angle of the previous phase center, required if not deriving
        baseline coordinates from antenna positions, from_enu=False, and a value
        for frame_pa is supplied. Shape is (Nblts,), units are radians.
    lst_array : ndarray of float
        Local apparent sidereal time, required if deriving baseline coordinates from
        antenna positions, or converting to/from ENU coordinates. Shape is (Nblts,).
    use_ant_pos : bool
        Switch to determine whether to derive uvw values from the antenna positions
        (if set to True), or to use the previously calculated uvw coordinates to derive
        new the new baseline vectors (if set to False). Default is True.
    uvw_array : ndarray of float
        Array of previous baseline coordinates (in either uvw or ENU), required if
        not deriving new coordinates from antenna positions.  Shape is (Nblts, 3).
    antenna_positions : ndarray of float
        List of antenna positions relative to array center in ECEF coordinates,
        required if not providing `uvw_array`. Shape is (Nants, 3).
    antenna_numbers: ndarray of int
        List of antenna numbers, ordered in the same way as `antenna_positions` (e.g.,
        `antenna_numbers[0]` should given the number of antenna that resides at ECEF
        position given by `antenna_positions[0]`). Shape is (Nants,), requred if not
        providing `uvw_array`. Contains all unique entires of the joint set of
        `ant_1_array` and `ant_2_array`.
    ant_1_array : ndarray of int
        Antenna number of the first antenna in the baseline pair, for all baselines
        Required if not providing `uvw_array`, shape is (Nblts,).
    ant_2_array : ndarray of int
        Antenna number of the second antenna in the baseline pair, for all baselines
        Required if not providing `uvw_array`, shape is (Nblts,).
    telescope_lat : float
        Latitude of the phase center, units radians, required if deriving baseline
        coordinates from antenna positions, or converting to/from ENU coordinates.
    telescope_lon : float
        Longitude of the phase center, units radians, required if deriving baseline
        coordinates from antenna positions, or converting to/from ENU coordinates.
    from_enu : boolean
        Set to True if uvw_array is expressed in ENU coordinates. Default is False.
    to_enu : boolean
        Set to True if you would like the output expressed in EN coordinates. Default
        is False.

    Returns
    -------
    new_coords : ndarray of float64
        Set of baseline coordinates, shape (Nblts, 3).
    """
    if to_enu:
        if lst_array is None and not use_ant_pos:
            raise ValueError(
                "Must include lst_array to calculate baselines in ENU coordinates!"
            )
        if telescope_lat is None:
            raise ValueError(
                "Must include telescope_lat to calculate baselines "
                "in ENU coordinates!"
            )
    else:
        if ((app_ra is None) or (app_dec is None)) and frame_pa is None:
            raise ValueError(
                "Must include both app_ra and app_dec, or frame_pa to calculate "
                "baselines in uvw coordinates!"
            )

    if use_ant_pos:
        # Assume at this point we are dealing w/ antenna positions
        if antenna_positions is None:
            raise ValueError("Must include antenna_positions if use_ant_pos=True.")
        if (ant_1_array is None) or (ant_2_array is None) or (antenna_numbers is None):
            raise ValueError(
                "Must include ant_1_array, ant_2_array, and antenna_numbers "
                "setting use_ant_pos=True."
            )
        if lst_array is None and not to_enu:
            raise ValueError(
                "Must include lst_array if use_ant_pos=True and not calculating "
                "baselines in ENU coordinates."
            )
        if telescope_lon is None:
            raise ValueError("Must include telescope_lon if use_ant_pos=True.")

        ant_dict = {ant_num: idx for idx, ant_num in enumerate(antenna_numbers)}
        ant_1_index = np.array([ant_dict[idx] for idx in ant_1_array], dtype=int)
        ant_2_index = np.array([ant_dict[idx] for idx in ant_2_array], dtype=int)

        N_ants = antenna_positions.shape[0]
        # Use the app_ra, app_dec, and lst_array arrays to figure out how many unique
        # rotations are actually needed. If the ratio of Nblts to number of unique
        # entries is favorable, we can just rotate the antenna positions and save
        # outselves a bit of work.
        if to_enu:
            # If to_enu, skip all this -- there's only one unique ha + dec combo
            unique_mask = np.zeros(len(ant_1_index), dtype=np.bool_)
            unique_mask[0] = True
        else:
            unique_mask = np.append(
                True,
                (
                    ((lst_array[:-1] - app_ra[:-1]) != (lst_array[1:] - app_ra[1:]))
                    | (app_dec[:-1] != app_dec[1:])
                ),
            )

        # GHA -> Hour Angle as measured at Greenwich (because antenna coords are
        # centered such that x-plane intersects the meridian at longitude 0).
        if to_enu:
            # Unphased coordinates appear to be stored in ENU coordinates -- that's
            # equivalent to calculating uvw's based on zenith. We can use that to our
            # advantage and spoof the gha and dec based on telescope lon and lat
            unique_gha = np.zeros(1) - telescope_lon
            unique_dec = np.zeros(1) + telescope_lat
            unique_pa = None
        else:
            unique_gha = (lst_array[unique_mask] - app_ra[unique_mask]) - telescope_lon
            unique_dec = app_dec[unique_mask]
            unique_pa = 0.0 if frame_pa is None else frame_pa[unique_mask]

        # Tranpose the ant vectors so that they are in the proper shape
        ant_vectors = np.transpose(antenna_positions)[np.newaxis, :, :]
        # Apply rotations, and then reorganize the ndarray so that you can access
        # individual antenna vectors quickly.
        ant_rot_vectors = np.reshape(
            np.transpose(
                _rotate_one_axis(
                    _rotate_two_axis(ant_vectors, unique_gha, unique_dec, 2, 1),
                    unique_pa,
                    0,
                ),
                axes=[0, 2, 1],
            ),
            (-1, 3),
        )

        unique_mask[0] = False
        unique_map = np.cumsum(unique_mask) * N_ants
        new_coords = (
            ant_rot_vectors[unique_map + ant_2_index]
            - ant_rot_vectors[unique_map + ant_1_index]
        )
    else:
        if uvw_array is None:
            raise ValueError("Must include uvw_array if use_ant_pos=False.")
        if from_enu:
            if to_enu:
                # Well this was pointless... returning your uvws unharmed
                return uvw_array
            # Unphased coordinates appear to be stored in ENU coordinates -- that's
            # equivalent to calculating uvw's based on zenith. We can use that to our
            # advantage and spoof old_app_ra and old_app_dec based on lst_array and
            # telescope_lat
            if telescope_lat is None:
                raise ValueError(
                    "Must include telescope_lat if moving between "
                    'ENU (i.e., "unphased") and uvw coordinates!'
                )
            if lst_array is None:
                raise ValueError(
                    'Must include lst_array if moving between ENU (i.e., "unphased") '
                    "and uvw coordinates!"
                )
        else:
            if (old_frame_pa is None) and not (frame_pa is None or to_enu):
                raise ValueError(
                    "Must include old_frame_pa values if data are phased and "
                    "applying new position angle values (frame_pa)."
                )
            if ((old_app_ra is None) and not (app_ra is None or to_enu)) or (
                (old_app_dec is None) and not (app_dec is None or to_enu)
            ):
                raise ValueError(
                    "Must include old_app_ra and old_app_dec values when data are "
                    "already phased and phasing to a new position."
                )
        # For this operation, all we need is the delta-ha coverage, which _should_ be
        # entirely encapsulated by the change in RA.
        if (app_ra is None) and (old_app_ra is None):
            gha_delta_array = 0.0
        else:
            gha_delta_array = (lst_array if from_enu else old_app_ra) - (
                lst_array if to_enu else app_ra
            )

        # Notice below there's an axis re-orientation here, to go from uvw -> XYZ,
        # where X is pointing in the direction of the source. This is mostly here
        # for convenience and code legibility -- a slightly different pair of
        # rotations would give you the same result w/o needing to cycle the axes.

        # Up front, we want to trap the corner-case where the sky position you are
        # phasing up to hasn't changed, just the position angle (i.e., which way is
        # up on the map). This is a much easier transform to handle.
        if np.all(gha_delta_array == 0.0) and np.all(old_app_dec == app_dec):
            new_coords = _rotate_one_axis(
                uvw_array[:, [2, 0, 1], np.newaxis],
                frame_pa - (0.0 if old_frame_pa is None else old_frame_pa),
                0,
            )[:, :, 0]
        else:
            new_coords = _rotate_two_axis(
                _rotate_two_axis(  # Yo dawg, I heard you like rotation maticies...
                    uvw_array[:, [2, 0, 1], np.newaxis],
                    0.0 if (from_enu or old_frame_pa is None) else (-old_frame_pa),
                    (-telescope_lat) if from_enu else (-old_app_dec),
                    0,
                    1,
                ),
                gha_delta_array,
                telescope_lat if to_enu else app_dec,
                2,
                1,
            )

            # One final rotation applied here, to compensate for the fact that we want
            # the Dec-axis of our image (Fourier dual to the v-axis) to be aligned with
            # the chosen frame, if we not in ENU coordinates
            if not to_enu:
                new_coords = _rotate_one_axis(new_coords, frame_pa, 0)

            # Finally drop the now-vestigal last axis of the array
            new_coords = new_coords[:, :, 0]

    # There's one last task to do, which is to re-align the axes from projected
    # XYZ -> uvw, where X (which points towards the source) falls on the w axis,
    # and Y and Z fall on the u and v axes, respectively.
    return new_coords[:, [1, 2, 0]]


def transform_sidereal_coords(
    lon,
    lat,
    in_coord_frame,
    out_coord_frame,
    in_coord_epoch=None,
    out_coord_epoch=None,
    time_array=None,
):
    """
    Transform a given set of coordinates from one sidereal coordinate frame to another.

    Uses astropy to convert from a coordinates from sidereal frame into another.
    This function will support transforms from several frames, including GCRS,
    FK5 (i.e., J2000), FK4 (i.e., B1950), Galactic, Supergalactic, CIRS, HCRS, and
    a few others (basically anything that doesn't require knowing the observers
    location on Earth/other celestial body).

    Parameters
    ----------
    lon_coord : float or ndarray of floats
        Logitudinal coordinate to be transformed, typically expressed as the right
        ascension, in units of radians. Can either be a float, or an ndarray of
        floats with shape (Ncoords,). Must agree with lat_coord.
    lat_coord : float or ndarray of floats
        Latitudinal coordinate to be transformed, typically expressed as the
        declination, in units of radians. Can either be a float, or an ndarray of
        floats with shape (Ncoords,). Must agree with lon_coord.
    in_coord_frame : string
        Reference frame for the provided coordinates.  Expected to match a list of
        those supported within the astropy SkyCoord object. An incomplete list includes
        'gcrs', 'fk4', 'fk5', 'galactic', 'supergalactic', 'cirs', and 'hcrs'.
    out_coord_frame : string
        Reference frame to output coordinates in. Expected to match a list of
        those supported within the astropy SkyCoord object. An incomplete list includes
        'gcrs', 'fk4', 'fk5', 'galactic', 'supergalactic', 'cirs', and 'hcrs'.
    in_coord_epoch : float
        Epoch for the input coordinate frame. Optional parameter, only required
        when using either the FK4 (B1950) or FK5 (J2000) coordinate systems. Units are
        in fractional years.
    out_coord_epoch : float
        Epoch for the output coordinate frame. Optional parameter, only required
        when using either the FK4 (B1950) or FK5 (J2000) coordinate systems. Units are
        in fractional years.
    time_array : float or ndarray of floats
        Julian date(s) to which the coordinates correspond to, only used in frames
        with annular motion terms (e.g., abberation in GCRS). Can either be a float,
        or an ndarray of floats with shape (Ntimes,), assuming that either lat_coord
        and lon_coord are floats, or that Ntimes == Ncoords.

    Returns
    -------
    new_lat : float or ndarray of floats
        Longitudinal coordinates, in units of radians. Output will be an ndarray
        if any inputs were, with shape (Ncoords,) or (Ntimes,), depending on inputs.
    new_lon : float or ndarray of floats
        Latidudinal coordinates, in units of radians. Output will be an ndarray
        if any inputs were, with shape (Ncoords,) or (Ntimes,), depending on inputs.
    """
    lon_coord = lon * units.rad
    lat_coord = lat * units.rad

    # Check here to make sure that lat_coord and lon_coord are the same length,
    # either 1 or len(time_array)
    if lat_coord.shape != lon_coord.shape:
        raise ValueError("lon and lat must be the same shape.")

    if lon_coord.ndim == 0:
        lon_coord.shape += (1,)
        lat_coord.shape += (1,)

    # Check to make sure that we have a properly formatted epoch for our in-bound
    # coordinate frame
    in_epoch = None
    if isinstance(in_coord_epoch, str) or isinstance(in_coord_epoch, Time):
        # If its a string or a Time object, we don't need to do anything more
        in_epoch = Time(in_coord_epoch)
    elif in_coord_epoch is not None:
        if in_coord_frame.lower() in ["fk4", "fk4noeterms"]:
            in_epoch = Time(in_coord_epoch, format="byear")
        else:
            in_epoch = Time(in_coord_epoch, format="jyear")

    # Now do the same for the outbound frame
    out_epoch = None
    if isinstance(out_coord_epoch, str) or isinstance(out_coord_epoch, Time):
        # If its a string or a Time object, we don't need to do anything more
        out_epoch = Time(out_coord_epoch)
    elif out_coord_epoch is not None:
        if out_coord_frame.lower() in ["fk4", "fk4noeterms"]:
            out_epoch = Time(out_coord_epoch, format="byear")
        else:
            out_epoch = Time(out_coord_epoch, format="jyear")

    # Make sure that time array matched up with what we expect. Thanks to astropy
    # weirdness, time_array has to be the same length as lat/lon coords
    rep_time = False
    rep_crds = False
    if time_array is None:
        time_obj_array = None
    else:
        if isinstance(time_array, Time):
            time_obj_array = time_array
        else:
            time_obj_array = Time(time_array, format="jd", scale="utc")
        if (time_obj_array.size != 1) and (lon_coord.size != 1):
            if time_obj_array.shape != lon_coord.shape:
                raise ValueError(
                    "Shape of time_array must be either that of "
                    " lat_coord/lon_coord if len(time_array) > 1."
                )
        else:
            rep_crds = (time_obj_array.size != 1) and (lon_coord.size == 1)
            rep_time = (time_obj_array.size == 1) and (lon_coord.size != 1)
    if rep_crds:
        lon_coord = np.repeat(lon_coord, len(time_array))
        lat_coord = np.repeat(lat_coord, len(time_array))
    if rep_time:
        time_obj_array = Time(
            np.repeat(time_obj_array.jd, len(lon_coord)), format="jd", scale="utc",
        )
    coord_object = SkyCoord(
        lon_coord,
        lat_coord,
        frame=in_coord_frame,
        equinox=in_epoch,
        obstime=time_obj_array,
    )

    # Easiest, most general way to transform to the new frame is to create a dummy
    # SkyCoord with all the attributes needed -- note that we particularly need this
    # in order to use a non-standard equinox/epoch
    new_coord = coord_object.transform_to(
        SkyCoord(0, 0, unit="rad", frame=out_coord_frame, equinox=out_epoch)
    )

    return new_coord.spherical.lon.rad, new_coord.spherical.lat.rad


def transform_icrs_to_app(
    time_array,
    ra,
    dec,
    telescope_loc,
    epoch=2000.0,
    pm_ra=None,
    pm_dec=None,
    vrad=None,
    dist=None,
    astrometry_library="erfa",
):
    """
    Transform a set of coordinates in ICRS to topocentric/apparent coordinates.

    This utility uses one of three libraries (astropy, NOVAS, or ERFA) to calculate
    the apparent (i.e., topocentric) coordinates of a source at a given time and
    location, given a set of coordinates expressed in the ICRS frame. These coordinates
    are most typically used for defining the phase center of the array (i.e, calculating
    baseline vectors).

    As of astropy v4.2, the agreement between the three libraries is consistent down to
    the level of better than 1 mas, with the values produced by astropy and pyERFA
    consistent to bettter than 10 µas (this is not surprising, given that astropy uses
    pyERFA under the hood for astrometry). ERFA is the default as it outputs
    coordinates natively in the apparent frame (whereas NOVAS and astropy do not), as
    well as the fact that of the three libraries, it produces results the fastest.

    Parameters
    ----------
    time_array : float or array-like of float
        Julian dates to calculate coordinate positions for. Can either be a single
        float, or an array-like of shape (Ntimes,).
    ra : float or array-like of float
        ICRS RA of the celestial target, expressed in units of radians. Can either
        be a single float or array of shape (Ntimes,), although this must be consistent
        with other parameters (with the exception of telescope location parameters).
    dec : float or array-like of float
        ICRS Dec of the celestial target, expressed in units of radians. Can either
        be a single float or array of shape (Ntimes,), although this must be consistent
        with other parameters (with the exception of telescope location parameters).
    telescope_loc : array-like of floats or EarthLocation
        ITRF latitude, longitude, and altitude (rel to sea-level) of the phase center
        of the array. Can either be provided as an astropy EarthLocation, or a tuple
        of shape (3,) containung (in order) the latitude, longitude, and altitude,
        in units of radians, radians, and meters, respectively.
    epoch : int or float or str or Time object
        Epoch of the coordinate data supplied, only used when supplying proper motion
        values. If supplying a number, it will assumed to be in Julian years. Default
        is J2000.0.
    pm_ra : float or array-like of float
        Proper motion in RA of the source, expressed in units of milliarcsec / year.
        Proper motion values are applied relative to the J2000 (i.e., RA/Dec ICRS
        values should be set to their expected values when the epoch is 2000.0).
        Can either be a single float or array of shape (Ntimes,), although this must
        be consistent with other parameters (namely ra_coord and dec_coord). Note that
        units are in dRA/dt, not cos(Dec)*dRA/dt. Not required.
    pm_dec : float or array-like of float
        Proper motion in Dec of the source, expressed in units of milliarcsec / year.
        Proper motion values are applied relative to the J2000 (i.e., RA/Dec ICRS
        values should be set to their expected values when the epoch is 2000.0).
        Can either be a single float or array of shape (Ntimes,), although this must
        be consistent with other parameters (namely ra_coord and dec_coord). Not
        required.
    vrad : float or array-like of float
        Radial velocity of the source, expressed in units of km / sec. Can either be
        a single float or array of shape (Ntimes,), although this must be consistent
        with other parameters (namely ra_coord and dec_coord). Not required.
    dist : float or array-like of float
        Distance of the source, expressed in milliarcseconds. Can either be a single
        float or array of shape (Ntimes,), although this must be consistent with other
        parameters (namely ra_coord and dec_coord). Not required.
    astrometry_library : str
        Library used for running the coordinate conversions. Allowed options are
        'erfa' (which uses the pyERFA), 'novas' (which uses the python-novas library),
        and 'astropy' (which uses the astropy utilities). Default is erfa.

    Returns
    -------
    app_ra : ndarray of floats
        Apparent right ascension coordinates, in units of radians, of shape (Ntimes,).
    app_dec : ndarray of floats
        Apparent declination coordinates, in units of radians, of shape (Ntimes,).
    """
    # Make sure that the library requested is actually permitted
    if astrometry_library not in ["erfa", "novas", "astropy"]:
        raise ValueError(
            "Requested coordinate transformation library is not supported, please "
            "select either 'erfa', 'novas', or 'astropy' for astrometry_library."
        )

    ra_coord = ra * units.rad
    dec_coord = dec * units.rad

    # Check here to make sure that ra_coord and dec_coord are the same length,
    # either 1 or len(time_array)
    multi_coord = ra_coord.size != 1
    if ra_coord.shape != dec_coord.shape:
        raise ValueError("ra and dec must be the same shape.")

    pm_ra_coord = None if pm_ra is None else pm_ra * (units.mas / units.yr)
    pm_dec_coord = None if pm_dec is None else pm_dec * (units.mas / units.yr)
    d_coord = (
        None if (dist is None or np.all(dist == 0.0)) else Distance(dist * units.pc)
    )
    v_coord = None if vrad is None else vrad * (units.km / units.s)

    opt_list = [pm_ra_coord, pm_dec_coord, d_coord, v_coord]
    opt_names = ["pm_ra", "pm_dec", "dist", "vrad"]
    # Check the optional inputs, make sure that they're sensible
    for item, name in zip(opt_list, opt_names):
        if item is not None:
            if ra_coord.shape != item.shape:
                raise ValueError("%s must be the same shape as ra and dec." % name)

    if isinstance(telescope_loc, EarthLocation):
        site_loc = telescope_loc
    else:
        site_loc = EarthLocation.from_geodetic(
            telescope_loc[1] * (180.0 / np.pi),
            telescope_loc[0] * (180.0 / np.pi),
            height=telescope_loc[2],
        )

    # Useful for both astropy and novas methods, the latter of which gives easy
    # access to the IERS data that we want.
    if isinstance(time_array, Time):
        time_obj_array = time_array
    else:
        time_obj_array = Time(time_array, format="jd", scale="utc")

    if time_obj_array.size != 1:
        if (time_obj_array.shape != ra_coord.shape) and multi_coord:
            raise ValueError(
                "time_array must be of either of length 1 (single "
                "float) or same length as ra and dec."
            )
    elif time_obj_array.ndim == 0:
        # Make the array at least 1-dimensional so we don't run into indexing
        # issues later.
        time_obj_array = Time([time_obj_array])

    # Check to make sure that we have a properly formatted epoch for our in-bound
    # coordinate frame
    coord_epoch = None
    if isinstance(epoch, str) or isinstance(epoch, Time):
        # If its a string or a Time object, we don't need to do anything more
        coord_epoch = Time(epoch)
    elif epoch is not None:
        coord_epoch = Time(epoch, format="jyear")

    # Note if time_array is a single element
    multi_time = time_obj_array.size != 1

    # Get IERS data, which is needed for NOVAS and ERFA
    polar_motion_data = iers.earth_orientation_table.get()

    pm_x_array, pm_y_array = polar_motion_data.pm_xy(time_obj_array)
    delta_x_array, delta_y_array = polar_motion_data.dcip_xy(time_obj_array)

    pm_x_array = pm_x_array.to_value("arcsec")
    pm_y_array = pm_y_array.to_value("arcsec")
    delta_x_array = delta_x_array.to_value("marcsec")
    delta_y_array = delta_y_array.to_value("marcsec")
    # Catch the case where we don't have CIP delta values yet (they don't typically have
    # predictive values like the polar motion does)
    delta_x_array[np.isnan(delta_x_array)] = 0.0
    delta_y_array[np.isnan(delta_y_array)] = 0.0

    # If the source was instantiated w/ floats, it'll be a 0-dim object, which will
    # throw errors if we try to treat it as an array. Reshape to a 1D array of len 1
    # so that all the calls can be uniform
    if ra_coord.ndim == 0:
        ra_coord.shape += (1,)
        dec_coord.shape += (1,)
        if pm_ra_coord is not None:
            pm_ra
        if d_coord is not None:
            d_coord.shape += (1,)
        if v_coord is not None:
            v_coord.shape += (1,)

    # If there is an epoch and a proper motion, apply that motion now

    if astrometry_library == "astropy":
        # Astropy doesn't have (oddly enough) a way of getting at the apparent RA/Dec
        # directly, but we can cheat this by going to AltAz, and then coverting back
        # to apparent RA/Dec using the telescope lat and LAST.
        if (epoch is not None) and (pm_ra is not None) and (pm_dec is not None):
            # astropy is a bit weird in how it handles proper motion, so rather than
            # fight with it to do it all in one step, we separate it into two: first
            # apply proper motion to ICRS, then transform to topocentric.
            sky_coord = SkyCoord(
                ra=ra_coord,
                dec=dec_coord,
                pm_ra_cosdec=pm_ra_coord * np.cos(dec_coord),
                pm_dec=pm_dec_coord,
                frame="icrs",
            )

            sky_coord = sky_coord.apply_space_motion(dt=(time_obj_array - coord_epoch))
            ra_coord = sky_coord.ra
            dec_coord = sky_coord.dec
            if d_coord is not None:
                d_coord = d_coord.repeat(ra_coord.size)
            if v_coord is not None:
                v_coord = v_coord.repeat(ra_coord.size)

        sky_coord = SkyCoord(
            ra=ra_coord,
            dec=dec_coord,
            distance=d_coord,
            radial_velocity=v_coord,
            frame="icrs",
        )

        azel_data = sky_coord.transform_to(
            SkyCoord(
                np.zeros_like(time_obj_array) * units.rad,
                np.zeros_like(time_obj_array) * units.rad,
                location=site_loc,
                obstime=time_obj_array,
                frame="altaz",
            )
        )

        app_ha, app_dec = erfa.ae2hd(
            azel_data.az.rad, azel_data.alt.rad, site_loc.lat.rad,
        )
        app_ra = np.mod(
            time_obj_array.sidereal_time("apparent", longitude=site_loc.lon).rad
            - app_ha,
            2 * np.pi,
        )

    elif astrometry_library == "novas":
        # Import the NOVAS library only if it's needed/available.
        try:
            from novas import compat as novas
            from novas.compat import eph_manager
            import novas_de405  # noqa
        except ImportError as e:  # pragma: no cover
            raise ImportError(
                "novas and/or novas_de405 are not installed but is required for "
                "NOVAS functionality"
            ) from e

        # Call is needed to load high-precision ephem data in NOVAS
        jd_start, jd_end, number = eph_manager.ephem_open()

        # Define the obs location, which is needed to calculate diurnal abb term
        # and polar wobble corrections
        site_loc = novas.make_on_surface(
            site_loc.lat.deg,  # latitude in deg
            site_loc.lon.deg,  # Longitude in deg
            site_loc.height.to_value("m"),  # Height in meters
            0.0,  # Temperature, set to 0 for now (no atm refrac)
            0.0,  # Pressure, set to 0 for now (no atm refrac)
        )

        # NOVAS wants things in terrestial time and UT1
        tt_time_array = time_obj_array.tt.jd
        ut1_time_array = time_obj_array.ut1.jd
        gast_array = time_obj_array.sidereal_time("apparent", "greenwich").rad

        if np.any(tt_time_array < jd_start) or np.any(tt_time_array > jd_end):
            raise ValueError(
                "No current support for JPL ephems outside of 1700 - 2300 AD. "
                "Check back later (or possibly earlier)..."
            )

        app_ra = np.zeros(tt_time_array.shape) + np.zeros(ra_coord.shape)
        app_dec = np.zeros(tt_time_array.shape) + np.zeros(ra_coord.shape)

        for idx in range(len(app_ra)):
            if multi_coord or (idx == 0):
                # Create a catalog entry for the source in question
                cat_entry = novas.make_cat_entry(
                    "dummy_name",  # Dummy source name
                    "GKK",  # Catalog ID, fixed for now
                    156,  # Star ID number, fixed for now
                    ra_coord[idx].to_value("hourangle"),
                    dec_coord[idx].to_value("deg"),
                    0.0
                    if pm_ra is None
                    else (
                        pm_ra_coord.to_value("mas/yr")
                        * np.cos(dec_coord[idx].to_value("rad"))
                    ),
                    0.0 if pm_dec is None else pm_dec_coord.to_value("mas/yr"),
                    0.0
                    if (dist is None or np.any(dist == 0.0))
                    else (d_coord.kiloparsec ** -1.0),
                    0.0 if (vrad is None) else v_coord.to_value("km/s"),
                )

            # Update polar wobble parameters for a given timestamp
            if multi_time or (idx == 0):
                gast = gast_array[idx]
                pm_x = pm_x_array[idx] * np.cos(gast) + pm_y_array[idx] * np.sin(gast)
                pm_y = pm_y_array[idx] * np.cos(gast) - pm_x_array[idx] * np.sin(gast)
                tt_time = tt_time_array[idx]
                ut1_time = ut1_time_array[idx]
                novas.cel_pole(
                    tt_time, 2, delta_x_array[idx], delta_y_array[idx],
                )

            # Calculate topocentric RA/Dec values
            [temp_ra, temp_dec] = novas.topo_star(
                tt_time,
                (tt_time - ut1_time) * 86400.0,
                cat_entry,
                site_loc,
                accuracy=0,
            )
            xyz_array = polar2_to_cart3(
                temp_ra * (np.pi / 12.0), temp_dec * (np.pi / 180.0)
            )
            xyz_array = novas.wobble(tt_time, pm_x, pm_y, xyz_array, 1)

            app_ra[idx], app_dec[idx] = cart3_to_polar2(np.array(xyz_array))
    elif astrometry_library == "erfa":
        # liberfa wants things in radians
        pm_x_array *= np.pi / (3600.0 * 180.0)
        pm_y_array *= np.pi / (3600.0 * 180.0)
        [_, _, _, app_dec, app_ra, eqn_org] = erfa.atco13(
            ra_coord.to_value("rad"),
            dec_coord.to_value("rad"),
            0.0 if (pm_ra is None) else pm_ra_coord.to_value("rad/yr"),
            0.0 if (pm_dec is None) else pm_dec_coord.to_value("rad/yr"),
            0.0 if (dist is None or np.any(dist == 0.0)) else (d_coord.pc ** -1.0),
            0.0 if (vrad is None) else v_coord.to_value("km/s"),
            time_obj_array.utc.jd,
            0.0,
            time_obj_array.delta_ut1_utc,
            site_loc.lon.rad,
            site_loc.lat.rad,
            site_loc.height.to_value("m"),
            pm_x_array,
            pm_y_array,
            0,  # ait pressure, used for refraction (ignored)
            0,  # amb temperature, used for refraction (ignored)
            0,  # rel humidity, used for refraction (ignored)
            0,  # wavelength, used for refraction (ignored)
        )

        app_ra = np.mod(app_ra - eqn_org, 2 * np.pi)

    return app_ra, app_dec


def transform_app_to_icrs(
    time_array, app_ra, app_dec, telescope_loc, astrometry_library="erfa",
):
    """
    Transform a set of coordinates in topocentric/apparent to ICRS coordinates.

    This utility uses either astropy or erfa to calculate the ICRS  coordinates of
    a given set of apparent source coordinates. These coordinates are most typically
    used for defining the celestial/catalog position of a source. Note that at present,
    this is only implemented in astropy and pyERFA, although it could hypothetically
    be extended to NOVAS at some point.

    Parameters
    ----------
    time_array : float or ndarray of float
        Julian dates to calculate coordinate positions for. Can either be a single
        float, or an ndarray of shape (Ntimes,).
    app_ra : float or ndarray of float
        ICRS RA of the celestial target, expressed in units of radians. Can either
        be a single float or array of shape (Ncoord,). Note that if time_array is
        not a singleton value, then Ncoord must be equal to Ntimes.
    app_dec : float or ndarray of float
        ICRS Dec of the celestial target, expressed in units of radians. Can either
        be a single float or array of shape (Ncoord,). Note that if time_array is
        not a singleton value, then Ncoord must be equal to Ntimes.
    telescope_loc : tuple of floats or EarthLocation
        ITRF latitude, longitude, and altitude (rel to sea-level) of the phase center
        of the array. Can either be provided as an astropy EarthLocation, or a tuple
        of shape (3,) containung (in order) the latitude, longitude, and altitude,
        in units of radians, radians, and meters, respectively.

    Returns
    -------
    icrs_ra : ndarray of floats
        ICRS right ascension coordinates, in units of radians, of either shape
        (Ntimes,) if Ntimes >1, otherwise (Ncoord,).
    icrs_dec : ndarray of floats
        ICRS declination coordinates, in units of radians, of either shape
        (Ntimes,) if Ntimes >1, otherwise (Ncoord,).
    """
    # Make sure that the library requested is actually permitted
    if astrometry_library not in ["erfa", "astropy"]:
        raise ValueError(
            "Requested coordinate transformation library is not supported, please "
            "select either 'erfa' or 'astropy' for astrometry_library."
        )

    ra_coord = app_ra * units.rad
    dec_coord = app_dec * units.rad

    # Check here to make sure that ra_coord and dec_coord are the same length,
    # either 1 or len(time_array)
    multi_coord = ra_coord.size != 1
    if ra_coord.shape != dec_coord.shape:
        raise ValueError("app_ra and app_dec must be the same shape.")

    if isinstance(telescope_loc, EarthLocation):
        site_loc = telescope_loc
    else:
        site_loc = EarthLocation.from_geodetic(
            telescope_loc[1] * (180.0 / np.pi),
            telescope_loc[0] * (180.0 / np.pi),
            height=telescope_loc[2],
        )

    if isinstance(time_array, Time):
        time_obj_array = time_array
    else:
        time_obj_array = Time(time_array, format="jd", scale="utc")

    if time_obj_array.size != 1:
        if (time_obj_array.shape != ra_coord.shape) and multi_coord:
            raise ValueError(
                "time_array must be of either of length 1 (single "
                "float) or same length as ra and dec."
            )
    elif time_obj_array.ndim == 0:
        # Make the array at least 1-dimensional so we don't run into indexing
        # issues later.
        time_obj_array = Time([time_obj_array])

    if astrometry_library == "astropy":
        az_coord, el_coord = erfa.hd2ae(
            np.mod(
                time_obj_array.sidereal_time("apparent", longitude=site_loc.lon).rad
                - ra_coord.to_value("rad"),
                2 * np.pi,
            ),
            dec_coord.to_value("rad"),
            site_loc.lat.rad,
        )

        sky_coord = SkyCoord(
            az_coord * units.rad,
            el_coord * units.rad,
            frame="altaz",
            location=site_loc,
            obstime=time_obj_array,
        )

        coord_data = sky_coord.transform_to("icrs")
        icrs_ra = coord_data.ra.rad
        icrs_dec = coord_data.dec.rad
    elif astrometry_library == "erfa":
        # Get IERS data, which is needed for highest precision
        polar_motion_data = iers.earth_orientation_table.get()

        pm_x_array, pm_y_array = polar_motion_data.pm_xy(time_obj_array)
        pm_x_array = pm_x_array.to_value("rad")
        pm_y_array = pm_y_array.to_value("rad")

        bpn_matrix = erfa.pnm06a(time_obj_array.tt.jd, 0.0)
        cip_x, cip_y = erfa.bpn2xy(bpn_matrix)
        cio_s = erfa.s06(time_obj_array.tt.jd, 0.0, cip_x, cip_y)
        eqn_org = erfa.eors(bpn_matrix, cio_s)

        # Observed to ICRS via ERFA
        icrs_ra, icrs_dec = erfa.atoc13(
            "r",
            ra_coord.to_value("rad") + eqn_org,
            dec_coord.to_value("rad"),
            time_obj_array.utc.jd,
            0.0,  # Second half of the UT date, not needed
            time_obj_array.delta_ut1_utc,
            site_loc.lon.rad,
            site_loc.lat.rad,
            site_loc.height.value,
            pm_x_array,
            pm_y_array,
            0,  # ait pressure, used for refraction (ignored)
            0,  # amb temperature, used for refraction (ignored)
            0,  # rel humidity, used for refraction (ignored)
            0,  # wavelength, used for refraction (ignored)
        )

    # Return back the two RA/Dec arrays
    return icrs_ra, icrs_dec


def calc_parallactic_angle(
    app_ra, app_dec, lst_array, telescope_lat,
):
    """
    Calculate the parallactic angle between RA/Dec and the AltAz frame.

    Parameters
    ----------
    app_ra : ndarray of floats
        Array of apparent RA values in units of radians, shape (Ntimes,).
    app_dec : ndarray of floats
        Array of apparent dec values in units of radians, shape (Ntimes,).
    telescope_lat : float
        Latitude of the observatory, in units of radians.
    lst_array : float or ndarray of float
        Array of local apparent sidereal timesto calculate position angle values
        for, in units of radians. Can either be a single float or an array of shape
        (Ntimes,).
    """
    # This is just a simple wrapped around the pas function in ERFA
    return erfa.pas(app_ra, app_dec, lst_array, telescope_lat)


def calc_frame_pos_angle(
    time_array,
    app_ra,
    app_dec,
    telescope_loc,
    ref_frame,
    ref_epoch=None,
    offset_pos=(np.pi / 360.0),
):
    """
    Calculate an position angle given apparent position and reference frame.

    This function is used to determine the position angle between the great
    circle of declination in apparent coordinates, versus that in a given
    reference frame. Note that this is slightly different than parallactic
    angle, which is the difference between apparent declination and elevation.

    Paramters
    ---------
    time_array : float or ndarray of floats
        Array of julian dates to calculate position angle values for, of shape
        (Ntimes,).
    app_ra : ndarray of floats
        Array of apparent RA values in units of radians, shape (Ntimes,).
    app_dec : ndarray of floats
        Array of apparent dec values in units of radians, shape (Ntimes,).
    telescope_loc : tuple of floats or EarthLocation
        ITRF latitude, longitude, and altitude (rel to sea-level) of the observer.
        Can either be provided as an astropy EarthLocation, or an array-like of shape
        (3,) containing the latitude, longitude, and altitude, in that order, with units
        of radians, radians, and meters, respectively.
    offset_pos : float
        Distance of the offset position used to calculate the frame PA. Default
        is 0.5 degrees, which should be sufficent for most applications.
    ref_frame : str
        Coordinate frame to calculate position angles for. Can be any of the
        several supported frames in astropy (a limited list: fk4, fk5, icrs,
        gcrs, cirs, galactic).
    ref_epoch : str or flt
        Epoch of the coordinates, only used when ref_frame = fk4 or fk5. Given
        in unites of fractional years, either as a float or as a string with
        the epoch abbreviation (e.g, Julian epoch 2000.0 would be J2000.0).


    Returns
    -------
    frame_pa : ndarray of floats
        Array of position angles, in units of radians.
    """
    # Check to see if the position angles should default to zero
    if (ref_frame is None) or (ref_frame == "topo"):
        # No-op detected, ENGAGE MAXIMUM SNARK!
        return np.zeros_like(time_array)

    # This creates an array of unique entries of ra + dec + time, since the processing
    # time for each element can be non-negligible, and entries along the Nblt axis can
    # be highly redundant.
    unique_mask = np.union1d(
        np.union1d(
            np.unique(app_ra, return_index=True)[1],
            np.unique(app_dec, return_index=True)[1],
        ),
        np.unique(time_array, return_index=True)[1],
    )

    # Pluck out the unique entries for each
    unique_ra = app_ra[unique_mask]
    unique_dec = app_dec[unique_mask]
    unique_time = time_array[unique_mask]

    # Figure out how many elements we need to transform
    n_coord = len(unique_mask)

    # Offset north/south positions by 0.5 deg, such that the PA is determined over a
    # 1 deg arc.
    up_dec = unique_dec + (np.pi / 360.0)
    dn_dec = unique_dec - (np.pi / 360.0)
    up_ra = dn_ra = unique_ra

    # Wrap the positions if they happen to go over the poles
    up_ra[up_dec > (np.pi / 2.0)] = np.mod(
        up_ra[up_dec > (np.pi / 2.0)] + np.pi, 2.0 * np.pi
    )
    up_dec[up_dec > (np.pi / 2.0)] = np.pi - up_dec[up_dec > (np.pi / 2.0)]

    dn_ra[-dn_dec > (np.pi / 2.0)] = np.mod(
        dn_ra[dn_dec > (np.pi / 2.0)] + np.pi, 2.0 * np.pi
    )
    dn_dec[-dn_dec > (np.pi / 2.0)] = np.pi - dn_dec[-dn_dec > (np.pi / 2.0)]

    # Run the set of offset coordinates through the "reverse" transform. The two offset
    # positions are concat'd together to help reduce overheads
    ref_ra, ref_dec = calc_sidereal_coords(
        np.tile(unique_time, 2),
        np.concatenate((dn_ra, up_ra)),
        np.concatenate((dn_dec, up_dec)),
        telescope_loc,
        ref_frame,
        coord_epoch=ref_epoch,
    )

    # Use the pas function from ERFA to calculate the position angle. The negative sign
    # is here because we're measuring PA of app -> frame, but we want frame -> app.
    unique_pa = -erfa.pas(
        ref_ra[:n_coord], ref_dec[:n_coord], ref_ra[n_coord:], ref_dec[n_coord:]
    )

    # Finally, we have to go back through and "fill in" the redundant entries
    frame_pa = np.zeros_like(app_ra)
    for idx in range(n_coord):
        select_mask = np.logical_and(
            np.logical_and(unique_ra[idx] == app_ra, unique_dec[idx] == app_dec,),
            unique_time[idx] == time_array,
        )
        frame_pa[select_mask] = unique_pa[idx]

    return frame_pa


def lookup_jplhorizons(
    target_name,
    time_array,
    telescope_loc=None,
    high_cadence=False,
    force_indv_lookup=None,
):
    """
    Lookup solar system body coordinates via the JPL-Horizons service.

    This utility is useful for generating ephemerides, which can then be interpolated in
    order to provide positional data for a target which is moving, such as planetary
    bodies and other solar system objects. Use of this function requires the
    installation of the `astroquery` module.


    Parameters
    ----------
    target_name : str
        Name of the target to gather an ephemeris for. Must match the name
        in the JPL-Horizons database.
    time_array : array-like of float
        Times in UTC Julian days to gather an ephemeris for.
    telescope_loc : array-like of float
        ITRF latitude, longitude, and altitude (rel to sea-level) of the observer. Must
        be an array-like of shape (3,) containing the latitude, longitude, and
        altitude, in that order, with units of radians, radians, and meters,
        respectively.
    high_cadence : bool
        If set to True, will calculate ephemeris points every 3 minutes in time, as
        opposed to the default of every 3 hours.
    force_indv_lookup : bool
        If set to True, will calculate coordinate values for each value found within
        `time_array`. If False, a regularized time grid is sampled that encloses the
        values contained within `time_array`. Default is False, unless `time_array` is
        of length 1, in which the default is set to True.


    Returns
    -------
    ephem_times : ndarray of float
        Times for which the ephemeris values were calculated, in UTC Julian days.
    ephem_ra : ndarray of float
        ICRS Right ascension of the target at the values within `ephem_times`, in
        units of radians.
    ephem_dec : ndarray of float
        ICRS Declination of the target at the values within `ephem_times`, in units
        of radians.
    ephem_dist : ndarray of float
        Distance of the target relative to the observer, at the values within
        `ephem_times`, in units of parsecs.
    ephem_vel : ndarray of float
        Velocity of the targets relative to the observer, at the values within
        `ephem_times`, in units of km/sec.
    """
    try:
        from astroquery.jplhorizons import Horizons
    except ImportError as err:  # pragma: no cover
        raise ImportError(
            "astroquery is not installed but is required for "
            "planet ephemeris functionality"
        ) from err
    from pyuvdata.data import DATA_PATH
    from os.path import join as path_join
    from json import load as json_load

    # Get the telescope location into a format that JPL-Horizons can understand,
    # which is nominally a dict w/ entries for lon (units of deg), lat (units of
    # deg), and elevation (units of km).
    if isinstance(telescope_loc, EarthLocation):
        site_loc = {
            "lon": telescope_loc.lon.deg,
            "lat": telescope_loc.lat.deg,
            "elevation": telescope_loc.height.to_value(unit=units.km),
        }
    elif telescope_loc is None:
        # Setting to None will report the geocentric position
        site_loc = None
    else:
        site_loc = {
            "lon": telescope_loc[1] * (180.0 / np.pi),
            "lat": telescope_loc[0] * (180.0 / np.pi),
            "elevation": telescope_loc[2] * (0.001),  # m -> km
        }

    # If force_indv_lookup is True, or unset but only providing a single value, then
    # just calculate the RA/Dec for the times requested rather than creating a table
    # to interpolate from.
    if force_indv_lookup or (
        (np.array(time_array).size == 1) and (force_indv_lookup is None)
    ):
        epoch_list = np.unique(time_array)
        if len(epoch_list) > 50:
            raise ValueError(
                "Requesting too many individual ephem points from JPL-Horizons. This "
                "can be remedied by setting force_indv_lookup=False or limiting the "
                "number of values in time_array."
            )
    else:
        # When querying for multiple times, its faster (and kinder to the
        # good folks at JPL) to create a range to query, and then interpolate
        # between values. The extra buffer of 0.001 or 0.25 days for high and
        # low cadence is to give enough data points to allow for spline
        # interpolation of the data.
        if high_cadence:
            start_time = np.min(time_array) - 0.001
            stop_time = np.max(time_array) + 0.001
            step_time = "3m"
            n_entries = (stop_time - start_time) * (1440.0 / 3.0)
        else:
            # The start/stop time here are setup to maximize reusability of the
            # data, since astroquery appears to cache the results from previous
            # queries.
            start_time = (0.25 * np.floor(4.0 * np.min(time_array))) - 0.25
            stop_time = (0.25 * np.ceil(4.0 * np.max(time_array))) + 0.25
            step_time = "3h"
            n_entries = (stop_time - start_time) * (24.0 / 3.0)
        # We don't want to overtax the JPL service, so limit ourselves to 1000
        # individual queries at a time. Note that this is likely a conservative
        # cap for JPL-Horizons, but there should be exceptionally few applications
        # that actually require more than this.
        if n_entries > 1000:
            if (len(np.unique(time_array)) <= 50) and (force_indv_lookup is None):
                # If we have a _very_ sparse set of epochs, pass that along instead
                epoch_list = np.unique(time_array)
            else:
                # Otherwise, time to raise an error
                raise ValueError(
                    "Too many ephem points requested from JPL-Horizons. This "
                    "can be remedied by setting high_cadance=False or limiting "
                    "the number of values in time_array."
                )
        else:
            epoch_list = {
                "start": Time(start_time, format="jd").isot,
                "stop": Time(stop_time, format="jd").isot,
                "step": step_time,
            }
    # Check to make sure dates are within the 1700-2200 time range,
    # since not all targets are supported outside of this range
    if (np.min(time_array) < 2341973.0) or (np.max(time_array) > 2524593.0):
        raise ValueError(
            "No current support for JPL ephems outside of 1700 - 2300 AD. "
            "Check back later (or possibly earlier)..."
        )

    # JPL-Horizons has a separate catalog with what it calls 'major bodies',
    # and will throw an error if you use the wrong catalog when calling for
    # astrometry. We'll use the dict below to capture this behavior.
    with open(path_join(DATA_PATH, "jpl_major_bodies.json"), "r") as fhandle:
        major_body_dict = json_load(fhandle)

    target_id = target_name
    id_type = "smallbody"
    # If we find the target in the major body database, then we can extract the
    # target ID to make the query a bit more robust (otherwise JPL-Horizons will fail
    # on account that id will find multiple partial matches: e.g., "Mars" will be
    # matched with "Mars", "Mars Explorer", "Mars Barycenter"..., and JPL-Horizons will
    # not know which to choose).
    if target_name in major_body_dict.keys():
        target_id = major_body_dict[target_name]
        id_type = None

    query_obj = Horizons(
        id=target_id, location=site_loc, epochs=epoch_list, id_type=id_type,
    )
    # If not in the major bodies catalog, try the minor bodies list, and if
    # still not found, throw an error.
    try:
        ephem_data = query_obj.ephemerides(extra_precision=True)
    except KeyError:
        # This is a fix for an astroquery + JPL-Horizons bug, that's related to
        # API change on JPL's side. In this case, the source is identified, but
        # astroquery can't correctly parse the return message from JPL-Horizons.
        # See astroquery issue #2169.
        ephem_data = query_obj.ephemerides(extra_precision=False)  # pragma: no cover
    except ValueError as err:
        query_obj._session.close()
        raise ValueError(
            "Target ID is not recognized in either the small or major bodies "
            "catalogs, please consult the JPL-Horizons database for supported "
            "targets (https://ssd.jpl.nasa.gov/?horizons)."
        ) from err
    # This is explicitly closed here to trap a bug that occassionally throws an
    # unexpected warning, see astroquery issue #1807
    query_obj._session.close()

    # Now that we have the ephem data, extract out the relevant data
    ephem_times = np.array(ephem_data["datetime_jd"])
    ephem_ra = np.array(ephem_data["RA"]) * (np.pi / 180.0)
    ephem_dec = np.array(ephem_data["DEC"]) * (np.pi / 180.0)
    ephem_dist = np.array(ephem_data["delta"])  # AU
    ephem_vel = np.array(ephem_data["delta_rate"])  # km/s

    return ephem_times, ephem_ra, ephem_dec, ephem_dist, ephem_vel


def interpolate_ephem(
    time_array, ephem_times, ephem_ra, ephem_dec, ephem_dist=None, ephem_vel=None,
):
    """
    Interpolates ephemerides to give positions for requested times.

    This is a simple tool for calculated interpolated RA and Dec positions, as well
    as distances and velocities, for a given ephemeris. Under the hood, the method
    uses as cubic spline interpolation to calculate values at the requested times,
    provided that there are enough values to interpolate over to do so (requires
    >= 4 points), otherwise a linear interpolation is used.

    Parameters
    ----------
    time_array : array-like of floats
        Times to interpolate positions for, in UTC Julian days.
    ephem_times : array-like of floats
        Times in UTC Julian days which describe that match to the recorded postions
        of the target. Must be array-like, of shape (Npts,), where Npts is the number
        of ephemeris points.
    ephem_ra : array-like of floats
        Right ascencion of the target, at the times given in `ephem_times`. Units are
        in radians, must have the same shape as `ephem_times`.
    ephem_dec : array-like of floats
        Declination of the target, at the times given in `ephem_times`. Units are
        in radians, must have the same shape as `ephem_times`.
    ephem_dist : array-like of floats
        Distance of the target from the observer, at the times given in `ephem_times`.
        Optional argument, in units of parsecs. Must have the same shape as
        `ephem_times`.
    ephem_vel : array-like of floats
        Velocities of the target, at the times given in `ephem_times`. Optional
        argument, in units of km/sec. Must have the same shape as `ephem_times`.

    Returns
    -------
    ra_vals : ndarray of float
        Interpolated RA values, returned as an ndarray of floats with
        units of radians, and the same shape as `time_array`.
    dec_vals : ndarray of float
        Interpolated declination values, returned as an ndarray of floats with
        units of radians, and the same shape as `time_array`.
    dist_vals : None or ndarray of float
        If `ephem_dist` was provided, an ndarray of floats (with same shape as
        `time_array`) with the interpolated target distances, in units of parsecs.
        If `ephem_dist` was not provided, this returns as None.
    vel_vals : None or ndarray of float
        If `ephem_vals` was provided, an ndarray of floats (with same shape as
        `time_array`) with the interpolated target velocities, in units of km/sec.
        If `ephem_vals` was not provided, this returns as None.

    """
    # We're importing this here since it's only used for this one function
    from scipy.interpolate import interp1d

    ephem_shape = np.array(ephem_times).shape

    # Make sure that things look reasonable
    if np.array(ephem_ra).shape != ephem_shape:
        raise ValueError("ephem_ra must have the same shape as ephem_times.")

    if np.array(ephem_dec).shape != ephem_shape:
        raise ValueError("ephem_dec must have the same shape as ephem_times.")

    if (np.array(ephem_dist).shape != ephem_shape) and (ephem_dist is not None):
        raise ValueError("ephem_dist must have the same shape as ephem_times.")

    if (np.array(ephem_vel).shape != ephem_shape) and (ephem_vel is not None):
        raise ValueError("ephem_vel must have the same shape as ephem_times.")

    ra_vals = np.zeros_like(time_array, dtype=float)
    dec_vals = np.zeros_like(time_array, dtype=float)
    dist_vals = None if ephem_dist is None else np.zeros_like(time_array, dtype=float)
    vel_vals = None if ephem_vel is None else np.zeros_like(time_array, dtype=float)

    if len(ephem_times) == 1:
        ra_vals += ephem_ra
        dec_vals += ephem_dec
        if ephem_dist is not None:
            dist_vals += ephem_dist
        if ephem_vel is not None:
            vel_vals += ephem_vel
    else:
        if len(ephem_times) > 3:
            interp_kind = "cubic"
        else:
            interp_kind = "linear"

        # If we have values that line up perfectly, just use those directly
        select_mask = np.isin(time_array, ephem_times)
        if np.any(select_mask):
            time_select = time_array[select_mask]
            ra_vals[select_mask] = interp1d(ephem_times, ephem_ra, kind="nearest")(
                time_select
            )
            dec_vals[select_mask] = interp1d(ephem_times, ephem_dec, kind="nearest")(
                time_select
            )
            if ephem_dist is not None:
                dist_vals[select_mask] = interp1d(
                    ephem_times, ephem_dist, kind="nearest"
                )(time_select)
            if ephem_vel is not None:
                vel_vals[select_mask] = interp1d(
                    ephem_times, ephem_vel, kind="nearest"
                )(time_select)

        # If we have values lining up between grid points, use spline interpolation
        # to calculate their values
        select_mask = ~select_mask
        if np.any(select_mask):
            time_select = time_array[select_mask]
            ra_vals[select_mask] = interp1d(ephem_times, ephem_ra, kind=interp_kind)(
                time_select
            )
            dec_vals[select_mask] = interp1d(ephem_times, ephem_dec, kind=interp_kind)(
                time_select
            )
            if ephem_dist is not None:
                dist_vals[select_mask] = interp1d(
                    ephem_times, ephem_dist, kind=interp_kind
                )(time_select)
            if ephem_vel is not None:
                vel_vals[select_mask] = interp1d(
                    ephem_times, ephem_vel, kind=interp_kind
                )(time_select)

    return (ra_vals, dec_vals, dist_vals, vel_vals)


def calc_app_coords(
    lon_coord,
    lat_coord,
    coord_frame="icrs",
    coord_epoch=None,
    coord_times=None,
    coord_type="sidereal",
    time_array=None,
    lst_array=None,
    telescope_loc=None,
    pm_ra=None,
    pm_dec=None,
    vrad=None,
    dist=None,
):
    """
    Calculate apparent coordinates for several different coordinate types.

    This function calculates apparent positions at the current epoch.

    Parameters
    ----------
    lon_coord : float or ndarray of float
        Longitudinal (e.g., RA) coordinates, units of radians. Must match the same
        shape as lat_coord.
    lat_coord : float or ndarray of float
        Latitudinal (e.g., Dec) coordinates, units of radians. Must match the same
        shape as lon_coord.
    coord_frame : string
        The requested reference frame for the output coordinates, can be any frame
        that is presently supported by astropy.
    coord_epoch : float or str or Time object
        Epoch for ref_frame, nominally only used if converting to either the FK4 or
        FK5 frames, in units of fractional years. If provided as a float and the
        coord_frame is an FK4-variant, value will assumed to be given in Besselian
        years (i.e., 1950 would be 'B1950'), otherwise the year is assumed to be
        in Julian years.
    coord_times : float or ndarray of float
        Only used when `coord_type="ephem"`, the JD UTC time for each value of
        `lon_coord` and `lat_coord`. These values are used to interpolate `lon_coord`
        and `lat_coord` values to those times listed in `time_array`.
    coord_type : str
        coord_type : str
            Type of source to calculate coordinates for. Must be one of:
                "sidereal" (fixed RA/Dec),
                "ephem" (RA/Dec that moves with time),
                "driftscan" (fixed az/el position),
                "unphased" (alias for "driftscan" with (Az, Alt) = (0 deg, 90 deg)).
    time_array : float or ndarray of float or Time object
        Times for which the apparent coordinates were calculated, in UTC JD. If more
        than a single element, must be the same shape as lon_coord and lat_coord if
        both of those are arrays (instead of single floats).
    telescope_loc : array-like of floats or EarthLocation
        ITRF latitude, longitude, and altitude (rel to sea-level) of the phase center
        of the array. Can either be provided as an astropy EarthLocation, or a tuple
        of shape (3,) containung (in order) the latitude, longitude, and altitude,
        in units of radians, radians, and meters, respectively.
    coord_frame : string
        The requested reference frame for the output coordinates, can be any frame
        that is presently supported by astropy. Default is ICRS.
    coord_epoch : float or str or Time object
        Epoch for ref_frame, nominally only used if converting to either the FK4 or
        FK5 frames, in units of fractional years. If provided as a float and the
        ref_frame is an FK4-variant, value will assumed to be given in Besselian
        years (i.e., 1950 would be 'B1950'), otherwise the year is assumed to be
        in Julian years.
    pm_ra : float or ndarray of float
        Proper motion in RA of the source, expressed in units of milliarcsec / year.
        Can either be a single float or array of shape (Ntimes,), although this must
        be consistent with other parameters (namely ra_coord and dec_coord). Not
        required, motion is calculated relative to the value of `coord_epoch`.
    pm_dec : float or ndarray of float
        Proper motion in Dec of the source, expressed in units of milliarcsec / year.
        Can either be a single float or array of shape (Ntimes,), although this must
        be consistent with other parameters (namely ra_coord and dec_coord). Not
        required, motion is calculated relative to the value of `coord_epoch`.
    vrad : float or ndarray of float
        Radial velocity of the source, expressed in units of km / sec. Can either be
        a single float or array of shape (Ntimes,), although this must be consistent
        with other parameters (namely ra_coord and dec_coord). Not required.
    dist : float or ndarray of float
        Distance of the source, expressed in milliarcseconds. Can either be a single
        float or array of shape (Ntimes,), although this must be consistent with other
        parameters (namely ra_coord and dec_coord). Not required.

    Returns
    -------
    app_ra : ndarray of floats
        Apparent right ascension coordinates, in units of radians.
    app_dec : ndarray of floats
        Apparent declination coordinates, in units of radians.
    """
    if isinstance(telescope_loc, EarthLocation):
        site_loc = telescope_loc
    else:
        site_loc = EarthLocation.from_geodetic(
            telescope_loc[1] * (180.0 / np.pi),
            telescope_loc[0] * (180.0 / np.pi),
            height=telescope_loc[2],
        )

    # Time objects and unique don't seem to play well together, so we break apart
    # their handling here
    if isinstance(time_array, Time):
        unique_time_array, unique_mask = np.unique(time_array.utc.jd, return_index=True)
    else:
        unique_time_array, unique_mask = np.unique(time_array, return_index=True)

    if coord_type in ["driftscan", "unphased"]:
        if lst_array is None:
            unique_lst = get_lst_for_time(
                unique_time_array,
                site_loc.lat.deg,
                site_loc.lon.deg,
                site_loc.height.to_value("m"),
            )
        else:
            unique_lst = lst_array[unique_mask]

    if coord_type == "sidereal":
        # If the coordinates are not in the ICRS frame, go ahead and transform them now
        if coord_frame != "icrs":
            icrs_ra, icrs_dec = transform_sidereal_coords(
                lon_coord,
                lat_coord,
                coord_frame,
                "icrs",
                in_coord_epoch=coord_epoch,
                time_array=unique_time_array,
            )
        else:
            icrs_ra = lon_coord
            icrs_dec = lat_coord
        unique_app_ra, unique_app_dec = transform_icrs_to_app(
            unique_time_array,
            icrs_ra,
            icrs_dec,
            site_loc,
            pm_ra=pm_ra,
            pm_dec=pm_dec,
            vrad=vrad,
            dist=dist,
        )

    elif coord_type == "driftscan":
        # Use the ERFA function ae2hd, which will do all the heavy
        # lifting for us
        unique_app_ha, unique_app_dec = erfa.ae2hd(
            lon_coord, lat_coord, site_loc.lat.rad
        )
        # The above returns HA/Dec, so we just need to rotate by
        # the LST to get back app RA and Dec
        unique_app_ra = np.mod(unique_app_ha + unique_lst, 2 * np.pi)
        unique_app_dec = unique_app_dec + np.zeros_like(unique_app_ra)
    elif coord_type == "ephem":
        interp_ra, interp_dec, _, _ = interpolate_ephem(
            unique_time_array, coord_times, lon_coord, lat_coord,
        )
        if coord_frame != "icrs":
            icrs_ra, icrs_dec = transform_sidereal_coords(
                interp_ra,
                interp_dec,
                coord_frame,
                "icrs",
                in_coord_epoch=coord_epoch,
                time_array=unique_time_array,
            )
        else:
            icrs_ra = interp_ra
            icrs_dec = interp_dec
        # TODO: Vel and distance handling to be integrated here, once they are are
        # needed for velocity frame tracking
        unique_app_ra, unique_app_dec = transform_icrs_to_app(
            unique_time_array, icrs_ra, icrs_dec, site_loc, pm_ra=pm_ra, pm_dec=pm_dec,
        )
    elif coord_type == "unphased":
        # This is the easiest one - this is just supposed to be ENU, so set the
        # apparent coords to the current lst and telescope_lon.
        unique_app_ra = unique_lst.copy()
        unique_app_dec = np.zeros_like(unique_app_ra) + site_loc.lat.rad
    else:
        raise ValueError("Object type %s is not recognized." % coord_type)

    # Now that we've calculated all the unique values, time to backfill through the
    # "redundant" entries in the Nblt axis.
    app_ra = np.zeros(np.array(time_array).shape)
    app_dec = np.zeros(np.array(time_array).shape)

    # Need this promotion in order to match entries
    if isinstance(time_array, Time):
        unique_time_array = Time(unique_time_array, format="jd", scale="utc")
    for idx, unique_time in enumerate(unique_time_array):
        select_mask = time_array == unique_time
        app_ra[select_mask] = unique_app_ra[idx]
        app_dec[select_mask] = unique_app_dec[idx]

    return app_ra, app_dec


def calc_sidereal_coords(
    time_array, app_ra, app_dec, telescope_loc, coord_frame, coord_epoch=None,
):
    """
    Calculate sidereal coordinates given apparent coordinates.

    This function calculates coordinates in the requested frame (at a given epoch)
    from a set of apparent coordinates.

    Parameters
    ----------
    time_array : float or ndarray of float or Time object
        Times for which the apparent coordinates were calculated, in UTC JD. Must
        match the shape of app_ra and app_dec.
    app_ra : float or ndarray of float
        Array of apparent right ascension coordinates, units of radians. Must match
        the shape of time_array and app_dec.
    app_ra : float or ndarray of float
        Array of apparent right declination coordinates, units of radians. Must match
        the shape of time_array and app_dec.
    telescope_loc : tuple of floats or EarthLocation
        ITRF latitude, longitude, and altitude (rel to sea-level) of the phase center
        of the array. Can either be provided as an astropy EarthLocation, or a tuple
        of shape (3,) containung (in order) the latitude, longitude, and altitude,
        in units of radians, radians, and meters, respectively.
    coord_frame : string
        The requested reference frame for the output coordinates, can be any frame
        that is presently supported by astropy. Default is ICRS.
    coord_epoch : float or str or Time object
        Epoch for ref_frame, nominally only used if converting to either the FK4 or
        FK5 frames, in units of fractional years. If provided as a float and the
        ref_frame is an FK4-variant, value will assumed to be given in Besselian
        years (i.e., 1950 would be 'B1950'), otherwise the year is assumed to be
        in Julian years.

    Returns
    -------
    ref_ra : ndarray of floats
        Right ascension coordinates in the requested frame, in units of radians.
        Either shape (Ntimes,) if Ntimes >1, otherwise (Ncoord,).
    ref_dec : ndarray of floats
        Declination coordinates in the requested frame, in units of radians.
        Either shape (Ntimes,) if Ntimes >1, otherwise (Ncoord,).
    """
    # Check to make sure that we have a properly formatted epoch for our in-bound
    # coordinate frame
    epoch = None
    if isinstance(coord_epoch, str) or isinstance(coord_epoch, Time):
        # If its a string or a Time object, we don't need to do anything more
        epoch = Time(coord_epoch)
    elif coord_epoch is not None:
        if coord_frame.lower() in ["fk4", "fk4noeterms"]:
            epoch = Time(coord_epoch, format="byear")
        else:
            epoch = Time(coord_epoch, format="jyear")

    icrs_ra, icrs_dec = transform_app_to_icrs(
        time_array, app_ra, app_dec, telescope_loc
    )

    if coord_frame == "icrs":
        ref_ra, ref_dec = (icrs_ra, icrs_dec)
    else:
        ref_ra, ref_dec = transform_sidereal_coords(
            icrs_ra,
            icrs_dec,
            "icrs",
            coord_frame,
            out_coord_epoch=epoch,
            time_array=time_array,
        )

    return ref_ra, ref_dec


def get_lst_for_time(
    jd_array, latitude, longitude, altitude, astrometry_library="erfa"
):
    """
    Get the local apparent sidereal time for a set of jd times at an earth location.

    This function calculates the local apparent sidereal time (LAST), given a UTC time
    and a position on the Earth, using either the astropy or NOVAS libraries. It
    is important to note that there is an apporoximate 20 microsecond difference
    between the two methods, presumably due to small differences in the apparent
    reference frame. These differences will cancel out when calculating coordinates
    in the TOPO frame, so long as apparent coordinates are calculated using the
    same library (i.e., astropy or NOVAS). Failing to do so can introduce errors
    up to ~1 mas in the horizontal coordinate system (i.e., AltAz).

    Parameters
    ----------
    jd_array : ndarray of float
        JD times to get lsts for.
    latitude : float
        Latitude of location to get lst for in degrees.
    longitude : float
        Longitude of location to get lst for in degrees.
    altitude : float
        Altitude of location to get lst for in meters.
    astrometry_library : str
        Library used for running the LST calculations. Allowed options are 'erfa'
        (which uses the pyERFA), 'novas' (which uses the python-novas library),
        and 'astropy' (which uses the astropy utilities). Default is erfa.

    Returns
    -------
    ndarray of float
        LASTs in radians corresponding to the jd_array.

    """
    if isinstance(jd_array, np.ndarray):
        lst_array = np.zeros_like(jd_array)
    else:
        lst_array = np.zeros(1)

    jd, reverse_inds = np.unique(jd_array, return_inverse=True)
    times = Time(
        jd,
        format="jd",
        scale="utc",
        location=(Angle(longitude, unit="deg"), Angle(latitude, unit="deg"), altitude),
    )

    if iers.conf.auto_max_age is None:  # pragma: no cover
        delta, status = times.get_delta_ut1_utc(return_status=True)
        if np.any(
            np.isin(status, (iers.TIME_BEFORE_IERS_RANGE, iers.TIME_BEYOND_IERS_RANGE))
        ):
            warnings.warn(
                "time is out of IERS range, setting delta ut1 utc to "
                "extrapolated value"
            )
            times.delta_ut1_utc = delta
    if astrometry_library == "erfa":
        # This appears to be what astropy is using under the hood,
        # so it _should_ be totally consistent.
        gast_array = erfa.gst06a(times.ut1.jd, 0.0, times.tt.jd, 0.0)
        lst_array = np.mod(gast_array + (longitude * (np.pi / 180.0)), 2.0 * np.pi)[
            reverse_inds
        ]
    elif astrometry_library == "astropy":
        lst_array = times.sidereal_time("apparent").radian[reverse_inds]
    elif astrometry_library == "novas":
        # Import the NOVAS library only if it's needed/available.
        try:
            from novas import compat as novas
            from novas.compat import eph_manager
            import novas_de405  # noqa
        except ImportError as e:  # pragma: no cover
            raise ImportError(
                "novas and/or novas_de405 are not installed but is required for "
                "NOVAS functionality"
            ) from e

        jd_start, jd_end, number = eph_manager.ephem_open()

        tt_time_array = times.tt.value
        ut1_time_array = times.ut1.value
        polar_motion_data = iers.earth_orientation_table.get()

        delta_x_array = np.interp(
            times.mjd,
            polar_motion_data["MJD"].value,
            polar_motion_data["dX_2000A_B"].value,
            left=0.0,
            right=0.0,
        )

        delta_y_array = np.interp(
            times.mjd,
            polar_motion_data["MJD"].value,
            polar_motion_data["dY_2000A_B"].value,
            left=0.0,
            right=0.0,
        )

        # Catch the case where we don't have CIP delta values yet (they don't typically
        # have predictive values like the polar motion does)
        delta_x_array[np.isnan(delta_x_array)] = 0.0
        delta_y_array[np.isnan(delta_y_array)] = 0.0

        for idx in range(len(times)):
            novas.cel_pole(
                tt_time_array[idx], 2, delta_x_array[idx], delta_y_array[idx]
            )
            # The NOVAS routine will return Greenwich Apparent Sidereal Time (GAST),
            # in units of hours
            lst_array[reverse_inds == idx] = novas.sidereal_time(
                ut1_time_array[idx],
                0.0,
                (tt_time_array[idx] - ut1_time_array[idx]) * 86400.0,
            )

        # Add the telescope lon to convert from GAST to LAST (local)
        lst_array = np.mod(lst_array + (longitude / 15.0), 24.0)

        # Convert from hours back to rad
        lst_array *= np.pi / 12.0

    return lst_array


def _adj_list(vecs, tol, n_blocks=None):
    """Identify neighbors of each vec in vecs, to distance tol."""
    n_items = len(vecs)
    max_items = 2 ** 10  # Max array size used is max_items**2. Avoid using > 1 GiB

    if n_blocks is None:
        n_blocks = max(n_items // max_items, 1)

    # We may sort blocks so that some pairs of blocks may be skipped.
    # Reorder vectors by x.

    order = np.argsort(vecs[:, 0])
    blocks = np.array_split(order, n_blocks)
    adj = [{k} for k in range(n_items)]  # Adjacency lists
    for b1 in blocks:
        for b2 in blocks:
            v1, v2 = vecs[b1], vecs[b2]
            # Check for no overlap, with tolerance.
            xmin1 = v1[0, 0] - tol
            xmax1 = v1[-1, 0] + tol
            xmin2 = v2[0, 0] - tol
            xmax2 = v2[-1, 0] + tol
            if max(xmin1, xmin2) > min(xmax1, xmax2):
                continue

            adj_mat = cdist(vecs[b1], vecs[b2]) < tol
            for bi, col in enumerate(adj_mat):
                adj[b1[bi]] = adj[b1[bi]].union(b2[col])
    return [frozenset(g) for g in adj]


def _find_cliques(adj, strict=False):
    n_items = len(adj)

    loc_gps = []
    visited = np.zeros(n_items, dtype=bool)
    for k in range(n_items):
        if visited[k]:
            continue
        a0 = adj[k]
        visited[k] = True
        if all(adj[it].__hash__() == a0.__hash__() for it in a0):
            group = list(a0)
            group.sort()
            visited[list(a0)] = True
            loc_gps.append(group)

    # Require all adjacency lists to be isolated maximal cliques:
    if strict:
        if not all(sorted(st) in loc_gps for st in adj):
            raise ValueError("Non-isolated cliques found in graph.")

    return loc_gps


def find_clusters(location_ids, location_vectors, tol, strict=False):
    """
    Find clusters of vectors (e.g. redundant baselines, times).

    Parameters
    ----------
    location_ids : array_like of int
        ID labels for locations.
    location_vectors : array_like of float
        location vectors, can be multidimensional
    tol : float
        tolerance for clusters
    strict : bool
        Require that all adjacency lists be isolated maximal cliques.
        This ensures that vectors do not fall into multiple clusters.
        Default: False

    Returns
    -------
    list of list of location_ids

    """
    location_vectors = np.asarray(location_vectors)
    location_ids = np.asarray(location_ids)
    if location_vectors.ndim == 1:
        location_vectors = location_vectors[:, np.newaxis]

    adj = _adj_list(location_vectors, tol)  # adj = list of sets

    loc_gps = _find_cliques(adj, strict=strict)
    loc_gps = [np.sort(location_ids[gp]).tolist() for gp in loc_gps]
    return loc_gps


def get_baseline_redundancies(baselines, baseline_vecs, tol=1.0, with_conjugates=False):
    """
    Find redundant baseline groups.

    Parameters
    ----------
    baselines : array_like of int
        Baseline numbers, shape (Nbls,)
    baseline_vecs : array_like of float
        Baseline vectors in meters, shape (Nbls, 3)
    tol : float
        Absolute tolerance of redundancy, in meters.
    with_conjugates : bool
        Option to include baselines that are redundant when flipped.

    Returns
    -------
    baseline_groups : list of lists of int
        list of lists of redundant baseline numbers
    vec_bin_centers : list of array_like of float
        List of vectors describing redundant group centers
    lengths : list of float
        List of redundant group baseline lengths in meters
    baseline_ind_conj : list of int
        List of baselines that are redundant when reversed. Only returned if
        with_conjugates is True

    """
    Nbls = baselines.shape[0]

    if not baseline_vecs.shape == (Nbls, 3):
        raise ValueError("Baseline vectors must be shape (Nbls, 3)")

    baseline_vecs = copy.copy(baseline_vecs)  # Protect the vectors passed in.

    if with_conjugates:
        conjugates = []
        for bv in baseline_vecs:
            uneg = bv[0] < -tol
            uzer = np.isclose(bv[0], 0.0, atol=tol)
            vneg = bv[1] < -tol
            vzer = np.isclose(bv[1], 0.0, atol=tol)
            wneg = bv[2] < -tol
            conjugates.append(uneg or (uzer and vneg) or (uzer and vzer and wneg))

        conjugates = np.array(conjugates, dtype=bool)
        baseline_vecs[conjugates] *= -1
        baseline_ind_conj = baselines[conjugates]
        bl_gps, vec_bin_centers, lens = get_baseline_redundancies(
            baselines, baseline_vecs, tol=tol, with_conjugates=False
        )
        return bl_gps, vec_bin_centers, lens, baseline_ind_conj

    try:
        bl_gps = find_clusters(baselines, baseline_vecs, tol, strict=True)
    except ValueError as exc:
        raise ValueError(
            "Some baselines are falling into multiple"
            " redundant groups. Lower the tolerance to resolve ambiguity."
        ) from exc

    n_unique = len(bl_gps)
    vec_bin_centers = np.zeros((n_unique, 3))
    for gi, gp in enumerate(bl_gps):
        inds = [np.where(i == baselines)[0] for i in gp]
        vec_bin_centers[gi] = np.mean(baseline_vecs[inds, :], axis=0)

    lens = np.sqrt(np.sum(vec_bin_centers ** 2, axis=1))
    return bl_gps, vec_bin_centers, lens


def get_antenna_redundancies(
    antenna_numbers, antenna_positions, tol=1.0, include_autos=False
):
    """
    Find redundant baseline groups based on antenna positions.

    Parameters
    ----------
    antenna_numbers : array_like of int
        Antenna numbers, shape (Nants,).
    antenna_positions : array_like of float
        Antenna position vectors in the ENU (topocentric) frame in meters,
        shape (Nants, 3).
    tol : float
        Redundancy tolerance in meters.
    include_autos : bool
        Option to include autocorrelations.

    Returns
    -------
    baseline_groups : list of lists of int
        list of lists of redundant baseline numbers
    vec_bin_centers : list of array_like of float
        List of vectors describing redundant group centers
    lengths : list of float
        List of redundant group baseline lengths in meters

    Notes
    -----
    The baseline numbers refer to antenna pairs (a1, a2) such that
    the baseline vector formed from ENU antenna positions,
    blvec = enu[a1] - enu[a2]
    is close to the other baselines in the group.

    This is achieved by putting baselines in a form of the u>0
    convention, but with a tolerance in defining the signs of
    vector components.

    To guarantee that the same baseline numbers are present in a UVData
    object, ``UVData.conjugate_bls('u>0', uvw_tol=tol)``, where `tol` is
    the tolerance used here.

    """
    Nants = antenna_numbers.size

    bls = []
    bl_vecs = []

    for aj in range(Nants):
        mini = aj + 1
        if include_autos:
            mini = aj
        for ai in range(mini, Nants):
            anti, antj = antenna_numbers[ai], antenna_numbers[aj]
            bidx = antnums_to_baseline(antj, anti, Nants)
            bv = antenna_positions[ai] - antenna_positions[aj]
            bl_vecs.append(bv)
            bls.append(bidx)
    bls = np.array(bls)
    bl_vecs = np.array(bl_vecs)
    gps, vecs, lens, conjs = get_baseline_redundancies(
        bls, bl_vecs, tol=tol, with_conjugates=True
    )
    # Flip the baselines in the groups.
    for gi, gp in enumerate(gps):
        for bi, bl in enumerate(gp):
            if bl in conjs:
                gps[gi][bi] = baseline_index_flip(bl, Nants)

    return gps, vecs, lens


def mean_collapse(
    arr, weights=None, axis=None, return_weights=False, return_weights_square=False
):
    """
    Collapse by averaging data.

    This is similar to np.average, except it handles infs (by giving them
    zero weight) and zero weight axes (by forcing result to be inf with zero
    output weight).

    Parameters
    ----------
    arr : array
        Input array to process.
    weights: ndarray, optional
        weights for average. If none, will default to equal weight for all
        non-infinite data.
    axis : int or tuple, optional
        Axis or axes to collapse (passed to np.sum). Default is all.
    return_weights : bool
        Whether to return sum of weights.
    return_weights_square: bool
        Whether to return the sum of the square of the weights. Default is False.

    """
    arr = copy.deepcopy(arr)  # avoid changing outside
    if weights is None:
        weights = np.ones_like(arr)
    else:
        weights = copy.deepcopy(weights)
    weights = weights * np.logical_not(np.isinf(arr))
    arr[np.isinf(arr)] = 0
    weight_out = np.sum(weights, axis=axis)
    if return_weights_square:
        weights_square = weights ** 2
        weights_square_out = np.sum(weights_square, axis=axis)
    out = np.sum(weights * arr, axis=axis)
    where = weight_out > 1e-10
    out = np.true_divide(out, weight_out, where=where)
    out = np.where(where, out, np.inf)
    if return_weights and return_weights_square:
        return out, weight_out, weights_square_out
    elif return_weights:
        return out, weight_out
    elif return_weights_square:
        return out, weights_square_out
    else:
        return out


def absmean_collapse(
    arr, weights=None, axis=None, return_weights=False, return_weights_square=False
):
    """
    Collapse by averaging absolute value of data.

    Parameters
    ----------
    arr : array
        Input array to process.
    weights: ndarray, optional
        weights for average. If none, will default to equal weight for all
        non-infinite data.
    axis : int or tuple, optional
        Axis or axes to collapse (passed to np.sum). Default is all.
    return_weights : bool
        Whether to return sum of weights.
    return_weights_square: bool
        whether to return the sum of the squares of the weights. Default is False.

    """
    return mean_collapse(
        np.abs(arr),
        weights=weights,
        axis=axis,
        return_weights=return_weights,
        return_weights_square=return_weights_square,
    )


def quadmean_collapse(
    arr, weights=None, axis=None, return_weights=False, return_weights_square=False
):
    """
    Collapse by averaging in quadrature.

    Parameters
    ----------
    arr : array
        Input array to process.
    weights: ndarray, optional
        weights for average. If none, will default to equal weight for all
        non-infinite data.
    axis : int or tuple, optional
        Axis or axes to collapse (passed to np.sum). Default is all.
    return_weights : bool
        Whether to return sum of weights.
    return_weights_square: bool
        whether to return the sum of the squares of the weights. Default is False.

    """
    out = mean_collapse(
        np.abs(arr) ** 2,
        weights=weights,
        axis=axis,
        return_weights=return_weights,
        return_weights_square=return_weights_square,
    )
    if return_weights and return_weights_square:
        return np.sqrt(out[0]), out[1], out[2]
    elif return_weights or return_weights_square:
        return np.sqrt(out[0]), out[1]
    else:
        return np.sqrt(out)


def or_collapse(
    arr, weights=None, axis=None, return_weights=False, return_weights_square=False
):
    """
    Collapse using OR operation.

    Parameters
    ----------
    arr : array
        Input array to process.
    weights: ndarray, optional
        NOT USED, but kept for symmetry with other collapsing functions.
    axis : int or tuple, optional
        Axis or axes to collapse (take OR over). Default is all.
    return_weights : bool
        Whether to return dummy weights array.
        NOTE: the dummy weights will simply be an array of ones
    return_weights_square: bool
        NOT USED, but kept for symmetry with other collapsing functions.

    """
    if arr.dtype != np.bool_:
        raise ValueError("Input to or_collapse function must be boolean array")
    out = np.any(arr, axis=axis)
    if (weights is not None) and not np.all(weights == weights.reshape(-1)[0]):
        warnings.warn("Currently weights are not handled when OR-ing boolean arrays.")
    if return_weights:
        return out, np.ones_like(out, dtype=np.float64)
    else:
        return out


def and_collapse(
    arr, weights=None, axis=None, return_weights=False, return_weights_square=False
):
    """
    Collapse using AND operation.

    Parameters
    ----------
    arr : array
        Input array to process.
    weights: ndarray, optional
        NOT USED, but kept for symmetry with other collapsing functions.
    axis : int or tuple, optional
        Axis or axes to collapse (take AND over). Default is all.
    return_weights : bool
        Whether to return dummy weights array.
        NOTE: the dummy weights will simply be an array of ones
    return_weights_square: bool
        NOT USED, but kept for symmetry with other collapsing functions.

    """
    if arr.dtype != np.bool_:
        raise ValueError("Input to and_collapse function must be boolean array")
    out = np.all(arr, axis=axis)
    if (weights is not None) and not np.all(weights == weights.reshape(-1)[0]):
        warnings.warn("Currently weights are not handled when AND-ing boolean arrays.")
    if return_weights:
        return out, np.ones_like(out, dtype=np.float64)
    else:
        return out


def collapse(
    arr, alg, weights=None, axis=None, return_weights=False, return_weights_square=False
):
    """
    Parent function to collapse an array with a given algorithm.

    Parameters
    ----------
    arr : array
        Input array to process.
    alg : str
        Algorithm to use. Must be defined in this function with
        corresponding subfunction above.
    weights: ndarray, optional
        weights for collapse operation (e.g. weighted mean).
        NOTE: Some subfunctions do not use the weights. See corresponding
        doc strings.
    axis : int or tuple, optional
        Axis or axes to collapse. Default is all.
    return_weights : bool
        Whether to return sum of weights.
    return_weights_square: bool
        Whether to return the sum of the squares of the weights. Default is False.

    """
    collapse_dict = {
        "mean": mean_collapse,
        "absmean": absmean_collapse,
        "quadmean": quadmean_collapse,
        "or": or_collapse,
        "and": and_collapse,
    }
    try:
        out = collapse_dict[alg](
            arr,
            weights=weights,
            axis=axis,
            return_weights=return_weights,
            return_weights_square=return_weights_square,
        )
    except KeyError:
        raise ValueError(
            "Collapse algorithm must be one of: "
            + ", ".join(collapse_dict.keys())
            + "."
        )
    return out


def uvcalibrate(
    uvdata,
    uvcal,
    inplace=True,
    prop_flags=True,
    Dterm_cal=False,
    flip_gain_conj=False,
    delay_convention="minus",
    undo=False,
    time_check=True,
    ant_check=True,
):
    """
    Calibrate a UVData object with a UVCal object.

    Parameters
    ----------
    uvdata : UVData object
        UVData object to calibrate.
    uvcal : UVCal object
        UVCal object containing the calibration.
    inplace : bool, optional
        if True edit uvdata in place, else return a calibrated copy
    prop_flags : bool, optional
        if True, propagate calibration flags to data flags
        and doesn't use flagged gains. Otherwise, uses flagged gains and
        does not propagate calibration flags to data flags.
    Dterm_cal : bool, optional
        Calibrate the off-diagonal terms in the Jones matrix if present
        in uvcal. Default is False. Currently not implemented.
    flip_gain_conj : bool, optional
        This function uses the UVData ant_1_array and ant_2_array to specify the
        antennas in the UVCal object. By default, the conjugation convention, which
        follows the UVData convention (i.e. ant2 - ant1), is that the applied
        gain = ant1_gain * conjugate(ant2_gain). If the other convention is required,
        set flip_gain_conj=True.
    delay_convention : str, optional
        Exponent sign to use in conversion of 'delay' to 'gain' cal_type
        if the input uvcal is not inherently 'gain' cal_type. Default to 'minus'.
    undo : bool, optional
        If True, undo the provided calibration. i.e. apply the calibration with
        flipped gain_convention. Flag propagation rules apply the same.
    time_check : bool
        Option to check that times match between the UVCal and UVData
        objects if UVCal has a single time or time range. Times are always
        checked if UVCal has multiple times.
    ant_check : bool
        Option to check that all antennas with data on the UVData
        object have calibration solutions in the UVCal object. If this option is
        set to False, uvcalibrate will proceed without erroring and data for
        antennas without calibrations will be flagged.

    Returns
    -------
    UVData, optional
        Returns if not inplace

    """
    if not inplace:
        uvdata = uvdata.copy()

    # check both objects
    uvdata.check()
    uvcal.check()

    # Check whether the UVData antennas *that have data associated with them*
    # have associated data in the UVCal object
    uvdata_unique_nums = np.unique(np.append(uvdata.ant_1_array, uvdata.ant_2_array))
    uvdata.antenna_names = np.asarray(uvdata.antenna_names)
    uvdata_used_antnames = np.array(
        [
            uvdata.antenna_names[np.where(uvdata.antenna_numbers == antnum)][0]
            for antnum in uvdata_unique_nums
        ]
    )
    uvcal_unique_nums = np.unique(uvcal.ant_array)
    uvcal.antenna_names = np.asarray(uvcal.antenna_names)
    uvcal_used_antnames = np.array(
        [
            uvcal.antenna_names[np.where(uvcal.antenna_numbers == antnum)][0]
            for antnum in uvcal_unique_nums
        ]
    )

    ant_arr_match = uvcal_used_antnames.tolist() == uvdata_used_antnames.tolist()

    if not ant_arr_match:
        # check more carefully
        name_missing = []
        for this_ant_name in uvdata_used_antnames:
            wh_ant_match = np.nonzero(uvcal_used_antnames == this_ant_name)
            if wh_ant_match[0].size == 0:
                name_missing.append(this_ant_name)

        if len(name_missing) > 0:
            if len(name_missing) == uvdata_used_antnames.size:
                # all antenna_names with data on UVData are missing on UVCal.
                if not ant_check:
                    warnings.warn(
                        "All antenna names with data on UVData are missing "
                        "on UVCal. Since ant_check is False, calibration will "
                        "proceed but all data will be flagged."
                    )
                else:
                    raise ValueError(
                        "All antenna names with data on UVData are missing "
                        "on UVCal. To continue with calibration "
                        "(and flag all the data), set ant_check=False."
                    )
            else:
                # Only some antenna_names with data on UVData are missing on UVCal
                if not ant_check:
                    warnings.warn(
                        f"Antennas {name_missing} have data on UVData but are missing "
                        "on UVCal. Since ant_check is False, calibration will "
                        "proceed and the data for these antennas will be flagged."
                    )
                else:
                    raise ValueError(
                        f"Antennas {name_missing} have data on UVData but "
                        "are missing on UVCal. To continue calibration and "
                        "flag the data from missing antennas, set ant_check=False."
                    )

    uvdata_times = np.unique(uvdata.time_array)
    downselect_cal_times = False
    if uvcal.Ntimes > 1:
        if uvcal.Ntimes < uvdata.Ntimes:
            raise ValueError(
                "The uvcal object has more than one time but fewer than the "
                "number of unique times on the uvdata object."
            )
        uvcal_times = np.unique(uvcal.time_array)
        try:
            time_arr_match = np.allclose(
                uvcal_times,
                uvdata_times,
                atol=uvdata._time_array.tols[1],
                rtol=uvdata._time_array.tols[0],
            )
        except ValueError:
            time_arr_match = False

        if not time_arr_match:
            # check more carefully
            uvcal_times_to_keep = []
            for this_time in uvdata_times:
                wh_time_match = np.nonzero(
                    np.isclose(
                        uvcal.time_array - this_time,
                        0,
                        atol=uvdata._time_array.tols[1],
                        rtol=uvdata._time_array.tols[0],
                    )
                )
                if wh_time_match[0].size > 0:
                    uvcal_times_to_keep.append(uvcal.time_array[wh_time_match][0])
                else:
                    raise ValueError(
                        f"Time {this_time} exists on UVData but not on UVCal."
                    )
            if len(uvcal_times_to_keep) < uvcal.Ntimes:
                downselect_cal_times = True

    elif uvcal.time_range is None:
        # only one UVCal time, no time_range.
        # This cannot match if UVData.Ntimes > 1.
        # If they are both NTimes = 1, then check if they're close.
        if uvdata.Ntimes > 1 or not np.isclose(
            uvdata_times,
            uvcal.time_array,
            atol=uvdata._time_array.tols[1],
            rtol=uvdata._time_array.tols[0],
        ):
            if not time_check:
                warnings.warn(
                    "Times do not match between UVData and UVCal "
                    "but time_check is False, so calibration "
                    "will be applied anyway."
                )
            else:
                raise ValueError(
                    "Times do not match between UVData and UVCal. "
                    "Set time_check=False to apply calibration anyway."
                )
    else:
        # time_array is length 1 and time_range exists: check uvdata_times in time_range
        if (
            np.min(uvdata_times) < uvcal.time_range[0]
            or np.max(uvdata_times) > uvcal.time_range[1]
        ):
            if not time_check:
                warnings.warn(
                    "Times do not match between UVData and UVCal "
                    "but time_check is False, so calibration "
                    "will be applied anyway."
                )
            else:
                raise ValueError(
                    "Times do not match between UVData and UVCal. "
                    "Set time_check=False to apply calibration anyway. "
                )

    downselect_cal_freq = False
    if uvdata.future_array_shapes:
        uvdata_freq_arr_use = uvdata.freq_array
    else:
        uvdata_freq_arr_use = uvdata.freq_array[0, :]
    if uvcal.future_array_shapes:
        uvcal_freq_arr_use = uvcal.freq_array
    else:
        uvcal_freq_arr_use = uvcal.freq_array[0, :]
    try:
        freq_arr_match = np.allclose(
            np.sort(uvcal_freq_arr_use),
            np.sort(uvdata_freq_arr_use),
            atol=uvdata._freq_array.tols[1],
            rtol=uvdata._freq_array.tols[0],
        )
    except ValueError:
        freq_arr_match = False

    if freq_arr_match is False:
        # check more carefully
        uvcal_freqs_to_keep = []
        for this_freq in uvdata_freq_arr_use:
            wh_freq_match = np.nonzero(
                np.isclose(
                    uvcal.freq_array - this_freq,
                    0,
                    atol=uvdata._freq_array.tols[1],
                    rtol=uvdata._freq_array.tols[0],
                )
            )
            if wh_freq_match[0].size > 0:
                uvcal_freqs_to_keep.append(uvcal.freq_array[wh_freq_match][0])
            else:
                raise ValueError(
                    f"Frequency {this_freq} exists on UVData but not on UVCal."
                )
        if len(uvcal_freqs_to_keep) < uvcal.Nfreqs:
            downselect_cal_freq = True

    # check if uvdata.x_orientation isn't set (it's required for uvcal)
    uvd_x = uvdata.x_orientation
    if uvd_x is None:
        # use the uvcal x_orientation throughout
        uvd_x = uvcal.x_orientation
        warnings.warn(
            "UVData object does not have `x_orientation` specified but UVCal does. "
            "Matching based on `x` and `y` only "
        )

    uvdata_pol_strs = polnum2str(uvdata.polarization_array, x_orientation=uvd_x)
    uvcal_pol_strs = jnum2str(uvcal.jones_array, x_orientation=uvcal.x_orientation)
    uvdata_feed_pols = {
        feed for pol in uvdata_pol_strs for feed in POL_TO_FEED_DICT[pol]
    }
    for feed in uvdata_feed_pols:
        # get diagonal jones str
        jones_str = parse_jpolstr(feed, x_orientation=uvcal.x_orientation)
        if jones_str not in uvcal_pol_strs:
            raise ValueError(
                f"Feed polarization {feed} exists on UVData but not on UVCal. "
            )

    # downselect UVCal times, frequencies
    if downselect_cal_freq or downselect_cal_times:
        if not downselect_cal_times:
            uvcal_times_to_keep = None
        elif not downselect_cal_freq:
            uvcal_freqs_to_keep = None

        uvcal_use = uvcal.select(
            times=uvcal_times_to_keep, frequencies=uvcal_freqs_to_keep, inplace=False
        )

        new_uvcal = True
    else:
        uvcal_use = uvcal
        new_uvcal = False

    # input checks
    if uvcal_use.cal_type == "delay":
        if not new_uvcal:
            # make a copy to convert to gain
            uvcal_use = uvcal_use.copy()
            new_uvcal = True
        uvcal_use.convert_to_gain(delay_convention=delay_convention)

    # D-term calibration
    if Dterm_cal:
        # check for D-terms
        if -7 not in uvcal_use.jones_array and -8 not in uvcal_use.jones_array:
            raise ValueError(
                "Cannot apply D-term calibration without -7 or -8"
                "Jones polarization in uvcal object."
            )
        raise NotImplementedError("D-term calibration is not yet implemented.")

    # No D-term calibration
    else:
        # key is number, value is name
        uvdata_ant_dict = dict(zip(uvdata.antenna_numbers, uvdata.antenna_names))
        # opposite: key is name, value is number
        uvcal_ant_dict = dict(zip(uvcal.antenna_names, uvcal.antenna_numbers))

        # iterate over keys
        for key in uvdata.get_antpairpols():
            # get indices for this key
            blt_inds = uvdata.antpair2ind(key)
            pol_ind = np.argmin(
                np.abs(uvdata.polarization_array - polstr2num(key[2], uvd_x))
            )

            # try to get gains for each antenna
            ant1_num = key[0]
            ant2_num = key[1]

            feed1, feed2 = POL_TO_FEED_DICT[key[2]]
            try:
                uvcal_ant1_num = uvcal_ant_dict[uvdata_ant_dict[ant1_num]]
            except KeyError:
                uvcal_ant1_num = None
            try:
                uvcal_ant2_num = uvcal_ant_dict[uvdata_ant_dict[ant2_num]]
            except KeyError:
                uvcal_ant2_num = None

            uvcal_key1 = (uvcal_ant1_num, feed1)
            uvcal_key2 = (uvcal_ant2_num, feed2)
            if (uvcal_ant1_num is None or uvcal_ant2_num is None) or not (
                uvcal_use._has_key(*uvcal_key1) and uvcal_use._has_key(*uvcal_key2)
            ):
                if uvdata.future_array_shapes:
                    uvdata.flag_array[blt_inds, :, pol_ind] = True
                else:
                    uvdata.flag_array[blt_inds, 0, :, pol_ind] = True
                continue
            if flip_gain_conj:
                gain = (
                    np.conj(uvcal_use.get_gains(uvcal_key1))
                    * uvcal_use.get_gains(uvcal_key2)
                ).T  # tranpose to match uvdata shape
            else:
                gain = (
                    uvcal_use.get_gains(uvcal_key1)
                    * np.conj(uvcal_use.get_gains(uvcal_key2))
                ).T  # tranpose to match uvdata shape
            flag = (uvcal_use.get_flags(uvcal_key1) | uvcal_use.get_flags(uvcal_key2)).T

            # propagate flags
            if prop_flags:
                mask = np.isclose(gain, 0.0) | flag
                gain[mask] = 1.0
                if uvdata.future_array_shapes:
                    uvdata.flag_array[blt_inds, :, pol_ind] += mask
                else:
                    uvdata.flag_array[blt_inds, 0, :, pol_ind] += mask

            # apply to data
            mult_gains = uvcal_use.gain_convention == "multiply"
            if undo:
                mult_gains = not mult_gains
            if uvdata.future_array_shapes:
                if mult_gains:
                    uvdata.data_array[blt_inds, :, pol_ind] *= gain
                else:
                    uvdata.data_array[blt_inds, :, pol_ind] /= gain
            else:
                if mult_gains:
                    uvdata.data_array[blt_inds, 0, :, pol_ind] *= gain
                else:
                    uvdata.data_array[blt_inds, 0, :, pol_ind] /= gain

    # update attributes
    uvdata.history += "\nCalibrated with pyuvdata.utils.uvcalibrate."
    if undo:
        uvdata.vis_units = "uncalib"
    else:
        if uvcal_use.gain_scale is not None:
            uvdata.vis_units = uvcal_use.gain_scale

    if not inplace:
        return uvdata


def apply_uvflag(
    uvd, uvf, inplace=True, unflag_first=False, flag_missing=True, force_pol=True
):
    """
    Apply flags from a UVFlag to a UVData instantiation.

    Note that if uvf.Nfreqs or uvf.Ntimes is 1, it will broadcast flags across
    that axis.

    Parameters
    ----------
    uvd : UVData object
        UVData object to add flags to.
    uvf : UVFlag object
        A UVFlag object in flag mode.
    inplace : bool
        If True overwrite flags in uvd, otherwise return new object
    unflag_first : bool
        If True, completely unflag the UVData before applying flags.
        Else, OR the inherent uvd flags with uvf flags.
    flag_missing : bool
        If input uvf is a baseline type and antpairs in uvd do not exist in uvf,
        flag them in uvd. Otherwise leave them untouched.
    force_pol : bool
        If True, broadcast flags to all polarizations if they do not match.
        Only works if uvf.Npols == 1.

    Returns
    -------
    UVData
        If not inplace, returns new UVData object with flags applied

    """
    # assertions
    if uvf.mode != "flag":
        raise ValueError("UVFlag must be flag mode")

    if not inplace:
        uvd = uvd.copy()

    # make a deepcopy by default b/c it is generally edited inplace downstream
    uvf = uvf.copy()

    # convert to baseline type
    if uvf.type != "baseline":
        # edits inplace
        uvf.to_baseline(uvd, force_pol=force_pol)

    else:
        # make sure polarizations match or force_pol
        uvd_pols, uvf_pols = (
            uvd.polarization_array.tolist(),
            uvf.polarization_array.tolist(),
        )
        if set(uvd_pols) != set(uvf_pols):
            if uvf.Npols == 1 and force_pol:
                # if uvf is 1pol we can make them match: also edits inplace
                uvf.polarization_array = uvd.polarization_array
                uvf.Npols = len(uvf.polarization_array)
                uvf_pols = uvf.polarization_array.tolist()

            else:
                raise ValueError("Input uvf and uvd polarizations do not match")

        # make sure polarization ordering is correct: also edits inplace
        uvf.polarization_array = uvf.polarization_array[
            [uvd_pols.index(pol) for pol in uvf_pols]
        ]

    # check time and freq shapes match: if Ntimes or Nfreqs is 1, allow
    # implicit broadcasting
    if uvf.Ntimes == 1:
        mismatch_times = False
    elif uvf.Ntimes == uvd.Ntimes:
        tdiff = np.unique(uvf.time_array) - np.unique(uvd.time_array)
        mismatch_times = np.any(tdiff > np.max(np.abs(uvf._time_array.tols)))
    else:
        mismatch_times = True
    if mismatch_times:
        raise ValueError("UVFlag and UVData have mismatched time arrays.")

    if uvf.Nfreqs == 1:
        mismatch_freqs = False
    elif uvf.Nfreqs == uvd.Nfreqs:
        fdiff = np.unique(uvf.freq_array) - np.unique(uvd.freq_array)
        mismatch_freqs = np.any(fdiff > np.max(np.abs(uvf._freq_array.tols)))
    else:
        mismatch_freqs = True
    if mismatch_freqs:
        raise ValueError("UVFlag and UVData have mismatched frequency arrays.")

    # unflag if desired
    if unflag_first:
        uvd.flag_array[:] = False

    # iterate over antpairs and apply flags: TODO need to be able to handle
    # conjugated antpairs
    uvf_antpairs = uvf.get_antpairs()
    for ap in uvd.get_antpairs():
        uvd_ap_inds = uvd.antpair2ind(ap)
        if ap not in uvf_antpairs:
            if flag_missing:
                uvd.flag_array[uvd_ap_inds] = True
            continue
        uvf_ap_inds = uvf.antpair2ind(*ap)
        # addition of boolean is OR
        if uvd.future_array_shapes:
            uvd.flag_array[uvd_ap_inds] += uvf.flag_array[uvf_ap_inds, 0, :, :]
        else:
            uvd.flag_array[uvd_ap_inds] += uvf.flag_array[uvf_ap_inds]

    uvd.history += "\nFlagged with pyuvdata.utils.apply_uvflags."

    if not inplace:
        return uvd


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

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

    Parameters
    ----------
    uv : UVBase Object
        A UVBased object that supports the following functions and parameters:
        - get_ants
        - get_antpairs
        - get_pols
        These are used to construct the baseline ant_pair_nums
        and polarizations returned.
    ant_str : str
        String containing antenna information to parse. Can be 'all',
        'auto', 'cross', or combinations of antenna numbers and polarization
        indicators 'l' and 'r' or 'x' and 'y'.  Minus signs can also be used
        in front of an antenna number or baseline to exclude it from being
        output in ant_pairs_nums. If ant_str has a minus sign as the first
        character, 'all,' will be appended to the beginning of the string.
        See the tutorial for examples of valid strings and their behavior.
    print_toggle : bool
        Boolean for printing parsed baselines for a visual user check.
    x_orientation : str, optional
        Orientation of the physical dipole corresponding to what is
        labelled as the x polarization ("east" or "north") to allow for
        converting from E/N strings. If input uv object has an `x_orientation`
        parameter and the input to this function is `None`, the value from the
        object will be used. Any input given to this function will override the
        value on the uv object. See corresonding parameter on UVData
        for more details.

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

    """
    required_attrs = ["get_ants", "get_antpairs", "get_pols"]
    if not all(hasattr(uv, attr) for attr in required_attrs):
        raise ValueError(
            "UVBased objects must have all the following attributes in order "
            f"to call 'parse_ants': {required_attrs}."
        )

    if x_orientation is None and (
        hasattr(uv, "x_orientation") and uv.x_orientation is not None
    ):
        x_orientation = uv.x_orientation

    ant_re = r"(\(((-?\d+[lrxy]?,?)+)\)|-?\d+[lrxy]?)"
    bl_re = "(^(%s_%s|%s),?)" % (ant_re, ant_re, ant_re)
    str_pos = 0
    ant_pairs_nums = []
    polarizations = []
    ants_data = uv.get_ants()
    ant_pairs_data = uv.get_antpairs()
    pols_data = uv.get_pols()
    warned_ants = []
    warned_pols = []

    if ant_str.startswith("-"):
        ant_str = "all," + ant_str

    while str_pos < len(ant_str):
        m = re.search(bl_re, ant_str[str_pos:])
        if m is None:
            if ant_str[str_pos:].upper().startswith("ALL"):
                if len(ant_str[str_pos:].split(",")) > 1:
                    ant_pairs_nums = uv.get_antpairs()
            elif ant_str[str_pos:].upper().startswith("AUTO"):
                for pair in ant_pairs_data:
                    if pair[0] == pair[1] and pair not in ant_pairs_nums:
                        ant_pairs_nums.append(pair)
            elif ant_str[str_pos:].upper().startswith("CROSS"):
                for pair in ant_pairs_data:
                    if not (pair[0] == pair[1] or pair in ant_pairs_nums):
                        ant_pairs_nums.append(pair)
            elif ant_str[str_pos:].upper().startswith("PI"):
                polarizations.append(polstr2num("pI"))
            elif ant_str[str_pos:].upper().startswith("PQ"):
                polarizations.append(polstr2num("pQ"))
            elif ant_str[str_pos:].upper().startswith("PU"):
                polarizations.append(polstr2num("pU"))
            elif ant_str[str_pos:].upper().startswith("PV"):
                polarizations.append(polstr2num("pV"))
            else:
                raise ValueError("Unparsible argument {s}".format(s=ant_str))

            comma_cnt = ant_str[str_pos:].find(",")
            if comma_cnt >= 0:
                str_pos += comma_cnt + 1
            else:
                str_pos = len(ant_str)
        else:
            m = m.groups()
            str_pos += len(m[0])
            if m[2] is None:
                ant_i_list = [m[8]]
                ant_j_list = list(uv.get_ants())
            else:
                if m[3] is None:
                    ant_i_list = [m[2]]
                else:
                    ant_i_list = m[3].split(",")

                if m[6] is None:
                    ant_j_list = [m[5]]
                else:
                    ant_j_list = m[6].split(",")

            for ant_i in ant_i_list:
                include_i = True
                if type(ant_i) == str and ant_i.startswith("-"):
                    ant_i = ant_i[1:]  # nibble the - off the string
                    include_i = False

                for ant_j in ant_j_list:
                    include_j = True
                    if type(ant_j) == str and ant_j.startswith("-"):
                        ant_j = ant_j[1:]
                        include_j = False

                    pols = None
                    ant_i, ant_j = str(ant_i), str(ant_j)
                    if not ant_i.isdigit():
                        ai = re.search(r"(\d+)([x,y,l,r])", ant_i).groups()

                    if not ant_j.isdigit():
                        aj = re.search(r"(\d+)([x,y,l,r])", ant_j).groups()

                    if ant_i.isdigit() and ant_j.isdigit():
                        ai = [ant_i, ""]
                        aj = [ant_j, ""]
                    elif ant_i.isdigit() and not ant_j.isdigit():
                        if "x" in ant_j or "y" in ant_j:
                            pols = ["x" + aj[1], "y" + aj[1]]
                        else:
                            pols = ["l" + aj[1], "r" + aj[1]]
                        ai = [ant_i, ""]
                    elif not ant_i.isdigit() and ant_j.isdigit():
                        if "x" in ant_i or "y" in ant_i:
                            pols = [ai[1] + "x", ai[1] + "y"]
                        else:
                            pols = [ai[1] + "l", ai[1] + "r"]
                        aj = [ant_j, ""]
                    elif not ant_i.isdigit() and not ant_j.isdigit():
                        pols = [ai[1] + aj[1]]

                    ant_tuple = (abs(int(ai[0])), abs(int(aj[0])))

                    # Order tuple according to order in object
                    if ant_tuple in ant_pairs_data:
                        pass
                    elif ant_tuple[::-1] in ant_pairs_data:
                        ant_tuple = ant_tuple[::-1]
                    else:
                        if not (
                            ant_tuple[0] in ants_data or ant_tuple[0] in warned_ants
                        ):
                            warned_ants.append(ant_tuple[0])
                        if not (
                            ant_tuple[1] in ants_data or ant_tuple[1] in warned_ants
                        ):
                            warned_ants.append(ant_tuple[1])
                        if pols is not None:
                            for pol in pols:
                                if not (pol.lower() in pols_data or pol in warned_pols):
                                    warned_pols.append(pol)
                        continue

                    if include_i and include_j:
                        if ant_tuple not in ant_pairs_nums:
                            ant_pairs_nums.append(ant_tuple)
                        if pols is not None:
                            for pol in pols:
                                if (
                                    pol.lower() in pols_data
                                    and polstr2num(pol, x_orientation=x_orientation)
                                    not in polarizations
                                ):
                                    polarizations.append(
                                        polstr2num(pol, x_orientation=x_orientation)
                                    )
                                elif not (
                                    pol.lower() in pols_data or pol in warned_pols
                                ):
                                    warned_pols.append(pol)
                    else:
                        if pols is not None:
                            for pol in pols:
                                if pol.lower() in pols_data:
                                    if uv.Npols == 1 and [pol.lower()] == pols_data:
                                        ant_pairs_nums.remove(ant_tuple)
                                    if (
                                        polstr2num(pol, x_orientation=x_orientation)
                                        in polarizations
                                    ):
                                        polarizations.remove(
                                            polstr2num(
                                                pol, x_orientation=x_orientation,
                                            )
                                        )
                                elif not (
                                    pol.lower() in pols_data or pol in warned_pols
                                ):
                                    warned_pols.append(pol)
                        elif ant_tuple in ant_pairs_nums:
                            ant_pairs_nums.remove(ant_tuple)

    if ant_str.upper() == "ALL":
        ant_pairs_nums = None
    elif len(ant_pairs_nums) == 0:
        if not ant_str.upper() in ["AUTO", "CROSS"]:
            ant_pairs_nums = None

    if len(polarizations) == 0:
        polarizations = None
    else:
        polarizations.sort(reverse=True)

    if print_toggle:
        print("\nParsed antenna pairs:")
        if ant_pairs_nums is not None:
            for pair in ant_pairs_nums:
                print(pair)

        print("\nParsed polarizations:")
        if polarizations is not None:
            for pol in polarizations:
                print(polnum2str(pol, x_orientation=x_orientation))

    if len(warned_ants) > 0:
        warnings.warn(
            "Warning: Antenna number {a} passed, but not present "
            "in the ant_1_array or ant_2_array".format(
                a=(",").join(map(str, warned_ants))
            )
        )

    if len(warned_pols) > 0:
        warnings.warn(
            "Warning: Polarization {p} is not present in "
            "the polarization_array".format(p=(",").join(warned_pols).upper())
        )

    return ant_pairs_nums, polarizations


def _combine_filenames(filename1, filename2):
    """Combine the filename attribute from multiple UVBase objects.

    The 4 cases are:
    1. `filename1` has been set, `filename2` has not
    2. `filename1` has not been set, `filename2` has
    3. `filename1` and `filename2` both have been set
    4. `filename1` and `filename2` both have not been set
    In case (1), we do not want to update the attribute, because it is
    already set correctly. In case (2), we want to replace `filename1`
    with the value from `filename2. In case (3), we want to take the union of
    the sets of the filenames. In case (4), we want the filename attribute
    to still be `None`.

    Parameters
    ----------
    filename1 : list of str or None
        The list of filenames for the first UVBase object. If it is not set, it
        should be `None`.
    filename2 : list of str or None
        The list of filenames for the second UVData object. If it is not set, it
        should be `None`.

    Returns
    -------
    combined_filenames : list of str or None
        The combined list, with potentially duplicate entries removed.
    """
    combined_filenames = filename1
    if filename1 is not None:
        if filename2 is not None:
            combined_filenames = sorted(set(filename1).union(set(filename2)))
    elif filename2 is not None:
        combined_filenames = filename2

    return combined_filenames


def _get_dset_shape(dset, indices):
    """
    Given a 3-tuple of indices, determine the indexed array shape.

    Parameters
    ----------
    dset : numpy array or h5py dataset
        A numpy array or a reference to an HDF5 dataset on disk. Requires the
        `dset.shape` attribute exists and returns a tuple.
    indices : tuple
        A 3-tuple with the indices to extract along each dimension of dset.
        Each element should contain a list of indices, a slice element,
        or a list of slice elements that will be concatenated after slicing.
        For data arrays with 4 dimensions, the second dimension (the old spw axis)
        should not be included because it can only be length one.

    Returns
    -------
    tuple
        a 3- or 4-tuple with the shape of the indexed array
    tuple
        a 3- or 4-tuple with indices used (will be different than input if dset has
        4 dimensions)
    """
    dset_shape = list(dset.shape)
    if len(dset_shape) == 4 and len(indices) == 3:
        indices = (indices[0], np.s_[:], indices[1], indices[2])

    for i, inds in enumerate(indices):
        # check for integer
        if isinstance(inds, (int, np.integer)):
            dset_shape[i] = 1
        # check for slice object
        if isinstance(inds, slice):
            dset_shape[i] = _get_slice_len(inds, dset_shape[i])
        # check for list
        if isinstance(inds, list):
            # check for list of integers
            if isinstance(inds[0], (int, np.integer)):
                dset_shape[i] = len(inds)
            elif isinstance(inds[0], slice):
                dset_shape[i] = sum((_get_slice_len(s, dset_shape[i]) for s in inds))

    return dset_shape, indices


def _convert_to_slices(indices, max_nslice_frac=0.1):
    """
    Convert list of indices to a list of slices.

    Parameters
    ----------
    indices : list
        A 1D list of integers for array indexing.
    max_nslice_frac : float
        A float from 0 -- 1. If the number of slices
        needed to represent input 'indices' divided by len(indices)
        exceeds this fraction, then we determine that we cannot
        easily represent 'indices' with a list of slices.

    Returns
    -------
    list
        list of slice objects used to represent indices
    bool
        If True, indices is easily represented by slices
        (max_nslice_frac condition met), otherwise False

    Notes
    -----
    Example:
        if: indices = [1, 2, 3, 4, 10, 11, 12, 13, 14]
        then: slices = [slice(1, 5, 1), slice(11, 15, 1)]
    """
    # check for integer index
    if isinstance(indices, (int, np.integer)):
        indices = [indices]
    # check for already a slice
    if isinstance(indices, slice):
        return [indices], True
    # assert indices is longer than 2, or return trivial solutions
    if len(indices) == 0:
        return [slice(0, 0, 0)], False
    elif len(indices) == 1:
        return [slice(indices[0], indices[0] + 1, 1)], True
    elif len(indices) == 2:
        return [slice(indices[0], indices[1] + 1, indices[1] - indices[0])], True

    # setup empty slices list
    Ninds = len(indices)
    slices = []

    # iterate over indices
    for i, ind in enumerate(indices):
        if i == 0:
            # start the first slice object
            start = ind
            last_step = indices[i + 1] - ind
            continue

        # calculate step from previous index
        step = ind - indices[i - 1]

        # if step != last_step, this ends the slice
        if step != last_step:
            # append to list
            slices.append(slice(start, indices[i - 1] + 1, last_step))

            # check if this is the last element
            if i == Ninds - 1:
                # append last element
                slices.append(slice(ind, ind + 1, 1))
                continue

            # setup next step
            start = ind
            last_step = indices[i + 1] - ind

        # check if this is the last element
        elif i == Ninds - 1:
            # end slice and append
            slices.append(slice(start, ind + 1, step))

    # determine whether slices are a reasonable representation
    Nslices = len(slices)
    passed = (float(Nslices) / len(indices)) < max_nslice_frac

    return slices, passed


def _get_slice_len(s, axlen):
    """
    Get length of a slice s into array of len axlen.

    Parameters
    ----------
    s : slice object
        Slice object to index with
    axlen : int
        Length of axis s slices into

    Returns
    -------
    int
        Length of slice object
    """
    if s.start is None:
        start = 0
    else:
        start = s.start
    if s.stop is None:
        stop = axlen
    else:
        stop = np.min([s.stop, axlen])
    if s.step is None:
        step = 1
    else:
        step = s.step

    return ((stop - 1 - start) // step) + 1


def _index_dset(dset, indices, input_array=None):
    """
    Index a UVH5 data, flags or nsamples h5py dataset.

    Parameters
    ----------
    dset : h5py dataset
        A reference to an HDF5 dataset on disk.
    indices : tuple
        A 3-tuple with the indices to extract along each dimension of dset.
        Each element should contain a list of indices, a slice element,
        or a list of slice elements that will be concatenated after slicing.
        Indices must be provided such that all dimensions can be indexed
        simultaneously. For data arrays with 4 dimensions, the second dimension
        (the old spw axis) should not be included because it can only be length one.

    Returns
    -------
    ndarray
        The indexed dset

    Notes
    -----
    This makes and fills an empty array with dset indices.
    For trivial indexing, (e.g. a trivial slice), constructing
    a new array and filling it is suboptimal over direct
    indexing, e.g. dset[indices].
    This function specializes in repeated slices over the same axis,
    e.g. if indices is [[slice(0, 5), slice(10, 15), ...], ..., ]
    """
    # get dset and arr shape
    dset_shape = dset.shape
    arr_shape, indices = _get_dset_shape(dset, indices)

    if input_array is None:
        # create empty array of dset dtype
        arr = np.empty(arr_shape, dtype=dset.dtype)
    else:
        arr = input_array

    # get arr and dset indices for each dimension in indices
    dset_indices = []
    arr_indices = []
    for i, dset_inds in enumerate(indices):
        if isinstance(dset_inds, (int, np.integer)):
            # this dimension is len 1, so slice is fine
            arr_indices.append([slice(None)])
            dset_indices.append([[dset_inds]])

        elif isinstance(dset_inds, slice):
            # this dimension is just a slice, so slice is fine
            arr_indices.append([slice(None)])
            dset_indices.append([dset_inds])

        elif isinstance(dset_inds, (list, np.ndarray)):
            if isinstance(dset_inds[0], (int, np.integer)):
                # this is a list of integers, append slice
                arr_indices.append([slice(None)])
                dset_indices.append([dset_inds])
            elif isinstance(dset_inds[0], slice):
                # this is a list of slices, need list of slice lens
                slens = [_get_slice_len(s, dset_shape[i]) for s in dset_inds]
                ssums = [sum(slens[:j]) for j in range(len(slens))]
                arr_inds = [slice(s, s + l) for s, l in zip(ssums, slens)]
                arr_indices.append(arr_inds)
                dset_indices.append(dset_inds)

    if len(dset_shape) == 3:
        freq_dim = 1
        pol_dim = 2
    else:
        freq_dim = 2
        pol_dim = 3

    # iterate over each of the 3 axes and fill the array
    for blt_arr, blt_dset in zip(arr_indices[0], dset_indices[0]):
        for freq_arr, freq_dset in zip(arr_indices[freq_dim], dset_indices[freq_dim]):
            for pol_arr, pol_dset in zip(arr_indices[pol_dim], dset_indices[pol_dim]):
                if input_array is None:
                    # index dset and assign to arr
                    if len(dset_shape) == 3:
                        arr[blt_arr, freq_arr, pol_arr] = dset[
                            blt_dset, freq_dset, pol_dset
                        ]
                    else:
                        arr[blt_arr, :, freq_arr, pol_arr] = dset[
                            blt_dset, :, freq_dset, pol_dset
                        ]
                else:
                    # index arr and assign to dset
                    if len(dset_shape) == 3:
                        dset[blt_dset, freq_dset, pol_dset] = arr[
                            blt_arr, freq_arr, pol_arr
                        ]
                    else:
                        dset[blt_dset, :, freq_dset, pol_dset] = arr[
                            blt_arr, :, freq_arr, pol_arr
                        ]

    if input_array is None:
        return arr
    else:
        return


# pyuvdata/uvbase.py

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

"""
Base class for objects with UVParameter attributes.

Subclassed by UVData and Telescope.
"""
import copy
import warnings

import numpy as np
from astropy.units import Quantity

from . import parameter as uvp
from .utils import _get_iterable
from . import __version__

__all__ = ["UVBase"]


def _warning(msg, *a, **kwargs):
    """Improve the printing of user warnings."""
    return str(msg) + "\n"


class UVBase(object):
    """
    Base class for objects with UVParameter attributes.

    This class is intended to be subclassed and its init method should be
    called in the subclass init after all associated UVParameter attributes are
    defined. The init method of this base class creates properties
    (named using UVParameter.name) from all the UVParameter attributes on the subclass.
    AngleParameter and LocationParameter attributes also have extra convenience
    properties defined:

    AngleParameter:

        UVParameter.name+'_degrees'

    LocationParameter:

        UVParameter.name+'_lat_lon_alt'
        UVParameter.name+'_lat_lon_alt_degrees'
    """

    def _setup_parameters(self):
        """Set up parameter objects to be able to be referenced by their names."""
        # set any UVParameter attributes to be properties
        for p in self:
            this_param = getattr(self, p)
            attr_name = this_param.name
            setattr(
                self.__class__,
                attr_name,
                property(self.prop_fget(p), self.prop_fset(p)),
            )
            if isinstance(this_param, uvp.AngleParameter):
                setattr(
                    self.__class__,
                    attr_name + "_degrees",
                    property(self.degree_prop_fget(p), self.degree_prop_fset(p)),
                )
            elif isinstance(this_param, uvp.LocationParameter):
                setattr(
                    self.__class__,
                    attr_name + "_lat_lon_alt",
                    property(
                        self.lat_lon_alt_prop_fget(p), self.lat_lon_alt_prop_fset(p)
                    ),
                )
                setattr(
                    self.__class__,
                    attr_name + "_lat_lon_alt_degrees",
                    property(
                        self.lat_lon_alt_degrees_prop_fget(p),
                        self.lat_lon_alt_degrees_prop_fset(p),
                    ),
                )
        return

    def __init__(self):
        """Create properties from UVParameter attributes."""
        warnings.formatwarning = _warning

        self._setup_parameters()

        # String to add to history of any files written with this version of pyuvdata
        self.pyuvdata_version_str = (
            f"  Read/written with pyuvdata version: {__version__ }."
        )

    def __setstate__(self, state):
        """Set the state of the object from given input state."""
        self.__dict__ = state
        self._setup_parameters()

    def prop_fget(self, param_name):
        """Getter method for UVParameter properties."""
        # Create function to return
        def fget(self):
            this_param = getattr(self, param_name)
            return this_param.value

        return fget

    def prop_fset(self, param_name):
        """Setter method for UVParameter properties."""
        # Create function to return
        def fset(self, value):
            this_param = getattr(self, param_name)
            this_param.value = value
            setattr(self, param_name, this_param)

        return fset

    def degree_prop_fget(self, param_name):
        """Degree getter method for AngleParameter properties."""
        # Create function to return
        def fget(self):
            this_param = getattr(self, param_name)
            return this_param.degrees()

        return fget

    def degree_prop_fset(self, param_name):
        """Degree setter method for AngleParameter properties."""
        # Create function to return
        def fset(self, value):
            this_param = getattr(self, param_name)
            this_param.set_degrees(value)
            setattr(self, param_name, this_param)

        return fset

    def lat_lon_alt_prop_fget(self, param_name):
        """Lat/lon/alt getter method for LocationParameter properties."""
        # Create function to return
        def fget(self):
            this_param = getattr(self, param_name)
            return this_param.lat_lon_alt()

        return fget

    def lat_lon_alt_prop_fset(self, param_name):
        """Lat/lon/alt setter method for LocationParameter properties."""
        # Create function to return
        def fset(self, value):
            this_param = getattr(self, param_name)
            this_param.set_lat_lon_alt(value)
            setattr(self, param_name, this_param)

        return fset

    def lat_lon_alt_degrees_prop_fget(self, param_name):
        """Lat/lon/alt degree getter method for LocationParameter properties."""
        # Create function to return
        def fget(self):
            this_param = getattr(self, param_name)
            return this_param.lat_lon_alt_degrees()

        return fget

    def lat_lon_alt_degrees_prop_fset(self, param_name):
        """Lat/lon/alt degree setter method for LocationParameter properties."""
        # Create function to return
        def fset(self, value):
            this_param = getattr(self, param_name)
            this_param.set_lat_lon_alt_degrees(value)
            setattr(self, param_name, this_param)

        return fset

    def __iter__(self, uvparams_only=True):
        """Iterate over all UVParameter attributes."""
        attribute_list = [
            a
            for a in dir(self)
            if not a.startswith("__") and not callable(getattr(self, a))
        ]
        param_list = []
        for a in attribute_list:
            attr = getattr(self, a)
            if uvparams_only:
                if isinstance(attr, uvp.UVParameter):
                    param_list.append(a)
            else:
                param_list.append(a)
        for a in param_list:
            yield a

    def required(self):
        """Iterate over all required UVParameter attributes."""
        attribute_list = [
            a
            for a in dir(self)
            if not a.startswith("__") and not callable(getattr(self, a))
        ]
        required_list = []
        for a in attribute_list:
            attr = getattr(self, a)
            if isinstance(attr, uvp.UVParameter):
                if attr.required:
                    required_list.append(a)
        for a in required_list:
            yield a

    def extra(self):
        """Iterate over all non-required UVParameter attributes."""
        attribute_list = [
            a
            for a in dir(self)
            if not a.startswith("__") and not callable(getattr(self, a))
        ]
        extra_list = []
        for a in attribute_list:
            attr = getattr(self, a)
            if isinstance(attr, uvp.UVParameter):
                if not attr.required:
                    extra_list.append(a)
        for a in extra_list:
            yield a

    def __eq__(self, other, check_extra=True, allowed_failures=("filename",)):
        """
        Test if classes match and parameters are equal.

        Parameters
        ----------
        other : class
            Other class instance to check
        check_extra : bool
            Option to specify whether to include all parameters, or just the
            required ones. Default is True.
        allowed_failures : iterable of str, optional
            List or tuple of parameter names that are allowed to fail while
            still passing an overall equality check. These should only include
            optional parameters. By default, the `filename` parameter will be
            ignored.

        Returns
        -------
        bool
            Whether the two instances are equivalent.
        """
        if isinstance(other, self.__class__):
            # only check that required parameters are identical
            self_required = []
            other_required = []
            for param in self.required():
                self_required.append(param)
            for param in other.required():
                other_required.append(param)
            if set(self_required) != set(other_required):
                print(
                    "Sets of required parameters do not match. "
                    f"Left is {self_required},"
                    f" right is {other_required}."
                )
                return False

            if check_extra:
                self_extra = []
                other_extra = []
                for param in self.extra():
                    self_extra.append(param)
                for param in other.extra():
                    other_extra.append(param)
                if set(self_extra) != set(other_extra):
                    print(
                        "Sets of extra parameters do not match. "
                        f"Left is {self_extra},"
                        f" right is {other_extra}."
                    )
                    return False
                p_check = self_required + self_extra
            else:
                p_check = self_required

            if allowed_failures is not None:
                if isinstance(allowed_failures, str):
                    # convert a single string into a length-1 list
                    allowed_failures = [allowed_failures]
                if isinstance(allowed_failures, tuple):
                    # convert a tuple into a list
                    allowed_failures = list(allowed_failures)

                for i, param in enumerate(allowed_failures):
                    if not param.startswith("_"):
                        param = "_" + param
                        allowed_failures[i] = param
                    if param not in self_required:
                        if param in p_check:
                            p_check.remove(param)

            p_equal = True
            for param in p_check:
                self_param = getattr(self, param)
                other_param = getattr(other, param)
                if self_param != other_param:
                    print(
                        f"parameter {param} does not match. Left is "
                        f"{self_param.value}, right is {other_param.value}."
                    )
                    p_equal = False

            if allowed_failures is not None:
                for param in allowed_failures:
                    if hasattr(self, param):
                        self_param = getattr(self, param)
                        other_param = getattr(other, param)
                        if self_param != other_param:
                            print(
                                f"parameter {param} does not match, but is not "
                                "required to for equality. Left is "
                                f"{self_param.value}, right is {other_param.value}."
                            )

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

    def __ne__(self, other):
        """Not equal."""
        return not self.__eq__(other)

    def check(
        self, check_extra=True, run_check_acceptability=True, ignore_requirements=False
    ):
        """
        Check that required parameters exist and have the correct shapes.

        Optionally, check that the values are acceptable.

        Parameters
        ----------
        check_extra : bool
            If true, check shapes and values on all parameters,
            otherwise only check required parameters.
        run_check_acceptability : bool
            Option to check if values in parameters are acceptable.
        ignore_requirements : bool
            Do not error if a required parameter isn't set.
            This allows the user to run the shape/acceptability checks
            on parameters in a partially-defined UVData object.

        """
        if check_extra:
            p_check = list(self.required()) + list(self.extra())
        else:
            p_check = list(self.required())

        for p in p_check:
            param = getattr(self, p)
            # Check required parameter exists
            if param.value is None:
                if ignore_requirements:
                    continue
                if param.required is True:
                    raise ValueError(f"Required UVParameter {p} has not been set.")
            else:
                # Check parameter shape
                eshape = param.expected_shape(self)
                # default value of eshape is ()
                if eshape == "str" or (eshape == () and param.expected_type == "str"):
                    # Check that it's a string
                    if not isinstance(param.value, str):
                        raise ValueError(
                            f"UVParameter {p} expected to be string, but is not."
                        )
                else:
                    # Check the shape of the parameter value. Note that np.shape
                    # returns an empty tuple for single numbers.
                    # eshape should do the same.
                    if not np.shape(param.value) == eshape:
                        raise ValueError(
                            "UVParameter {param} is not expected shape. "
                            "Parameter shape is {pshape}, expected shape is "
                            "{eshape}.".format(
                                param=p, pshape=np.shape(param.value), eshape=eshape
                            )
                        )
                    # Quantity objects complicate things slightly
                    # Do a separate check with warnings until a quantity based
                    # parameter value is created
                    if isinstance(param.value, Quantity):
                        # check if user put expected type as a type of quantity
                        # not a more generic type of number.
                        if any(
                            issubclass(param_type, Quantity)
                            for param_type in _get_iterable(param.expected_type)
                        ):
                            # Verify the param is an instance
                            # of the specific Quantity type
                            if not isinstance(param.value, param.expected_type):
                                raise ValueError(
                                    f"UVParameter {p} is a Quantity object "
                                    "but not the appropriate type. "
                                    f"Is {type(param.value)} but "
                                    f"expected {param.expected_type}."
                                )
                            else:
                                # matches expected type
                                continue  # pragma: no cover
                        else:
                            # Expected type is not a Quantity subclass
                            # Assuming it is a data type like float, int, etc
                            # continuing with check below
                            warnings.warn(
                                f"Parameter {p} is a Quantity object, "
                                "but the expected type is a precision identifier: "
                                f"{param.expected_type}. "
                                "Testing the precision of the value, but this "
                                "check will fail in a future version."
                            )
                            check_vals = [param.value.item(0).value]

                    elif eshape == ():
                        # Single element
                        check_vals = [param.value]
                    else:
                        if isinstance(param.value, (list, tuple)):
                            # List & tuples needs to be handled differently than array
                            # list values may be different types, so they all
                            # need to be checked
                            check_vals = list(param.value)
                        else:
                            # numpy array
                            check_vals = [param.value.item(0)]

                    for val in check_vals:
                        if not isinstance(val, param.expected_type):
                            raise ValueError(
                                f"UVParameter {p} is not the appropriate"
                                f" type. Is:  {type(val)}. "
                                f"Should be: {param.expected_type}."
                            )

                if run_check_acceptability:
                    accept, message = param.check_acceptability()
                    if not accept:
                        raise ValueError(
                            f"UVParameter {p} has unacceptable values. {message}"
                        )

        return True

    def copy(self):
        """Make and return a copy of the object."""
        return copy.deepcopy(self)


# pyuvdata/uvbeam/__init__.py

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

"""Init file for UVBeam."""
from .uvbeam import *  # noqa


# pyuvdata/uvbeam/beamfits.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 beamfits files."""
import os
import warnings

import numpy as np
from astropy.io import fits

from .uvbeam import UVBeam
from .. import utils as uvutils

__all__ = ["BeamFITS"]

hpx_primary_ax_nums = {
    "pixel": 1,
    "freq": 2,
    "feed_pol": 3,
    "spw": 4,
    "basisvec": 5,
    "complex": 6,
}
reg_primary_ax_nums = {
    "img_ax1": 1,
    "img_ax2": 2,
    "freq": 3,
    "feed_pol": 4,
    "spw": 5,
    "basisvec": 6,
    "complex": 7,
}

hxp_basisvec_ax_nums = {"pixel": 1, "ncomp": 2, "basisvec": 3}
reg_basisvec_ax_nums = {"img_ax1": 1, "img_ax2": 2, "ncomp": 3, "basisvec": 4}

fits_axisname_dict = {
    "hpx_inds": "PIX_IND",
    "azimuth": "AZIMUTH",
    "zen_angle": "ZENANGLE",
    "zenorth_x": "ZENX-SIN",
    "zenorth_y": "ZENY-SIN",
}


class BeamFITS(UVBeam):
    """
    Defines a fits-specific subclass of UVBeam for reading and writing beamfits files.

    This class should not be interacted with directly, instead use the
    read_beamfits and write_beamfits methods on the UVBeam class.

    The beamfits format supports regularly gridded or healpix beam files.
    The format defined here for healpix beams is not compatible with true healpix
    formats because it needs to support multiple dimensions (e.g. polarization,
    frequency, efield vectors).
    """

    def read_beamfits(
        self, filename, run_check=True, check_extra=True, run_check_acceptability=True
    ):
        """
        Read the data from a beamfits file.

        Parameters
        ----------
        filename : str
            The beamfits file to read from.
        run_check : bool
            Option to check for the existence and proper shapes of
            required parameters after reading in the file.
        check_extra : bool
            Option to check optional parameters as well as required ones.
        run_check_acceptabilit : bool
            Option to check acceptable range of the values of
            required parameters after reading in the file.
        """
        # update filename attribute
        basename = os.path.basename(filename)
        self.filename = [basename]
        self._filename.form = (1,)

        with fits.open(filename) as fname:
            primary_hdu = fname[0]
            primary_header = primary_hdu.header.copy()
            hdunames = uvutils._fits_indexhdus(fname)  # find the rest of the tables

            data = primary_hdu.data

            # only support simple antenna_types for now.
            # support for phased arrays should be added
            self._set_simple()

            self.beam_type = primary_header.pop("BTYPE", None)
            if self.beam_type is not None:
                self.beam_type = self.beam_type.lower()
            else:
                bunit = primary_header.pop("BUNIT", None)
                if bunit is not None and bunit.lower().strip() == "jy/beam":
                    self.beam_type = "power"

            if self.beam_type == "intensity":
                self.beam_type = "power"

            n_dimensions = primary_header.pop("NAXIS")
            ctypes = [
                primary_header[ctype].lower()
                for ctype in (key for key in primary_header if "ctype" in key.lower())
            ]

            self.pixel_coordinate_system = primary_header.pop("COORDSYS", None)
            if self.pixel_coordinate_system is None:
                for cs, cs_dict in self.coordinate_system_dict.items():
                    ax_names = [
                        fits_axisname_dict[ax].lower() for ax in cs_dict["axes"]
                    ]
                    if ax_names == ctypes[0 : len(ax_names)]:
                        coord_list = ctypes[0 : len(ax_names)]
                        self.pixel_coordinate_system = cs
            else:
                ax_names = [
                    fits_axisname_dict[ax].lower()
                    for ax in self.coordinate_system_dict[self.pixel_coordinate_system][
                        "axes"
                    ]
                ]
                coord_list = ctypes[0 : len(ax_names)]
                if coord_list != ax_names:
                    raise ValueError(
                        "Coordinate axis list does not match coordinate system"
                    )

            if self.pixel_coordinate_system == "healpix":
                # get pixel values out of HPX_IND extension
                hpx_hdu = fname[hdunames["HPX_INDS"]]
                self.Npixels = hpx_hdu.header["NAXIS2"]
                hpx_data = hpx_hdu.data
                self.pixel_array = hpx_data["hpx_inds"]

                ax_nums = hpx_primary_ax_nums
                self.nside = primary_header.pop("NSIDE", None)
                self.ordering = primary_header.pop("ORDERING", None)
                data_Npixels = primary_header.pop("NAXIS" + str(ax_nums["pixel"]))
                if data_Npixels != self.Npixels:
                    raise ValueError(
                        "Number of pixels in HPX_IND extension does "
                        "not match number of pixels in data array"
                    )
            else:
                ax_nums = reg_primary_ax_nums
                self.Naxes1 = primary_header.pop("NAXIS" + str(ax_nums["img_ax1"]))
                self.Naxes2 = primary_header.pop("NAXIS" + str(ax_nums["img_ax2"]))

                self.axis1_array = uvutils._fits_gethduaxis(
                    primary_hdu, ax_nums["img_ax1"]
                )
                self.axis2_array = uvutils._fits_gethduaxis(
                    primary_hdu, ax_nums["img_ax2"]
                )

                # if units aren't defined they are degrees by FITS convention
                # convert degrees to radians because UVBeam uses radians
                axis1_units = primary_header.pop(
                    "CUNIT" + str(ax_nums["img_ax1"]), "deg"
                )
                if axis1_units == "deg":
                    self.axis1_array = np.deg2rad(self.axis1_array)
                elif axis1_units != "rad":
                    raise ValueError(
                        'Units of first axis array are not "deg" or "rad".'
                    )
                axis2_units = primary_header.pop(
                    "CUNIT" + str(ax_nums["img_ax2"]), "deg"
                )
                if axis2_units == "deg":
                    self.axis2_array = np.deg2rad(self.axis2_array)
                elif axis2_units != "rad":
                    raise ValueError(
                        'Units of second axis array are not "deg" or "rad".'
                    )

            n_efield_dims = max(ax_nums[key] for key in ax_nums)

            if self.beam_type == "power":
                # check for case where the data is complex (e.g. for xy beams)
                if n_dimensions > ax_nums["complex"] - 1:
                    complex_arrs = np.split(data, 2, axis=0)
                    self.data_array = np.squeeze(
                        complex_arrs[0] + 1j * complex_arrs[1], axis=0
                    )
                else:
                    self.data_array = data

                # Note: This axis is called STOKES by analogy with the equivalent
                # uvfits axis
                # However, this is confusing because it is NOT a true Stokes axis,
                #   it is really the polarization axis.
                if (
                    primary_header.pop("CTYPE" + str(ax_nums["feed_pol"]))
                    .lower()
                    .strip()
                    == "stokes"
                ):
                    self.Npols = primary_header.pop("NAXIS" + str(ax_nums["feed_pol"]))

                self.polarization_array = np.int32(
                    uvutils._fits_gethduaxis(primary_hdu, ax_nums["feed_pol"])
                )
                self._set_power()
            elif self.beam_type == "efield":
                self._set_efield()
                if n_dimensions < n_efield_dims:
                    raise ValueError(
                        "beam_type is efield and data dimensionality is too low"
                    )
                complex_arrs = np.split(data, 2, axis=0)
                self.data_array = np.squeeze(
                    complex_arrs[0] + 1j * complex_arrs[1], axis=0
                )
                if (
                    primary_header.pop("CTYPE" + str(ax_nums["feed_pol"]))
                    .lower()
                    .strip()
                    == "feedind"
                ):
                    self.Nfeeds = primary_header.pop("NAXIS" + str(ax_nums["feed_pol"]))
                feedlist = primary_header.pop("FEEDLIST", None)
                if feedlist is not None:
                    self.feed_array = np.array(feedlist[1:-1].split(", "))
            else:
                raise ValueError(
                    "Unknown beam_type: {type}, beam_type should be "
                    '"efield" or "power".'.format(type=self.beam_type)
                )

            self.data_normalization = primary_header.pop("NORMSTD", None)

            self.telescope_name = primary_header.pop("TELESCOP")
            self.feed_name = primary_header.pop("FEED", None)
            self.feed_version = primary_header.pop("FEEDVER", None)
            self.model_name = primary_header.pop("MODEL", None)
            self.model_version = primary_header.pop("MODELVER", None)
            self.x_orientation = primary_header.pop("XORIENT", None)
            self.interpolation_function = primary_header.pop("INTERPFN", None)
            self.freq_interp_kind = primary_header.pop("FINTERP", None)

            # shapes
            if (
                primary_header.pop("CTYPE" + str(ax_nums["freq"])).lower().strip()
                == "freq"
            ):
                self.Nfreqs = primary_header.pop("NAXIS" + str(ax_nums["freq"]))

            if n_dimensions > ax_nums["spw"] - 1:
                if (
                    primary_header.pop("CTYPE" + str(ax_nums["spw"])).lower().strip()
                    == "if"
                ):
                    self.Nspws = primary_header.pop("NAXIS" + str(ax_nums["spw"]), None)
                    # subtract 1 to be zero-indexed
                    self.spw_array = (
                        uvutils._fits_gethduaxis(primary_hdu, ax_nums["spw"]) - 1
                    )

            if n_dimensions > ax_nums["basisvec"] - 1:
                if (
                    primary_header.pop("CTYPE" + str(ax_nums["basisvec"]))
                    .lower()
                    .strip()
                    == "vecind"
                ):
                    self.Naxes_vec = primary_header.pop(
                        "NAXIS" + str(ax_nums["basisvec"]), None
                    )

            if (
                self.Nspws is None or self.Naxes_vec is None
            ) and self.beam_type == "power":
                if self.Nspws is None:
                    self.Nspws = 1
                    self.spw_array = np.array([0])
                if self.Naxes_vec is None:
                    self.Naxes_vec = 1

                # add extra empty dimensions to data_array as appropriate
                while len(self.data_array.shape) < n_efield_dims - 1:
                    self.data_array = np.expand_dims(self.data_array, axis=0)

            self.freq_array = uvutils._fits_gethduaxis(primary_hdu, ax_nums["freq"])
            self.freq_array.shape = (self.Nspws,) + self.freq_array.shape
            # default frequency axis is Hz, but check for corresonding CUNIT
            freq_units = primary_header.pop("CUNIT" + str(ax_nums["freq"]), "Hz")
            if freq_units != "Hz":
                freq_factor = {"kHz": 1e3, "MHz": 1e6, "GHz": 1e9}
                if freq_units in freq_factor.keys():
                    self.freq_array = self.freq_array * freq_factor[freq_units]
                else:
                    raise ValueError("Frequency units not recognized.")

            self.history = str(primary_header.get("HISTORY", ""))
            if not uvutils._check_history_version(
                self.history, self.pyuvdata_version_str
            ):
                self.history += self.pyuvdata_version_str

            self.extra_keywords = uvutils._get_fits_extra_keywords(primary_header)

            # read BASISVEC HDU if present
            if "BASISVEC" in hdunames:
                basisvec_hdu = fname[hdunames["BASISVEC"]]
                basisvec_header = basisvec_hdu.header
                self.basis_vector_array = basisvec_hdu.data

                if self.pixel_coordinate_system == "healpix":
                    basisvec_ax_nums = hxp_basisvec_ax_nums
                    if (
                        basisvec_header["CTYPE" + str(basisvec_ax_nums["pixel"])]
                        .lower()
                        .strip()
                        != "pix_ind"
                    ):
                        raise ValueError(
                            "First axis in BASISVEC HDU must be 'Pix_Ind' for "
                            "healpix beams"
                        )

                    basisvec_Npixels = basisvec_header.pop(
                        "NAXIS" + str(basisvec_ax_nums["pixel"])
                    )

                    if basisvec_Npixels != self.Npixels:
                        raise ValueError(
                            "Number of pixels in BASISVEC HDU does not match "
                            "primary HDU"
                        )
                else:
                    basisvec_ax_nums = reg_basisvec_ax_nums
                    basisvec_coord_list = [
                        basisvec_header[
                            "CTYPE" + str(basisvec_ax_nums["img_ax1"])
                        ].lower(),
                        basisvec_header[
                            "CTYPE" + str(basisvec_ax_nums["img_ax2"])
                        ].lower(),
                    ]
                    basisvec_axis1_array = uvutils._fits_gethduaxis(
                        basisvec_hdu, basisvec_ax_nums["img_ax1"]
                    )
                    basisvec_axis2_array = uvutils._fits_gethduaxis(
                        basisvec_hdu, basisvec_ax_nums["img_ax2"]
                    )

                    # if units aren't defined they are degrees by FITS convention
                    # convert degrees to radians because UVBeam uses radians
                    axis1_units = basisvec_header.pop(
                        "CUNIT" + str(basisvec_ax_nums["img_ax1"]), "deg"
                    )
                    if axis1_units == "deg":
                        basisvec_axis1_array = np.deg2rad(basisvec_axis1_array)
                    elif axis1_units != "rad":
                        raise ValueError(
                            "Units of first axis array in BASISVEC HDU are not"
                            " 'deg' or 'rad'."
                        )
                    axis2_units = basisvec_header.pop(
                        "CUNIT" + str(basisvec_ax_nums["img_ax2"]), "deg"
                    )
                    if axis2_units == "deg":
                        basisvec_axis2_array = np.deg2rad(basisvec_axis2_array)
                    elif axis2_units != "rad":
                        raise ValueError(
                            "Units of second axis array in BASISVEC HDU are not"
                            " 'deg' or 'rad'."
                        )

                    if not np.all(basisvec_axis1_array == self.axis1_array):
                        raise ValueError(
                            "First image axis in BASISVEC HDU does not match "
                            "primary HDU"
                        )
                    if not np.all(basisvec_axis2_array == self.axis2_array):
                        raise ValueError(
                            "Second image axis in BASISVEC HDU does not "
                            "match primary HDU"
                        )
                    if basisvec_coord_list != coord_list:
                        raise ValueError(
                            "Pixel coordinate list in BASISVEC HDU does not "
                            "match primary HDU"
                        )

                basisvec_Naxes_vec = basisvec_header[
                    "NAXIS" + str(basisvec_ax_nums["basisvec"])
                ]
                self.Ncomponents_vec = basisvec_header[
                    "NAXIS" + str(basisvec_ax_nums["ncomp"])
                ]

                basisvec_cs = basisvec_header["COORDSYS"]
                if basisvec_cs != self.pixel_coordinate_system:
                    raise ValueError(
                        "Pixel coordinate system in BASISVEC HDU does "
                        "not match primary HDU"
                    )

                if basisvec_Naxes_vec != self.Naxes_vec:
                    raise ValueError(
                        "Number of vector coordinate axes in BASISVEC "
                        "HDU does not match primary HDU"
                    )

            # check to see if BANDPARM HDU exists and read it out if it does
            if "BANDPARM" in hdunames:
                bandpass_hdu = fname[hdunames["BANDPARM"]]
                bandpass_header = bandpass_hdu.header.copy()
                self.reference_impedance = bandpass_header.pop("ref_imp", None)

                freq_data = bandpass_hdu.data
                columns = [c.name for c in freq_data.columns]
                self.bandpass_array = freq_data["bandpass"]
                self.bandpass_array = self.bandpass_array[np.newaxis, :]

                if "rx_temp" in columns:
                    self.receiver_temperature_array = freq_data["rx_temp"]
                    self.receiver_temperature_array = self.receiver_temperature_array[
                        np.newaxis, :
                    ]
                if "loss" in columns:
                    self.loss_array = freq_data["loss"]
                    self.loss_array = self.loss_array[np.newaxis, :]
                if "mismatch" in columns:
                    self.mismatch_array = freq_data["mismatch"]
                    self.mismatch_array = self.mismatch_array[np.newaxis, :]
                if "s11" in columns:
                    s11 = freq_data["s11"]
                    s12 = freq_data["s12"]
                    s21 = freq_data["s21"]
                    s22 = freq_data["s22"]
                    self.s_parameters = np.zeros((4, 1, len(s11)))
                    self.s_parameters[0, 0, :] = s11
                    self.s_parameters[1, 0, :] = s12
                    self.s_parameters[2, 0, :] = s21
                    self.s_parameters[3, 0, :] = s22
            else:
                # no bandpass information, set it to an array of ones
                self.bandpass_array = np.zeros((self.Nspws, self.Nfreqs)) + 1.0

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

    def write_beamfits(
        self,
        filename,
        run_check=True,
        check_extra=True,
        run_check_acceptability=True,
        clobber=False,
    ):
        """
        Write the data to a beamfits file.

        Parameters
        ----------
        filename : str
            The beamfits file to write to.
        run_check : bool
            Option to check for the existence and proper shapes of
            required 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
            required parameters before writing the file.
        clobber : bool
            Option to overwrite the filename if the file already exists.

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

        if self.antenna_type != "simple":
            raise ValueError(
                "This beam fits writer currently only supports "
                "simple (rather than phased array) antenna beams"
            )

        if self.Nfreqs > 1:
            freq_spacing = self.freq_array[0, 1:] - self.freq_array[0, :-1]
            if not np.isclose(
                np.min(freq_spacing),
                np.max(freq_spacing),
                rtol=self._freq_array.tols[0],
                atol=self._freq_array.tols[1],
            ):
                raise ValueError(
                    "The frequencies are not evenly spaced (probably "
                    "because of a select operation). The beamfits format "
                    "does not support unevenly spaced frequencies."
                )
            freq_spacing = freq_spacing[0]
        else:
            freq_spacing = 1

        if self.pixel_coordinate_system == "healpix":
            ax_nums = hpx_primary_ax_nums
        else:
            ax_nums = reg_primary_ax_nums
            if self.Naxes1 > 1:
                axis1_spacing = np.diff(self.axis1_array)
                if not np.isclose(
                    np.min(axis1_spacing),
                    np.max(axis1_spacing),
                    rtol=self._axis1_array.tols[0],
                    atol=self._axis1_array.tols[1],
                ):
                    raise ValueError(
                        "The pixels are not evenly spaced along first axis. "
                        "The beam fits format does not support "
                        "unevenly spaced pixels."
                    )
                axis1_spacing = axis1_spacing[0]
            else:
                axis1_spacing = 1

            if self.Naxes2 > 1:
                axis2_spacing = np.diff(self.axis2_array)
                if not np.isclose(
                    np.min(axis2_spacing),
                    np.max(axis2_spacing),
                    rtol=self._axis2_array.tols[0],
                    atol=self._axis2_array.tols[1],
                ):
                    raise ValueError(
                        "The pixels are not evenly spaced along second axis. "
                        "The beam fits format does not support "
                        "unevenly spaced pixels."
                    )
                axis2_spacing = axis2_spacing[0]
            else:
                axis2_spacing = 1

        primary_header = fits.Header()

        # Conforming to fits format
        primary_header["SIMPLE"] = True
        primary_header["BITPIX"] = 32

        primary_header["BTYPE"] = self.beam_type
        primary_header["NORMSTD"] = self.data_normalization
        primary_header["COORDSYS"] = self.pixel_coordinate_system

        # metadata
        primary_header["TELESCOP"] = self.telescope_name
        primary_header["FEED"] = self.feed_name
        primary_header["FEEDVER"] = self.feed_version
        primary_header["MODEL"] = self.model_name
        primary_header["MODELVER"] = self.model_version

        if self.x_orientation is not None:
            primary_header["XORIENT"] = self.x_orientation

        if self.interpolation_function is not None:
            primary_header["INTERPFN"] = (
                self.interpolation_function,
                "interpolation function",
            )

        if self.freq_interp_kind is not None:
            primary_header["FINTERP"] = (
                self.freq_interp_kind,
                "frequency " "interpolation kind (scipy interp1d)",
            )

        if self.beam_type == "efield":
            primary_header["FEEDLIST"] = "[" + ", ".join(self.feed_array) + "]"

        if self.pixel_coordinate_system == "healpix":
            primary_header["NSIDE"] = self.nside
            primary_header["ORDERING"] = self.ordering

            # set up pixel axis
            primary_header["CTYPE" + str(ax_nums["pixel"])] = (
                "Pix_Ind",
                "Index into pixel array in HPX_INDS extension.",
            )
            primary_header["CRVAL" + str(ax_nums["pixel"])] = 1
            primary_header["CRPIX" + str(ax_nums["pixel"])] = 1
            primary_header["CDELT" + str(ax_nums["pixel"])] = 1

        else:
            # set up first image axis
            # FITs standard is to use degrees (not radians as in the UVBeam object)
            deg_axis1_array = np.rad2deg(self.axis1_array)
            deg_axis1_spacing = np.rad2deg(axis1_spacing)
            primary_header["CTYPE" + str(ax_nums["img_ax1"])] = fits_axisname_dict[
                self.coordinate_system_dict[self.pixel_coordinate_system]["axes"][0]
            ]
            primary_header["CRVAL" + str(ax_nums["img_ax1"])] = deg_axis1_array[0]
            primary_header["CRPIX" + str(ax_nums["img_ax1"])] = 1
            primary_header["CDELT" + str(ax_nums["img_ax1"])] = deg_axis1_spacing
            primary_header["CUNIT" + str(ax_nums["img_ax1"])] = "deg"

            # set up second image axis
            deg_axis2_array = np.rad2deg(self.axis2_array)
            deg_axis2_spacing = np.rad2deg(axis2_spacing)
            primary_header["CTYPE" + str(ax_nums["img_ax2"])] = fits_axisname_dict[
                self.coordinate_system_dict[self.pixel_coordinate_system]["axes"][1]
            ]
            primary_header["CRVAL" + str(ax_nums["img_ax2"])] = deg_axis2_array[0]
            primary_header["CRPIX" + str(ax_nums["img_ax2"])] = 1
            primary_header["CDELT" + str(ax_nums["img_ax2"])] = deg_axis2_spacing
            primary_header["CUNIT" + str(ax_nums["img_ax2"])] = "deg"

        # set up frequency axis
        primary_header["CTYPE" + str(ax_nums["freq"])] = "FREQ"
        primary_header["CUNIT" + str(ax_nums["freq"])] = "Hz"
        primary_header["CRVAL" + str(ax_nums["freq"])] = self.freq_array[0, 0]
        primary_header["CRPIX" + str(ax_nums["freq"])] = 1
        primary_header["CDELT" + str(ax_nums["freq"])] = freq_spacing

        # set up feed or pol axis
        if self.beam_type == "power":
            if self.Npols > 1:
                pol_spacing = np.diff(self.polarization_array)
                if np.min(pol_spacing) < np.max(pol_spacing):
                    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 = pol_spacing[0]
            else:
                pol_spacing = 1

            # Note: This axis is called STOKES by analogy with the equivalent
            # uvfits axis
            # However, this is confusing because it is NOT a true Stokes axis,
            #   it is really the polarization axis.
            primary_header["CTYPE" + str(ax_nums["feed_pol"])] = (
                "STOKES",
                "Polarization integers, see uvbeam memo",
            )
            primary_header[
                "CRVAL" + str(ax_nums["feed_pol"])
            ] = self.polarization_array[0]
            primary_header["CDELT" + str(ax_nums["feed_pol"])] = pol_spacing

            # handle case where data_array is complex (e.g. for xy beams)
            if np.iscomplexobj(self.data_array):
                primary_data = np.concatenate(
                    [
                        np.expand_dims(self.data_array.real, axis=0),
                        np.expand_dims(self.data_array.imag, axis=0),
                    ],
                    axis=0,
                )
            else:
                primary_data = self.data_array
        elif self.beam_type == "efield":
            primary_header["CTYPE" + str(ax_nums["feed_pol"])] = (
                "FEEDIND",
                'Feed: index into "FEEDLIST".',
            )
            primary_header["CRVAL" + str(ax_nums["feed_pol"])] = 1
            primary_header["CDELT" + str(ax_nums["feed_pol"])] = 1

            primary_data = np.concatenate(
                [
                    np.expand_dims(self.data_array.real, axis=0),
                    np.expand_dims(self.data_array.imag, axis=0),
                ],
                axis=0,
            )
        else:
            raise ValueError(
                "Unknown beam_type: {type}, beam_type should be "
                '"efield" or "power".'.format(type=self.beam_type)
            )
        primary_header["CRPIX" + str(ax_nums["feed_pol"])] = 1

        # set up spw axis
        primary_header["CTYPE" + str(ax_nums["spw"])] = (
            "IF",
            "Spectral window number.",
        )
        primary_header["CUNIT" + str(ax_nums["spw"])] = "Integer"
        primary_header["CRVAL" + str(ax_nums["spw"])] = 1
        primary_header["CRPIX" + str(ax_nums["spw"])] = 1
        primary_header["CDELT" + str(ax_nums["spw"])] = 1

        # set up basis vector axis
        primary_header["CTYPE" + str(ax_nums["basisvec"])] = (
            "VECIND",
            "Basis vector index.",
        )
        primary_header["CUNIT" + str(ax_nums["basisvec"])] = "Integer"
        primary_header["CRVAL" + str(ax_nums["basisvec"])] = 1
        primary_header["CRPIX" + str(ax_nums["basisvec"])] = 1
        primary_header["CDELT" + str(ax_nums["basisvec"])] = 1

        if np.iscomplexobj(self.data_array):
            # set up complex axis
            primary_header["CTYPE" + str(ax_nums["complex"])] = (
                "COMPLEX",
                "real, imaginary",
            )
            primary_header["CRVAL" + str(ax_nums["complex"])] = 1
            primary_header["CRPIX" + str(ax_nums["complex"])] = 1
            primary_header["CDELT" + str(ax_nums["complex"])] = 1

        # end standard keywords; begin user-defined keywords
        for key, value in self.extra_keywords.items():
            # header keywords have to be 8 characters or less
            if len(str(key)) > 8:
                warnings.warn(
                    "key {key} in extra_keywords is longer than 8 "
                    "characters. It will be truncated to 8 as required "
                    "by the 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():
                    primary_header.add_comment(line)
            else:
                primary_header[keyword] = value

        for line in self.history.splitlines():
            primary_header.add_history(line)

        primary_hdu = fits.PrimaryHDU(data=primary_data, header=primary_header)
        hdulist = fits.HDUList([primary_hdu])

        if self.basis_vector_array is not None:
            basisvec_header = fits.Header()
            basisvec_header["EXTNAME"] = "BASISVEC"
            basisvec_header["COORDSYS"] = self.pixel_coordinate_system

            if self.pixel_coordinate_system == "healpix":
                basisvec_ax_nums = hxp_basisvec_ax_nums

                # set up pixel axis
                basisvec_header["CTYPE" + str(basisvec_ax_nums["pixel"])] = (
                    "Pix_Ind",
                    "Index into pixel array in HPX_INDS extension.",
                )
                basisvec_header["CRVAL" + str(basisvec_ax_nums["pixel"])] = 1
                basisvec_header["CRPIX" + str(basisvec_ax_nums["pixel"])] = 1
                basisvec_header["CDELT" + str(basisvec_ax_nums["pixel"])] = 1

            else:
                basisvec_ax_nums = reg_basisvec_ax_nums

                # set up first image axis
                basisvec_header[
                    "CTYPE" + str(basisvec_ax_nums["img_ax1"])
                ] = fits_axisname_dict[
                    self.coordinate_system_dict[self.pixel_coordinate_system]["axes"][0]
                ]
                basisvec_header[
                    "CRVAL" + str(basisvec_ax_nums["img_ax1"])
                ] = deg_axis1_array[0]
                basisvec_header["CRPIX" + str(basisvec_ax_nums["img_ax1"])] = 1
                basisvec_header[
                    "CDELT" + str(basisvec_ax_nums["img_ax1"])
                ] = deg_axis1_spacing
                basisvec_header["CUNIT" + str(basisvec_ax_nums["img_ax1"])] = "deg"

                # set up second image axis
                basisvec_header[
                    "CTYPE" + str(basisvec_ax_nums["img_ax2"])
                ] = fits_axisname_dict[
                    self.coordinate_system_dict[self.pixel_coordinate_system]["axes"][1]
                ]
                basisvec_header[
                    "CRVAL" + str(basisvec_ax_nums["img_ax2"])
                ] = deg_axis2_array[0]
                basisvec_header["CRPIX" + str(basisvec_ax_nums["img_ax2"])] = 1
                basisvec_header[
                    "CDELT" + str(basisvec_ax_nums["img_ax2"])
                ] = deg_axis2_spacing
                basisvec_header["CUNIT" + str(basisvec_ax_nums["img_ax2"])] = "deg"

            # set up vector component axis (length Ncomponents_vec)
            basisvec_header["CTYPE" + str(basisvec_ax_nums["ncomp"])] = (
                "COMPIND",
                "Vector component index",
            )
            basisvec_header["CUNIT" + str(basisvec_ax_nums["ncomp"])] = "Integer"
            basisvec_header["CRVAL" + str(basisvec_ax_nums["ncomp"])] = 1
            basisvec_header["CRPIX" + str(basisvec_ax_nums["ncomp"])] = 1
            basisvec_header["CDELT" + str(basisvec_ax_nums["ncomp"])] = 1

            # set up vector coordinate system axis (length Naxis_vec)
            basisvec_header["CTYPE" + str(basisvec_ax_nums["basisvec"])] = (
                "VECCOORD",
                "Basis vector index",
            )
            basisvec_header["CUNIT" + str(basisvec_ax_nums["basisvec"])] = "Integer"
            basisvec_header["CRVAL" + str(basisvec_ax_nums["basisvec"])] = 1
            basisvec_header["CRPIX" + str(basisvec_ax_nums["basisvec"])] = 1
            basisvec_header["CDELT" + str(basisvec_ax_nums["basisvec"])] = 1

            basisvec_data = self.basis_vector_array
            basisvec_hdu = fits.ImageHDU(data=basisvec_data, header=basisvec_header)
            hdulist.append(basisvec_hdu)

        if self.pixel_coordinate_system == "healpix":
            # make healpix pixel number column. 'K' format indicates 64-bit integer
            c1 = fits.Column(name="hpx_inds", format="K", array=self.pixel_array)
            coldefs = fits.ColDefs([c1])
            hpx_hdu = fits.BinTableHDU.from_columns(coldefs)
            hpx_hdu.header["EXTNAME"] = "HPX_INDS"
            hdulist.append(hpx_hdu)

        # check for frequency-specific optional arrays. If they're not None,
        # add them to the BANDPARM binary table HDU along with the bandpass_array
        bandpass_col = fits.Column(
            name="bandpass", format="D", array=self.bandpass_array[0, :]
        )
        col_list = [bandpass_col]

        if self.receiver_temperature_array is not None:
            rx_temp_col = fits.Column(
                name="rx_temp", format="D", array=self.receiver_temperature_array[0, :]
            )
            col_list.append(rx_temp_col)
        if self.loss_array is not None:
            loss_col = fits.Column(name="loss", format="D", array=self.loss_array[0, :])
            col_list.append(loss_col)
        if self.mismatch_array is not None:
            mismatch_col = fits.Column(
                name="mismatch", format="D", array=self.mismatch_array[0, :]
            )
            col_list.append(mismatch_col)
        if self.s_parameters is not None:
            s11_col = fits.Column(
                name="s11", format="D", array=self.s_parameters[0, 0, :]
            )
            s12_col = fits.Column(
                name="s12", format="D", array=self.s_parameters[1, 0, :]
            )
            s21_col = fits.Column(
                name="s21", format="D", array=self.s_parameters[2, 0, :]
            )
            s22_col = fits.Column(
                name="s22", format="D", array=self.s_parameters[3, 0, :]
            )
            col_list += [s11_col, s12_col, s21_col, s22_col]

        coldefs = fits.ColDefs(col_list)
        bandpass_hdu = fits.BinTableHDU.from_columns(coldefs)
        bandpass_hdu.header["EXTNAME"] = "BANDPARM"
        if self.reference_impedance is not None:
            bandpass_hdu.header["ref_imp"] = self.reference_impedance
        hdulist.append(bandpass_hdu)

        hdulist.writeto(filename, overwrite=clobber)
        hdulist.close()


# pyuvdata/uvbeam/cst_beam.py

# -*- mode: python; coding: utf-8 -*-
# Copyright (c) 2018 Radio Astronomy Software Group
# Licensed under the 2-clause BSD License
"""Class for reading beam CST files."""
import os
import re
import warnings

import numpy as np

from .uvbeam import UVBeam
from .. import utils as uvutils

__all__ = ["CSTBeam"]


class CSTBeam(UVBeam):
    """
    Defines a CST-specific subclass of UVBeam for reading CST text files.

    This class should not be interacted with directly, instead use the
    read_cst_beam method on the UVBeam class.

    """

    def name2freq(self, fname):
        """
        Extract frequency from the filename.

        Assumes the file name contains a substring with the frequency channel
        in MHz that the data represents.
        e.g. "HERA_Sim_120.87MHz.txt" should yield 120.87e6

        Parameters
        ----------
        fname : str
            Filename to parse.

        Returns
        -------
        float
            Frequency extracted from filename in Hz.
        """
        fi = fname.rfind("Hz")
        frequency = float(re.findall(r"\d*\.\d+|\d+", fname[:fi])[-1])

        si_prefix = fname[fi - 1]
        si_dict = {"k": 1e3, "M": 1e6, "G": 1e9}
        if si_prefix in si_dict.keys():
            frequency = frequency * si_dict[si_prefix]

        return frequency

    def read_cst_beam(
        self,
        filename,
        beam_type="power",
        feed_pol="x",
        rotate_pol=True,
        frequency=None,
        telescope_name=None,
        feed_name=None,
        feed_version=None,
        model_name=None,
        model_version=None,
        history="",
        x_orientation=None,
        reference_impedance=None,
        extra_keywords=None,
        run_check=True,
        check_extra=True,
        run_check_acceptability=True,
    ):
        """
        Read in data from a cst file.

        Parameters
        ----------
        filename : str
            The cst file to read from.
        beam_type : str
            What beam_type to read in ('power' or 'efield').
        feed_pol : str
            The feed or polarization or list of feeds or polarizations the
            files correspond to.
            Defaults to 'x' (meaning x for efield or xx for power beams).
        rotate_pol : bool
            If True, assume the structure in the simulation is symmetric under
            90 degree rotations about the z-axis (so that the y polarization can be
            constructed by rotating the x polarization or vice versa).
            Default: True if feed_pol is a single value or a list with all
            the same values in it, False if it is a list with varying values.
        frequency : float or list of float
            The frequency or list of frequencies corresponding to the filename(s).
            This is assumed to be in the same order as the files.
            If not passed, the code attempts to parse it from the filenames.
        telescope_name : str
            The name of the telescope corresponding to the filename(s).
        feed_name : str
            The name of the feed corresponding to the filename(s).
        feed_version : str
            The version of the feed corresponding to the filename(s).
        model_name : str
            The name of the model corresponding to the filename(s).
        model_version : str
            The version of the model corresponding to the filename(s).
        history : str
            A string detailing the history of the filename(s).
        x_orientation : str, optional
            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)
        reference_impedance : float, optional
            The reference impedance of the model(s).
        extra_keywords : dict, optional
            A dictionary containing any extra_keywords.
        run_check : bool
            Option to check for the existence and proper shapes of
            required parameters after reading in the file.
        check_extra : bool
            Option to check optional parameters as well as
            required ones.
        run_check_acceptability : bool
            Option to check acceptable range of the values of
            required parameters after reading in the file.

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

        self.telescope_name = telescope_name
        self.feed_name = feed_name
        self.feed_version = feed_version
        self.model_name = model_name
        self.model_version = model_version
        self.history = history
        if not uvutils._check_history_version(self.history, self.pyuvdata_version_str):
            self.history += self.pyuvdata_version_str

        if x_orientation is not None:
            self.x_orientation = x_orientation
        if reference_impedance is not None:
            self.reference_impedance = float(reference_impedance)
        if extra_keywords is not None:
            self.extra_keywords = extra_keywords

        if beam_type == "power":
            self.Naxes_vec = 1

            if feed_pol == "x":
                feed_pol = "xx"
            elif feed_pol == "y":
                feed_pol = "yy"

            if rotate_pol:
                rot_pol_dict = {"xx": "yy", "yy": "xx", "xy": "yx", "yx": "xy"}
                pol2 = rot_pol_dict[feed_pol]
                self.polarization_array = np.array(
                    [uvutils.polstr2num(feed_pol), uvutils.polstr2num(pol2)]
                )
            else:
                self.polarization_array = np.array([uvutils.polstr2num(feed_pol)])

            self.Npols = len(self.polarization_array)
            self._set_power()
        else:
            self.Naxes_vec = 2
            self.Ncomponents_vec = 2
            if rotate_pol:
                if feed_pol == "x":
                    self.feed_array = np.array(["x", "y"])
                else:
                    self.feed_array = np.array(["y", "x"])
            else:
                if feed_pol == "x":
                    self.feed_array = np.array(["x"])
                else:
                    self.feed_array = np.array(["y"])
            self.Nfeeds = self.feed_array.size
            self._set_efield()

        self.data_normalization = "physical"
        self.antenna_type = "simple"

        self.Nfreqs = 1
        self.Nspws = 1
        self.freq_array = np.zeros((self.Nspws, self.Nfreqs))
        self.bandpass_array = np.zeros((self.Nspws, self.Nfreqs))

        self.spw_array = np.array([0])
        self.pixel_coordinate_system = "az_za"
        self._set_cs_params()

        out_file = open(filename, "r")
        line = out_file.readline().strip()  # Get the first line
        out_file.close()
        raw_names = line.split("]")
        raw_names = [raw_name for raw_name in raw_names if not raw_name == ""]
        column_names = []
        units = []
        for raw_name in raw_names:
            column_name, unit = tuple(raw_name.split("["))
            column_names.append("".join(column_name.lower().split(" ")))
            units.append(unit.lower().strip())

        data = np.loadtxt(filename, skiprows=2)

        theta_col = np.where(np.array(column_names) == "theta")[0][0]
        phi_col = np.where(np.array(column_names) == "phi")[0][0]

        if "deg" in units[theta_col]:
            theta_data = np.radians(data[:, theta_col])
        else:
            theta_data = data[:, theta_col]
        if "deg" in units[phi_col]:
            phi_data = np.radians(data[:, phi_col])
        else:
            phi_data = data[:, phi_col]

        theta_axis = np.sort(np.unique(theta_data))
        phi_axis = np.sort(np.unique(phi_data))
        if not theta_axis.size * phi_axis.size == theta_data.size:
            raise ValueError("Data does not appear to be on a grid")

        theta_data = theta_data.reshape((theta_axis.size, phi_axis.size), order="F")
        phi_data = phi_data.reshape((theta_axis.size, phi_axis.size), order="F")

        delta_theta = np.diff(theta_axis)
        if not np.isclose(np.max(delta_theta), np.min(delta_theta)):
            raise ValueError(
                "Data does not appear to be regularly gridded in zenith angle"
            )
        delta_theta = delta_theta[0]

        delta_phi = np.diff(phi_axis)
        if not np.isclose(np.max(delta_phi), np.min(delta_phi)):
            raise ValueError(
                "Data does not appear to be regularly gridded in azimuth angle"
            )
        delta_phi = delta_phi[0]

        self.axis1_array = phi_axis
        self.Naxes1 = self.axis1_array.size
        self.axis2_array = theta_axis
        self.Naxes2 = self.axis2_array.size

        if self.beam_type == "power":
            # type depends on whether cross pols are present
            # (if so, complex, else float)
            self.data_array = np.zeros(
                self._data_array.expected_shape(self),
                dtype=self._data_array.expected_type,
            )
        else:
            self.data_array = np.zeros(
                self._data_array.expected_shape(self), dtype=np.complex128
            )

        if frequency is not None:
            self.freq_array[0] = frequency
        else:
            self.freq_array[0] = self.name2freq(filename)

        if rotate_pol:
            # for second polarization, rotate by pi/2
            rot_phi = phi_data + np.pi / 2
            rot_phi[np.where(rot_phi >= 2 * np.pi)] -= 2 * np.pi
            roll_rot_phi = np.roll(rot_phi, int((np.pi / 2) / delta_phi), axis=1)
            if not np.allclose(roll_rot_phi, phi_data):
                raise ValueError("Rotating by pi/2 failed")

            # theta is not affected by the rotation

        # get beam
        if self.beam_type == "power":

            data_col_enum = ["abs(e)", "abs(v)"]
            data_col = []
            for name in data_col_enum:
                this_col = np.where(np.array(column_names) == name)[0]
                if this_col.size > 0:
                    data_col = data_col + this_col.tolist()
            if len(data_col) == 0:
                raise ValueError("No power column found in file: {}".format(filename))
            elif len(data_col) > 1:
                raise ValueError(
                    "Multiple possible power columns found in file: {}".format(filename)
                )
            data_col = data_col[0]
            power_beam1 = (
                data[:, data_col].reshape((theta_axis.size, phi_axis.size), order="F")
                ** 2.0
            )

            self.data_array[0, 0, 0, 0, :, :] = power_beam1

            if rotate_pol:
                # rotate by pi/2 for second polarization
                power_beam2 = np.roll(power_beam1, int((np.pi / 2) / delta_phi), axis=1)
                self.data_array[0, 0, 1, 0, :, :] = power_beam2
        else:
            self.basis_vector_array = np.zeros(
                (self.Naxes_vec, self.Ncomponents_vec, self.Naxes2, self.Naxes1)
            )
            self.basis_vector_array[0, 0, :, :] = 1.0
            self.basis_vector_array[1, 1, :, :] = 1.0

            theta_mag_col = np.where(np.array(column_names) == "abs(theta)")[0][0]
            theta_phase_col = np.where(np.array(column_names) == "phase(theta)")[0][0]
            phi_mag_col = np.where(np.array(column_names) == "abs(phi)")[0][0]
            phi_phase_col = np.where(np.array(column_names) == "phase(phi)")[0][0]

            theta_mag = data[:, theta_mag_col].reshape(
                (theta_axis.size, phi_axis.size), order="F"
            )
            phi_mag = data[:, phi_mag_col].reshape(
                (theta_axis.size, phi_axis.size), order="F"
            )
            if "deg" in units[theta_phase_col]:
                theta_phase = np.radians(data[:, theta_phase_col])
            else:
                theta_phase = data[:, theta_phase_col]
            if "deg" in units[phi_phase_col]:
                phi_phase = np.radians(data[:, phi_phase_col])
            else:
                phi_phase = data[:, phi_phase_col]
            theta_phase = theta_phase.reshape(
                (theta_axis.size, phi_axis.size), order="F"
            )
            phi_phase = phi_phase.reshape((theta_axis.size, phi_axis.size), order="F")

            theta_beam = theta_mag * np.exp(1j * theta_phase)
            phi_beam = phi_mag * np.exp(1j * phi_phase)

            self.data_array[0, 0, 0, 0, :, :] = phi_beam
            self.data_array[1, 0, 0, 0, :, :] = theta_beam

            if rotate_pol:
                # rotate by pi/2 for second polarization
                theta_beam2 = np.roll(theta_beam, int((np.pi / 2) / delta_phi), axis=1)
                phi_beam2 = np.roll(phi_beam, int((np.pi / 2) / delta_phi), axis=1)
                self.data_array[0, 0, 1, 0, :, :] = phi_beam2
                self.data_array[1, 0, 1, 0, :, :] = theta_beam2

        self.bandpass_array[0] = 1

        if frequency is None:
            warnings.warn(
                "No frequency provided. Detected frequency is: "
                "{freqs} Hz".format(freqs=self.freq_array)
            )

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


# pyuvdata/uvbeam/mwa_beam.py

# -*- mode: python; coding: utf-8 -*-
# Copyright (c) 2019 Radio Astronomy Software Group
# Licensed under the 2-clause BSD License
"""Read in the Sujinto et al. full embedded element MWA Beam."""
import os
import warnings

import numpy as np
import h5py
from scipy.special import factorial, lpmv  # associated Legendre function

from . import UVBeam
from .. import utils as uvutils

__all__ = ["P1sin", "P1sin_array", "MWABeam"]


def P1sin(nmax, theta):
    """
    Create the Legendre function flavors for FF expansion using spherical waves.

    Note this is not vectorized so is a bit slow, but it handles the special
    case of theta = 0 and pi. We primarily use the vectorized version
    (`P1sin_array`), but call this to handle the special cases.

    See:
    Calculating Far-Field Radiation Based on FEKO Spherical Wave Coefficients,
    draft 10 June 2015.
    Available at pyuvdata/docs/references/Far_field_spherical_FEKO_draft2.pdf
    This memo gives a full description of the equations implemented here,
    including descriptions of the approximations and numerical approaches used.
    In line comments below are helpful reminders, but see the memo for the full
    detail.
    Also see Sokolowski, M. et al, "Calibration and Stokes Imaging with Full
    Embedded Element Primary Beam Model for the Murchison Widefield Array",
    PASA, 2017 (10.1017/pasa.2017.54) for details specific to the MWA.

    Parameters
    ----------
    nmax : int
        Maximum n from FEKO Q1mn and Q2mn, n must be >=1
    theta : float
        The argument of the cosine or sine function used in the associated
        Legendre functions, in radians.

    Returns
    -------
    P_sin : array of float
        P_{n}^{abs(m)}(cos(theta))/sin(theta) with FEKO order M,N.
        Shape (nmax ** 2 + 2 * nmax).
    P1 : array of float
        P_{n}^{abs(m)+1}(cos(theta)) with FEKO order M,N.
        Shape (nmax ** 2 + 2 * nmax).

    """
    # initialize for nmax, we have 2(1+...+nmax)+nmax=nmax^2+2*nmax long array
    P_sin = np.zeros((nmax ** 2 + 2 * nmax))
    P1 = np.zeros((nmax ** 2 + 2 * nmax))

    # theta arguments
    cos_th = np.cos(theta)
    sin_th = np.sin(theta)
    delta_cos = 1e-6  # for slope estimation

    # step from 1 to nmax
    for n in range(1, nmax + 1):
        # legendre P_{n}^{abs(m)=0...n} (cos_th)
        orders = np.arange(0, n + 1)
        orders = orders.reshape(n + 1, 1)
        P = lpmv(orders, n, cos_th)

        # THESE ARE THE SAME:
        # legendre(2,0:0.1:0.2) (matlab)
        # scipy:
        # a=np.arange(0,3)
        # a=a.reshape(3,1)
        # lpmv(b,2,np.arange(0,0.3,0.1))

        # P_{n}^{abs(m)+1} (cos_th)
        Pm1 = np.append(P[1::], 0)
        Pm1 = Pm1.reshape(len(Pm1), 1)

        # P_{n}^{abs(m)}(cos_th)/sin_th
        Pm_sin = np.zeros((n + 1, 1))  # initialize

        if cos_th == 1:
            # special treatment depending on m;
            # for m=0, Pm_sin=inf so, the product m*Pm_sin is zero;
            # for m=1, we need a substitution
            # m>=2, value is 0, so initial values are OK
            # The first approach, to just use the analytical derivative
            # is not stable for n>~45
            # Instead use slope estimate with a small delta_cos
            # Pn(cos x)/sin x = -dPn(cos_th)/dcos_th
            Pm_cos_delta_cos = lpmv(orders, n, cos_th - delta_cos)
            # backward difference
            Pm_sin[1, 0] = -(P[0] - Pm_cos_delta_cos[0]) / delta_cos

        elif cos_th == -1:
            # The first approach, to just use the analytical derivative
            # is not stable for n>~45
            # Instead use slope estimate with a small delta_cos
            # Pn(cos x)/sin x = -dPn(cos_th)/dcos_th
            Pm_cos_delta_cos = lpmv(orders, n, cos_th - delta_cos)
            # forward difference
            Pm_sin[1, 0] = -(Pm_cos_delta_cos[0] - P[0]) / delta_cos
        else:
            Pm_sin = P / sin_th

        # accumulate Psin and P1 for the m values
        ind_start = (n - 1) ** 2 + 2 * (n - 1)  # start index to populate
        ind_stop = n ** 2 + 2 * n  # stop index to populate
        # assign
        P_sin[np.arange(ind_start, ind_stop)] = np.append(
            np.flipud(Pm_sin[1::, 0]), Pm_sin
        )
        P1[np.arange(ind_start, ind_stop)] = np.append(np.flipud(Pm1[1::, 0]), Pm1)

    return P_sin, P1


def P1sin_array(nmax, theta):
    """
    Calculate P^abs(m)_n(cos(theta))/sin(theta) and P^(abs(m)+1)_n(cos(theta)).

    Similar to the "P1sin" function, but calculates for all theta in one go.
    At the end of the function, patches are made using the original P1sin function
    to solve the 0/0 issue.


    Parameters
    ----------
    nmax : int
        Maximum n from FEKO Q1mn and Q2mn, n must be >=1
    theta : array of float
        The argument of the cosine or sine functions used in the associated
        Legendre functions, in radians.

    Returns
    -------
    P_sin : array of float
        P_{n}^{abs(m)}(cos(theta))/sin(theta) with FEKO order M,N.
        Shape (nmax ** 2 + 2 * nmax, theta.size).
    P1 : array of float
        P_{n}^{abs(m)+1}(cos(theta)) with FEKO order M,N.
        Shape (nmax ** 2 + 2 * nmax, theta.size).

    """
    cos_th = np.cos(theta)
    sin_theta = np.sin(theta)

    # Make sure that we don't divide by 0 (sin(0) = sin(pi) = 0 ) proper results
    # are inserted at the end of this function. Set to NaN for now
    sin_theta[(theta == 0) | (theta == np.pi)] = np.NaN

    # create at forehand
    P_sin = np.zeros((nmax ** 2 + 2 * nmax, np.size(theta)))
    P1 = np.zeros((nmax ** 2 + 2 * nmax, np.size(theta)))
    for n in range(1, nmax + 1):
        # legendre P_{n}^{abs(m)=0...n} (cos_th)
        orders = np.arange(0, n + 1)
        orders = orders.reshape(n + 1, 1)

        # fetch entire matrix in one go (for a particular n)
        # in theory, fetching for all n in one go should also be possible
        P = lpmv(orders, n, cos_th)

        # P_{n}^{abs(m)+1} (cos_th)
        Pm1 = np.vstack([P[1::, :], np.zeros((1, np.size(theta)))])

        # P_{n}^{abs(m)}(u)/sin_th
        Pm_sin = P / sin_theta

        # accumulate Psin and P1 for the m values
        # start index to populate
        ind_start = (n - 1) ** 2 + 2 * (n - 1)
        # stop index to populate
        ind_stop = n ** 2 + 2 * n
        # assign
        P_sin[np.arange(ind_start, ind_stop), :] = np.vstack(
            [np.flipud(Pm_sin[1::, :]), Pm_sin]
        )
        P1[np.arange(ind_start, ind_stop), :] = np.vstack([np.flipud(Pm1[1::, :]), Pm1])

    # fix for theta = 0 and theta = pi
    # (properly handled in P1sin, so use that function)
    P_sin[:, theta == 0] = np.array([P1sin(nmax, 0)[0]]).transpose()
    P_sin[:, theta == np.pi] = np.array([P1sin(nmax, np.pi)[0]]).transpose()

    return P_sin.transpose(), P1.transpose()


class MWABeam(UVBeam):
    """
    Defines an MWA-specific subclass of UVBeam for representing MWA beams.

    This class should not be interacted with directly, instead use the
    read_mwa_beam method on the UVBeam class.

    This is based on https://github.com/MWATelescope/mwa_pb/ but we don’t import
    that module because it’s not python 3 compatible.

    Note that the azimuth convention in for the UVBeam object is different than the
    azimuth convention in the mwa_pb repo. In that repo, the azimuth convention is
    changed from the native FEKO convention (the FEKO convention is the same as the
    UVBeam convention). The convention in the mwa_pb repo has a different zero point
    and a different direction (so it is in a left handed coordinate system).

    """

    def _read_metadata(self, h5filepath):
        """
        Get metadata (frequencies, polarizations, dipole numbers) from input file.

        Parameters
        ----------
        h5filepath : str
            path to input h5 file containing the MWA full embedded element spherical
            harmonic modes.

        Returns
        -------
        freqs_hz : array of int
            Frequencies in Hz present in the file.
        pol_names : list of str
            Polarizations present in the file.
        dipole_names :
            Dipoles names present in the file.
        max_length : dict
            Dictionary keyed on pol and freq, giving max number of modes in the
            file for each pol and freq.
        """
        pol_names = set()
        dipole_names = set()
        freqs_hz = set()
        other_names = []
        max_length = {}
        with h5py.File(h5filepath, "r") as h5f:
            for name in h5f.keys():
                if name.startswith("X") or name.startswith("Y"):
                    pol = name[0]
                    dipole, freq = name[1:].split("_")
                    pol_names.add(pol)
                    dipole_names.add(dipole)
                    freq = np.int64(freq)
                    freqs_hz.add(freq)

                    if pol not in max_length:
                        max_length[pol] = {}

                    this_length = h5f[name].shape[1] // 2
                    if freq not in max_length[pol]:
                        max_length[pol][freq] = this_length
                    elif this_length > max_length[pol][freq]:
                        max_length[pol][freq] = this_length

                else:
                    other_names.append(name)

        pol_names = sorted(pol_names)

        dipole_names = np.array(sorted(dipole_names))

        freqs_hz = np.array(sorted(freqs_hz))

        return freqs_hz, pol_names, dipole_names, max_length

    def _get_beam_modes(
        self,
        h5filepath,
        freqs_hz,
        pol_names,
        dipole_names,
        max_length,
        delays,
        amplitudes,
    ):
        """
        Get beam modes from input file and save as a dict to the object.

        Parameters
        ----------
        h5filepath : str
            path to input h5 file containing the MWA full embedded element spherical
            harmonic modes.
        freqs_hz : array of int
            Frequencies in Hz to get modes for. Must be present in the file.
        pol_names : list of str
            Polarizations  to get modes for. Must be present in the file.
        dipole_names : array of str
            Dipoles names present in the file.
        max_length : dict
            Dictionary keyed on pol and freq, giving max number of modes in the
            file for each pol and freq.
        delays : array of ints
            Array of MWA beamformer delay steps. Should be shape (n_pols, n_dipoles).
        amplitudes : array of floats
            Array of dipole amplitudes, these are absolute values
            (i.e. relatable to physical units). Should be shape (n_pols, n_dipoles).

        Returns
        -------
        beam_modes : dict
            A multi-level dict keyed on (in order) pol, freq, mode name (Q1, Q2, M, N).
        """
        beam_modes = {}
        for pol_i, pol in enumerate(pol_names):
            beam_modes[pol] = {}
            for freq in freqs_hz:
                # Calculate complex excitation voltages
                # convert delay to phase
                # 435e-12 is the delay step size in seconds (435 picosec)
                phases = 2 * np.pi * freq * (-delays[pol_i, :]) * 435e-12
                # complex excitation col voltage
                Vcplx = amplitudes[pol_i, :] * np.exp(1.0j * phases)

                Q1_accum = np.zeros(max_length[pol][freq], dtype=np.complex128)
                Q2_accum = np.zeros(max_length[pol][freq], dtype=np.complex128)

                # Read in modes
                with h5py.File(h5filepath, "r") as h5f:
                    Q_modes_all = h5f["modes"][()].T
                    Nmax = 0
                    M_accum = None
                    N_accum = None
                    for dp_i, dp in enumerate(dipole_names):
                        # re-initialise Q1 and Q2 for every dipole
                        Q1 = np.zeros(max_length[pol][freq], dtype=np.complex128)
                        Q2 = np.zeros(max_length[pol][freq], dtype=np.complex128)

                        # select spherical wave table
                        name = pol + dp + "_" + str(freq)
                        Q_all = h5f[name][()].T

                        # current length
                        my_len = np.max(Q_all.shape)
                        my_len_half = my_len // 2

                        # Get modes for this dipole
                        Q_modes = Q_modes_all[0:my_len, :]

                        # convert Qall to M, N, Q1, Q2 vectors for processing

                        # find s=1 and s=2 indices
                        # only find s1 and s2 for this dipole
                        # s = 1 and s = 2 refer to TE and TM modes, respectively
                        # see the Far_field_spherical_FEKO_draft2 memo under
                        # pyuvdata/docs/references/
                        s1 = Q_modes[0:my_len, 0] <= 1
                        s2 = Q_modes[0:my_len, 0] > 1

                        # grab m,n vectors
                        M = Q_modes[s1, 1]
                        N = Q_modes[s1, 2]

                        # update to the larger M and N
                        if np.max(N) > Nmax:
                            M_accum = M
                            N_accum = N
                            Nmax = np.max(N_accum)

                        # grab Q1mn and Q2mn and make them complex
                        Q1[0:my_len_half] = Q_all[s1, 0] * np.exp(
                            1.0j * np.deg2rad(Q_all[s1, 1])
                        )
                        Q2[0:my_len_half] = Q_all[s2, 0] * np.exp(
                            1.0j * np.deg2rad(Q_all[s2, 1])
                        )

                        # accumulate Q1 and Q2, scaled by excitation voltage
                        Q1_accum = Q1_accum + Q1 * Vcplx[dp_i]
                        Q2_accum = Q2_accum + Q2 * Vcplx[dp_i]

                    beam_modes[pol][freq] = {
                        "Q1": Q1_accum,
                        "Q2": Q2_accum,
                        "M": M_accum,
                        "N": N_accum,
                    }
        return beam_modes

    def _get_response(self, freqs_hz, pol_names, beam_modes, phi_arr, theta_arr):
        """
        Calculate full Jones matrix response (E-field) of beam on a regular az/za grid.

        Parameters
        ----------
        freqs_hz : array of int
            Frequencies in Hz to get modes for. Must be present in the file.
        pol_names : list of str
            Polarizations  to get modes for. Must be present in the file.
        beam_modes : dict
            A multi-level dict keyed on (in order) pol, freq, mode name (Q1, Q2, M, N).
        phi_arr : float or array of float
            azimuth angles (radians), east through north.
        theta_arr : float or array of float
            zenith angles (radian)

        Returns
        -------
        jones : array of float
            jones vectors, shape (Npol, 2, Nfreq, Nphi, Ntheta), e.g.
                [J_11=Xtheta J_12=Xphi]
                [J_21=Ytheta J_21=Yphi]

        """
        jones = np.zeros(
            (len(pol_names), 2, freqs_hz.size, phi_arr.size, theta_arr.size),
            dtype=np.complex128,
        )

        for pol_i, pol in enumerate(pol_names):
            for freq_i, freq in enumerate(freqs_hz):
                M = beam_modes[pol][freq]["M"]
                N = beam_modes[pol][freq]["N"]
                Q1 = beam_modes[pol][freq]["Q1"]
                Q2 = beam_modes[pol][freq]["Q2"]

                # form P(cos(theta))/(sin\theta) and P^{m+1}(cos(theta))with
                # FEKO M,N order
                nmax = int(np.max(N))
                assert (
                    np.max(N) - nmax == 0
                ), "The maximum of N should be an integer value!"

                # calculate equation C_mn from equation 4 of
                # pyuvdata/docs/references/Far_field_spherical_FEKO_draft2.pdf
                # These are the normalization factors for the associated
                # Legendre function of order n and rank abs(m)
                C_MN = (
                    0.5 * (2 * N + 1) * factorial(N - abs(M)) / factorial(N + abs(M))
                ) ** 0.5

                # 1 for M<=0, -1 for odd M>0
                MabsM = np.ones(M.shape)
                MabsM[(M > 0) & (M % 2 != 0)] = -1

                # nomenclature:
                # T and P are the sky polarisations theta and phi
                # theta and phi are direction coordinates

                phi_comp = np.ascontiguousarray(
                    np.exp(1.0j * np.outer(phi_arr, range(-nmax, nmax + 1)))
                )

                (P_sin, P1) = P1sin_array(nmax, theta_arr)
                M_u = np.outer(np.cos(theta_arr), np.abs(M))
                phi_const = C_MN * MabsM / (N * (N + 1)) ** 0.5

                emn_T = (
                    (1.0j) ** N * (P_sin * (M_u * Q2 - M * Q1) + Q2 * P1) * phi_const
                )
                emn_P = (
                    (1.0j) ** (N + 1)
                    * (P_sin * (M * Q2 - Q1 * M_u) - Q1 * P1)
                    * phi_const
                )

                # Use a matrix multiplication to calculate Emn_P and Emn_T.
                # Sum results of Emn_P and emn_T for each unique M
                emn_P_sum = np.zeros(
                    (len(theta_arr), 2 * nmax + 1), dtype=np.complex128
                )
                emn_T_sum = np.zeros(
                    (len(theta_arr), 2 * nmax + 1), dtype=np.complex128
                )
                for m in range(-nmax, nmax + 1):
                    emn_P_sum[:, m + nmax] = np.sum(emn_P[:, M == m], axis=1)
                    emn_T_sum[:, m + nmax] = np.sum(emn_T[:, M == m], axis=1)

                Sigma_P = np.inner(phi_comp, emn_P_sum)
                Sigma_T = np.inner(phi_comp, emn_T_sum)

                jones[pol_i, 0, freq_i] = Sigma_T
                jones[pol_i, 1, freq_i] = -Sigma_P

        return jones

    def read_mwa_beam(
        self,
        h5filepath,
        delays=None,
        amplitudes=None,
        pixels_per_deg=5,
        freq_range=None,
        run_check=True,
        check_extra=True,
        run_check_acceptability=True,
    ):
        """
        Read in the full embedded element MWA beam.

        Parameters
        ----------
        h5filepath : str
            path to input h5 file containing the MWA full embedded element spherical
            harmonic modes. Download via
            `wget http://cerberus.mwa128t.org/mwa_full_embedded_element_pattern.h5`
        delays : array of ints
            Array of MWA beamformer delay steps. Should be shape (n_pols, n_dipoles).
        amplitudes : array of floats
            Array of dipole amplitudes, these are absolute values
            (i.e. relatable to physical units).
            Should be shape (n_pols, n_dipoles).
        pixels_per_deg : float
            Number of theta/phi pixels per degree. Sets the resolution of the beam.
        freq_range : array_like of float
            Range of frequencies to include in Hz, defaults to all available
            frequencies. Must be length 2.
        run_check : bool
            Option to check for the existence and proper shapes of
            required parameters after reading in the file.
        check_extra : bool
            Option to check optional parameters as well as required ones.
        run_check_acceptability : bool
            Option to check acceptable range of the values of
            required parameters after reading in the file.

        Returns
        -------
        None

        Raises
        ------
        ValueError
            If the amplitudes or delays are the wrong shape or there are delays
            greater than 32 or delays are not integer types.
            If the frequency range doesn't include any
            available frequencies.

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

        freqs_hz, pol_names, dipole_names, max_length = self._read_metadata(h5filepath)

        n_dp = dipole_names.size
        n_pol = len(pol_names)

        if delays is None:
            delays = np.zeros([n_pol, n_dp], dtype="int")
        else:
            if not np.issubdtype(delays.dtype, np.integer):
                raise ValueError("Delays must be integers.")

        if amplitudes is None:
            amplitudes = np.ones([n_pol, n_dp])

        if amplitudes.shape != (n_pol, n_dp):
            raise ValueError(
                "amplitudes must be shape ({npol}, {nd})".format(npol=n_pol, nd=n_dp)
            )

        if delays.shape != (n_pol, n_dp):
            raise ValueError(
                "delays must be shape ({npol}, {nd})".format(npol=n_pol, nd=n_dp)
            )

        if (delays > 32).any():
            raise ValueError(
                "There are delays greater than 32: {delays}".format(delays=delays)
            )

        # check for terminated dipoles and reset delays and amplitudes
        terminated = delays == 32
        if (terminated).any():
            warnings.warn(
                "There are some terminated dipoles "
                "(delay setting 32). Setting the amplitudes and "
                "delays of terminated dipoles to zero."
            )
            delays[terminated] = 0
            amplitudes[terminated] = 0

        if freq_range is not None:
            if np.array(freq_range).size != 2:
                raise ValueError("freq_range must have 2 elements.")
            freqs_use = freqs_hz[
                np.nonzero((freqs_hz >= freq_range[0]) & (freqs_hz <= freq_range[1]))
            ]
            if freqs_use.size < 1:
                raise ValueError(
                    "No frequencies available in freq_range. "
                    "Available frequencies are between {fmin} Hz "
                    "and {fmax} Hz".format(fmin=np.min(freqs_hz), fmax=np.max(freqs_hz))
                )
            if freqs_use.size < 2:
                warnings.warn("Only one available frequency in freq_range.")
        else:
            freqs_use = freqs_hz

        beam_modes = self._get_beam_modes(
            h5filepath,
            freqs_hz,
            pol_names,
            dipole_names,
            max_length,
            delays,
            amplitudes,
        )

        n_phi = np.floor(360 * pixels_per_deg)
        n_theta = np.floor(90 * pixels_per_deg) + 1
        theta_arr = np.deg2rad(np.arange(0, n_theta) / pixels_per_deg)
        phi_arr = np.deg2rad(np.arange(0, n_phi) / pixels_per_deg)

        jones = self._get_response(freqs_use, pol_names, beam_modes, phi_arr, theta_arr)

        # work out zenith normalization
        # (MWA beams are peak normalized to 1 when pointed at zenith)

        # start filling in the object
        self.telescope_name = "MWA"
        self.feed_name = "MWA"
        self.feed_version = "1.0"
        self.model_name = "full embedded element"
        self.model_version = "1.0"
        self.history = (
            "Sujito et al. full embedded element beam, derived from "
            "https://github.com/MWATelescope/mwa_pb/"
        )

        delay_str_list = []
        gain_str_list = []
        for pol in range(n_pol):
            delay_str_list.append(
                "[" + ", ".join([str(x) for x in delays[pol, :]]) + "]"
            )
            gain_str_list.append(
                "[" + ", ".join([str(x) for x in amplitudes[pol, :]]) + "]"
            )
        delay_str = "[" + ", ".join(delay_str_list) + "]"
        gain_str = "[" + ", ".join(gain_str_list) + "]"

        self.history += "  delays set to " + delay_str + "  gains set to " + gain_str
        if not uvutils._check_history_version(self.history, self.pyuvdata_version_str):
            self.history += self.pyuvdata_version_str

        self.x_orientation = "east"

        self._set_efield()
        self.Naxes_vec = 2
        self.Ncomponents_vec = 2
        self.feed_array = np.array([str(pol.lower()) for pol in pol_names])
        self.Nfeeds = self.feed_array.size

        self.data_normalization = "physical"

        # for now this returns a simple beam because it requires amps & delays
        # to make the beam
        self.antenna_type = "simple"

        self.Nspws = 1
        self.spw_array = np.array([0])
        self.Nfreqs = freqs_use.size
        self.freq_array = freqs_use.astype(np.float64)
        self.freq_array = self.freq_array[np.newaxis, :]
        self.bandpass_array = np.ones((self.Nspws, self.Nfreqs))

        self.pixel_coordinate_system = "az_za"
        self._set_cs_params()

        self.axis1_array = phi_arr
        self.Naxes1 = self.axis1_array.size
        self.axis2_array = theta_arr
        self.Naxes2 = self.axis2_array.size

        # The array that come from `_get_response` has shape shape
        # (Npol, 2, Nfreq, Nphi, Ntheta)
        # UVBeam wants shape
        # ('Naxes_vec', 'Nspws', 'Nfeeds', 'Nfreqs', 'Naxes2', 'Naxes1')
        # where the Naxes_vec dimension lines up with the 2 from `_get_response`,
        # Nfeeds is UVBeam's Npol for E-field beams,
        # and axes (2, 1) correspond to (theta, phi)
        # Then add an empty dimension for Nspws.
        self.data_array = np.transpose(jones, axes=[1, 0, 2, 4, 3])
        self.data_array = self.data_array[:, np.newaxis, :, :, :, :]

        self.basis_vector_array = np.zeros(
            (self.Naxes_vec, self.Ncomponents_vec, self.Naxes2, self.Naxes1)
        )
        self.basis_vector_array[0, 0, :, :] = 1.0
        self.basis_vector_array[1, 1, :, :] = 1.0

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


# pyuvdata/uvbeam/uvbeam.py

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

"""Primary container for radio telescope antenna beams."""
import os
import warnings
import copy

import numpy as np
from scipy import interpolate
from astropy import units
from astropy.coordinates import Angle

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

__all__ = ["UVBeam"]


class UVBeam(UVBase):
    """
    A class for defining a radio telescope antenna beam.

    Attributes
    ----------
    UVParameter objects: For full list see UVBeam Parameters
        (http://pyuvdata.readthedocs.io/en/latest/uvbeam_parameters.html).
        Some are always required, some are required for certain beam_types,
        antenna_types and pixel_coordinate_systems and others are always optional.

    """

    coordinate_system_dict = {
        "az_za": {
            "axes": ["azimuth", "zen_angle"],
            "description": "uniformly gridded azimuth, zenith angle coordinate system, "
            "where az runs from East to North in radians",
        },
        "orthoslant_zenith": {
            "axes": ["zenorth_x", "zenorth_y"],
            "description": "orthoslant projection at zenith where y points North, "
            "x point East",
        },
        "healpix": {
            "axes": ["hpx_inds"],
            "description": "HEALPix map with zenith at the north pole and "
            "az, za coordinate axes (for the basis_vector_array) "
            "where az runs from East to North",
        },
    }

    interpolation_function_dict = {
        "az_za_simple": {
            "description": "scipy RectBivariate spline interpolation",
            "func": "_interp_az_za_rect_spline",
        },
        "healpix_simple": {
            "description": "healpix nearest-neighbor bilinear interpolation",
            "func": "_interp_healpix_bilinear",
        },
    }

    def __init__(self):
        """Create a new UVBeam object."""
        # add the UVParameters to the class
        self._Nfreqs = uvp.UVParameter(
            "Nfreqs", description="Number of frequency channels", expected_type=int
        )

        self._Nspws = uvp.UVParameter(
            "Nspws",
            description="Number of spectral windows "
            "(ie non-contiguous spectral chunks). "
            "More than one spectral window is not "
            "currently supported.",
            expected_type=int,
        )

        desc = (
            "Number of basis vectors specified at each pixel, options "
            'are 2 or 3 (or 1 if beam_type is "power")'
        )
        self._Naxes_vec = uvp.UVParameter(
            "Naxes_vec", description=desc, expected_type=int, acceptable_vals=[2, 3]
        )

        desc = (
            "Number of basis vectors components specified at each pixel, options "
            "are 2 or 3.  Only required for E-field beams."
        )
        self._Ncomponents_vec = uvp.UVParameter(
            "Ncomponents_vec",
            description=desc,
            expected_type=int,
            acceptable_vals=[2, 3],
            required=False,
        )

        desc = (
            'Pixel coordinate system, options are: "'
            + '", "'.join(list(self.coordinate_system_dict.keys()))
            + '".'
        )
        for key in self.coordinate_system_dict:
            desc = desc + (
                ' "'
                + key
                + '" is a '
                + self.coordinate_system_dict[key]["description"]
                + ". It has axes ["
                + ", ".join(self.coordinate_system_dict[key]["axes"])
                + "]."
            )
        self._pixel_coordinate_system = uvp.UVParameter(
            "pixel_coordinate_system",
            description=desc,
            form="str",
            expected_type=str,
            acceptable_vals=list(self.coordinate_system_dict.keys()),
        )

        desc = (
            "Number of elements along the first pixel axis. "
            'Not required if pixel_coordinate_system is "healpix".'
        )
        self._Naxes1 = uvp.UVParameter(
            "Naxes1", description=desc, expected_type=int, required=False
        )

        desc = (
            "Coordinates along first pixel axis. "
            'Not required if pixel_coordinate_system is "healpix".'
        )
        self._axis1_array = uvp.UVParameter(
            "axis1_array",
            description=desc,
            expected_type=float,
            required=False,
            form=("Naxes1",),
        )

        desc = (
            "Number of elements along the second pixel axis. "
            'Not required if pixel_coordinate_system is "healpix".'
        )
        self._Naxes2 = uvp.UVParameter(
            "Naxes2", description=desc, expected_type=int, required=False
        )

        desc = (
            "Coordinates along second pixel axis. "
            'Not required if pixel_coordinate_system is "healpix".'
        )
        self._axis2_array = uvp.UVParameter(
            "axis2_array",
            description=desc,
            expected_type=float,
            required=False,
            form=("Naxes2",),
        )

        desc = (
            "Healpix nside parameter. Only required if "
            "pixel_coordinate_system is 'healpix'."
        )
        self._nside = uvp.UVParameter(
            "nside", description=desc, expected_type=int, required=False
        )

        desc = (
            'Healpix ordering parameter, allowed values are "ring" and "nested". '
            'Only required if pixel_coordinate_system is "healpix".'
        )
        self._ordering = uvp.UVParameter(
            "ordering",
            description=desc,
            expected_type=str,
            required=False,
            acceptable_vals=["ring", "nested"],
        )

        desc = (
            "Number of healpix pixels. Only required if "
            "pixel_coordinate_system is 'healpix'."
        )
        self._Npixels = uvp.UVParameter(
            "Npixels", description=desc, expected_type=int, required=False
        )

        desc = (
            "Healpix pixel numbers. Only required if "
            "pixel_coordinate_system is 'healpix'."
        )
        self._pixel_array = uvp.UVParameter(
            "pixel_array",
            description=desc,
            expected_type=int,
            required=False,
            form=("Npixels",),
        )

        desc = "String indicating beam type. Allowed values are 'efield', and 'power'."
        self._beam_type = uvp.UVParameter(
            "beam_type",
            description=desc,
            form="str",
            expected_type=str,
            acceptable_vals=["efield", "power"],
        )

        desc = (
            "Beam basis vector components -- directions for which the "
            "electric field values are recorded in the pixel coordinate system. "
            'Not required if beam_type is "power". The shape depends on the '
            'pixel_coordinate_system, if it is "healpix", the shape is: '
            "(Naxes_vec, Ncomponents_vec, Npixels), otherwise it is "
            "(Naxes_vec, Ncomponents_vec, Naxes2, Naxes1)"
        )
        self._basis_vector_array = uvp.UVParameter(
            "basis_vector_array",
            description=desc,
            required=False,
            expected_type=float,
            form=("Naxes_vec", "Ncomponents_vec", "Naxes2", "Naxes1"),
            acceptable_range=(0, 1),
            tols=1e-3,
        )

        self._Nfeeds = uvp.UVParameter(
            "Nfeeds",
            description="Number of feeds. " 'Not required if beam_type is "power".',
            expected_type=int,
            acceptable_vals=[1, 2],
            required=False,
        )

        desc = (
            "Array of feed orientations. shape (Nfeeds). "
            'options are: N/E or x/y or R/L. Not required if beam_type is "power".'
        )
        self._feed_array = uvp.UVParameter(
            "feed_array",
            description=desc,
            required=False,
            expected_type=str,
            form=("Nfeeds",),
            acceptable_vals=["N", "E", "x", "y", "R", "L"],
        )

        self._Npols = uvp.UVParameter(
            "Npols",
            description="Number of polarizations. "
            'Only required if beam_type is "power".',
            expected_type=int,
            required=False,
        )

        desc = (
            "Array of polarization integers, shape (Npols). "
            "Uses the same convention as UVData: pseudo-stokes 1:4 (pI, pQ, pU, pV);  "
            "circular -1:-4 (RR, LL, RL, LR); linear -5:-8 (XX, YY, XY, YX). "
            'Only required if beam_type is "power".'
        )
        self._polarization_array = uvp.UVParameter(
            "polarization_array",
            description=desc,
            required=False,
            expected_type=int,
            form=("Npols",),
            acceptable_vals=list(np.arange(-8, 0)) + list(np.arange(1, 5)),
        )

        desc = (
            "Array of frequencies, center of the channel, "
            "shape (Nspws, Nfreqs), units Hz"
        )
        self._freq_array = uvp.UVParameter(
            "freq_array",
            description=desc,
            form=("Nspws", "Nfreqs"),
            expected_type=float,
            tols=1e-3,
        )  # mHz

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

        desc = (
            "Normalization standard of data_array, options are: "
            '"physical", "peak" or "solid_angle". Physical normalization '
            "means that the frequency dependence of the antenna sensitivity "
            "is included in the data_array while the frequency dependence "
            "of the receiving chain is included in the bandpass_array. "
            "Peak normalized means that for each frequency the data_array"
            "is separately normalized such that the peak is 1 (so the beam "
            "is dimensionless) and all direction-independent frequency "
            'dependence is moved to the bandpass_array (if the beam_type is "efield", '
            "then peak normalized means that the absolute value of the peak is 1). "
            "Solid angle normalized means the peak normalized "
            "beam is divided by the integral of the beam over the sphere, "
            "so the beam has dimensions of 1/stradian."
        )
        self._data_normalization = uvp.UVParameter(
            "data_normalization",
            description=desc,
            form="str",
            expected_type=str,
            acceptable_vals=["physical", "peak", "solid_angle"],
        )

        desc = (
            "Depending on beam type, either complex E-field values "
            "('efield' beam type) or power values ('power' beam type) for "
            "beam model. Units are normalized to either peak or solid angle as "
            "given by data_normalization. The shape depends on the beam_type "
            "and pixel_coordinate_system, if it is 'healpix', the shape "
            "is: (Naxes_vec, Nspws, Nfeeds or Npols, Nfreqs, Npixels), "
            "otherwise it is "
            "(Naxes_vec, Nspws, Nfeeds or Npols, Nfreqs, Naxes2, Naxes1)."
        )
        self._data_array = uvp.UVParameter(
            "data_array",
            description=desc,
            expected_type=complex,
            form=("Naxes_vec", "Nspws", "Nfeeds", "Nfreqs", "Naxes2", "Naxes1"),
            tols=1e-3,
        )

        desc = (
            "Frequency dependence of the beam. Depending on the data_normalization, "
            "this may contain only the frequency dependence of the receiving "
            'chain ("physical" normalization) or all the frequency dependence '
            '("peak" normalization).'
        )
        self._bandpass_array = uvp.UVParameter(
            "bandpass_array",
            description=desc,
            expected_type=float,
            form=("Nspws", "Nfreqs"),
            tols=1e-3,
        )

        # --------- metadata -------------
        self._telescope_name = uvp.UVParameter(
            "telescope_name",
            description="Name of telescope (string)",
            form="str",
            expected_type=str,
        )

        self._feed_name = uvp.UVParameter(
            "feed_name",
            description="Name of physical feed (string)",
            form="str",
            expected_type=str,
        )

        self._feed_version = uvp.UVParameter(
            "feed_version",
            description="Version of physical feed (string)",
            form="str",
            expected_type=str,
        )

        self._model_name = uvp.UVParameter(
            "model_name",
            description="Name of beam model (string)",
            form="str",
            expected_type=str,
        )

        self._model_version = uvp.UVParameter(
            "model_version",
            description="Version of beam model (string)",
            form="str",
            expected_type=str,
        )

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

        # ---------- phased_array stuff -------------
        desc = (
            'String indicating antenna type. Allowed values are "simple", and '
            '"phased_array"'
        )
        self._antenna_type = uvp.UVParameter(
            "antenna_type",
            form="str",
            expected_type=str,
            description=desc,
            acceptable_vals=["simple", "phased_array"],
        )

        desc = (
            'Required if antenna_type = "phased_array". Number of elements '
            "in phased array"
        )
        self._Nelements = uvp.UVParameter(
            "Nelements", required=False, description=desc, expected_type=int
        )

        desc = (
            'Required if antenna_type = "phased_array". Element coordinate '
            "system, options are: N-E or x-y"
        )
        self._element_coordinate_system = uvp.UVParameter(
            "element_coordinate_system",
            required=False,
            description=desc,
            expected_type=str,
            acceptable_vals=["N-E", "x-y"],
        )

        desc = (
            'Required if antenna_type = "phased_array". Array of element '
            "locations in element coordinate system,  shape: (2, Nelements)"
        )
        self._element_location_array = uvp.UVParameter(
            "element_location_array",
            required=False,
            description=desc,
            form=(2, "Nelements"),
            expected_type=float,
        )

        desc = (
            'Required if antenna_type = "phased_array". Array of element '
            "delays, units: seconds, shape: (Nelements)"
        )
        self._delay_array = uvp.UVParameter(
            "delay_array",
            required=False,
            description=desc,
            form=("Nelements",),
            expected_type=float,
        )

        desc = (
            'Required if antenna_type = "phased_array". Array of element '
            "gains, units: dB, shape: (Nelements)"
        )
        self._gain_array = uvp.UVParameter(
            "gain_array",
            required=False,
            description=desc,
            form=("Nelements",),
            expected_type=float,
        )

        desc = (
            'Required if antenna_type = "phased_array". Matrix of complex '
            "element couplings, units: dB, "
            "shape: (Nelements, Nelements, Nfeed, Nfeed, Nspws, Nfreqs)"
        )
        self._coupling_matrix = uvp.UVParameter(
            "coupling_matrix",
            required=False,
            description=desc,
            form=("Nelements", "Nelements", "Nfeed", "Nfeed", "Nspws", "Nfreqs"),
            expected_type=complex,
        )

        # -------- 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"],
        )

        desc = (
            "String indicating interpolation function. Must be set to use "
            'the interp_* methods. Allowed values are : "'
            + '", "'.join(list(self.interpolation_function_dict.keys()))
            + '".'
        )
        self._interpolation_function = uvp.UVParameter(
            "interpolation_function",
            required=False,
            form="str",
            expected_type=str,
            description=desc,
            acceptable_vals=list(self.interpolation_function_dict.keys()),
        )
        desc = (
            "String indicating frequency interpolation kind. "
            "See scipy.interpolate.interp1d for details. Default is linear."
        )
        self._freq_interp_kind = uvp.UVParameter(
            "freq_interp_kind",
            required=False,
            form="str",
            expected_type=str,
            description=desc,
        )
        self.freq_interp_kind = "linear"

        desc = (
            "Any user supplied extra keywords, type=dict. Keys should be "
            "8 character or less strings if writing to beam fits 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 = (
            "Reference impedance of the beam model. The radiated E-farfield "
            "or the realised gain depend on the impedance of the port used to "
            "excite the simulation. This is the reference impedance (Z0) of "
            "the simulation. units: Ohms"
        )
        self._reference_impedance = uvp.UVParameter(
            "reference_impedance",
            required=False,
            description=desc,
            expected_type=float,
            tols=1e-3,
        )

        desc = "Array of receiver temperatures, shape (Nspws, Nfreqs), units K"
        self._receiver_temperature_array = uvp.UVParameter(
            "receiver_temperature_array",
            required=False,
            description=desc,
            form=("Nspws", "Nfreqs"),
            expected_type=float,
            tols=1e-3,
        )

        desc = "Array of antenna losses, shape (Nspws, Nfreqs), units dB?"
        self._loss_array = uvp.UVParameter(
            "loss_array",
            required=False,
            description=desc,
            form=("Nspws", "Nfreqs"),
            expected_type=float,
            tols=1e-3,
        )

        desc = "Array of antenna-amplifier mismatches, shape (Nspws, Nfreqs), units ?"
        self._mismatch_array = uvp.UVParameter(
            "mismatch_array",
            required=False,
            description=desc,
            form=("Nspws", "Nfreqs"),
            expected_type=float,
            tols=1e-3,
        )

        desc = (
            "S parameters of receiving chain, shape (4, Nspws, Nfreqs), "
            "ordering: s11, s12, s21, s22. see "
            "https://en.wikipedia.org/wiki/Scattering_parameters#Two-Port_S-Parameters"
        )
        self._s_parameters = uvp.UVParameter(
            "s_parameters",
            required=False,
            description=desc,
            form=(4, "Nspws", "Nfreqs"),
            expected_type=float,
            tols=1e-3,
        )

        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(UVBeam, self).__init__()

    def _set_cs_params(self):
        """Set parameters depending on pixel_coordinate_system."""
        if self.pixel_coordinate_system == "healpix":
            self._Naxes1.required = False
            self._axis1_array.required = False
            self._Naxes2.required = False
            self._axis2_array.required = False
            self._nside.required = True
            self._ordering.required = True
            self._Npixels.required = True
            self._pixel_array.required = True
            self._basis_vector_array.form = ("Naxes_vec", "Ncomponents_vec", "Npixels")
            if self.beam_type == "power":
                self._data_array.form = (
                    "Naxes_vec",
                    "Nspws",
                    "Npols",
                    "Nfreqs",
                    "Npixels",
                )
            else:
                self._data_array.form = (
                    "Naxes_vec",
                    "Nspws",
                    "Nfeeds",
                    "Nfreqs",
                    "Npixels",
                )
        else:
            self._Naxes1.required = True
            self._axis1_array.required = True
            self._Naxes2.required = True
            self._axis2_array.required = True
            if self.pixel_coordinate_system == "az_za":
                self._axis1_array.acceptable_range = [0, 2.0 * np.pi]
                self._axis2_array.acceptable_range = [0, np.pi]
            self._nside.required = False
            self._ordering.required = False
            self._Npixels.required = False
            self._pixel_array.required = False
            self._basis_vector_array.form = (
                "Naxes_vec",
                "Ncomponents_vec",
                "Naxes2",
                "Naxes1",
            )
            if self.beam_type == "power":
                self._data_array.form = (
                    "Naxes_vec",
                    "Nspws",
                    "Npols",
                    "Nfreqs",
                    "Naxes2",
                    "Naxes1",
                )
            else:
                self._data_array.form = (
                    "Naxes_vec",
                    "Nspws",
                    "Nfeeds",
                    "Nfreqs",
                    "Naxes2",
                    "Naxes1",
                )

    def _set_efield(self):
        """Set beam_type to 'efield' and adjust required parameters."""
        self.beam_type = "efield"
        self._Naxes_vec.acceptable_vals = [2, 3]
        self._Ncomponents_vec.required = True
        self._basis_vector_array.required = True
        self._Nfeeds.required = True
        self._feed_array.required = True
        self._Npols.required = False
        self._polarization_array.required = False
        self._data_array.expected_type = complex
        # call set_cs_params to fix data_array form
        self._set_cs_params()

    def _set_power(self):
        """Set beam_type to 'power' and adjust required parameters."""
        self.beam_type = "power"
        self._Naxes_vec.acceptable_vals = [1, 2, 3]
        self._basis_vector_array.required = False
        self._Ncomponents_vec.required = False
        self._Nfeeds.required = False
        self._feed_array.required = False
        self._Npols.required = True
        self._polarization_array.required = True

        # If cross pols are included, the power beam is complex. Otherwise it's real
        self._data_array.expected_type = float
        for pol in self.polarization_array:
            if pol in [3, 4, -3, -4, -7, -8]:
                self._data_array.expected_type = complex

        # call set_cs_params to fix data_array form
        self._set_cs_params()

    def _set_simple(self):
        """Set antenna_type to 'simple' and adjust required parameters."""
        self.antenna_type = "simple"
        self._Nelements.required = False
        self._element_coordinate_system.required = False
        self._element_location_array.required = False
        self._delay_array.required = False
        self._gain_array.required = False
        self._coupling_matrix.required = False

    def _set_phased_array(self):
        """Set antenna_type to 'phased_array' and adjust required parameters."""
        self.antenna_type = "phased_array"
        self._Nelements.required = True
        self._element_coordinate_system.required = True
        self._element_location_array.required = True
        self._delay_array.required = True
        self._gain_array.required = True
        self._coupling_matrix.required = True

    def check(self, check_extra=True, run_check_acceptability=True):
        """
        Check that all required parameters are set reasonably.

        Check that required parameters exist and have appropriate shapes.
        Optionally check if the values are acceptable.

        Parameters
        ----------
        check_extra : bool
            Option to check optional parameters as well as required ones.
        run_check_acceptability : bool
            Option to check if values in required parameters are acceptable.

        """
        # first make sure the required parameters and forms are set properly
        # for the pixel_coordinate_system
        self._set_cs_params()

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

        # check that basis_vector_array are basis vectors
        if self.basis_vector_array is not None:
            if np.max(np.linalg.norm(self.basis_vector_array, axis=1)) > (1 + 1e-15):
                raise ValueError("basis vectors must have lengths of 1 or less.")

        # issue warning if extra_keywords keys are longer than 8 characters
        for key in list(self.extra_keywords.keys()):
            if len(key) > 8:
                warnings.warn(
                    "key {key} in extra_keywords is longer than 8 "
                    "characters. It will be truncated to 8 if written "
                    "to a fits file format.".format(key=key)
                )

        # issue warning if extra_keywords values are lists, arrays or dicts
        for key, value in self.extra_keywords.items():
            if isinstance(value, (list, dict, np.ndarray)):
                warnings.warn(
                    "{key} in extra_keywords is a list, array or dict, "
                    "which will raise an error when writing fits "
                    "files".format(key=key)
                )

        return True

    def peak_normalize(self):
        """Convert to peak normalization."""
        if self.data_normalization == "solid_angle":
            raise NotImplementedError(
                "Conversion from solid_angle to peak "
                "normalization is not yet implemented"
            )
        for i in range(self.Nfreqs):
            max_val = abs(self.data_array[:, :, :, i, :]).max()
            self.data_array[:, :, :, i, :] /= max_val
            self.bandpass_array[:, i] *= max_val
        self.data_normalization = "peak"

    def efield_to_power(
        self,
        calc_cross_pols=True,
        keep_basis_vector=False,
        run_check=True,
        check_extra=True,
        run_check_acceptability=True,
        inplace=True,
    ):
        """
        Convert E-field beam to power beam.

        Parameters
        ----------
        calc_cross_pols : bool
            If True, calculate the crossed polarization beams
            (e.g. 'xy' and 'yx'), otherwise only calculate the same
            polarization beams (e.g. 'xx' and 'yy').
        keep_basis_vector : bool
            If True, keep the directionality information and
            just multiply the efields for each basis vector separately
            (caution: this is not what is standardly meant by the power beam).
        inplace : bool
            Option to apply conversion directly on self or to return a new
            UVBeam object.
        run_check : bool
            Option to check for the existence and proper shapes of the required
            parameters after converting to power.
        run_check_acceptability : bool
            Option to check acceptable range of the values of required parameters
            after converting to power.
        check_extra : bool
            Option to check optional parameters as well as required ones.

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

        if beam_object.beam_type != "efield":
            raise ValueError("beam_type must be efield")

        if beam_object.Nfeeds == 1:
            # There are no cross pols with one feed. Set this so the power beam is real
            calc_cross_pols = False

        efield_data = beam_object.data_array
        efield_naxes_vec = beam_object.Naxes_vec

        feed_pol_order = [(0, 0)]
        if beam_object.Nfeeds > 1:
            feed_pol_order.append((1, 1))

        if calc_cross_pols:
            beam_object.Npols = beam_object.Nfeeds ** 2
            # to get here we have Nfeeds > 1
            feed_pol_order.extend([(0, 1), (1, 0)])
        else:
            beam_object.Npols = beam_object.Nfeeds

        pol_strings = []
        for pair in feed_pol_order:
            pol_strings.append(
                beam_object.feed_array[pair[0]] + beam_object.feed_array[pair[1]]
            )
        beam_object.polarization_array = np.array(
            [
                uvutils.polstr2num(ps.upper(), x_orientation=self.x_orientation)
                for ps in pol_strings
            ]
        )

        if not keep_basis_vector:
            beam_object.Naxes_vec = 1

        # adjust requirements, fix data_array form
        beam_object._set_power()
        power_data = np.zeros(
            beam_object._data_array.expected_shape(beam_object), dtype=np.complex128
        )

        if keep_basis_vector:
            for pol_i, pair in enumerate(feed_pol_order):
                power_data[:, :, pol_i] = efield_data[:, :, pair[0]] * np.conj(
                    efield_data[:, :, pair[1]]
                )

        else:
            for pol_i, pair in enumerate(feed_pol_order):
                if efield_naxes_vec == 2:
                    for comp_i in range(2):
                        power_data[0, :, pol_i] += (
                            (
                                efield_data[0, :, pair[0]]
                                * np.conj(efield_data[0, :, pair[1]])
                            )
                            * beam_object.basis_vector_array[0, comp_i] ** 2
                            + (
                                efield_data[1, :, pair[0]]
                                * np.conj(efield_data[1, :, pair[1]])
                            )
                            * beam_object.basis_vector_array[1, comp_i] ** 2
                            + (
                                efield_data[0, :, pair[0]]
                                * np.conj(efield_data[1, :, pair[1]])
                                + efield_data[1, :, pair[0]]
                                * np.conj(efield_data[0, :, pair[1]])
                            )
                            * (
                                beam_object.basis_vector_array[0, comp_i]
                                * beam_object.basis_vector_array[1, comp_i]
                            )
                        )
                else:
                    raise ValueError(
                        "Conversion to power with 3-vector efields "
                        "is not currently supported because we have "
                        "no examples to work with."
                    )

        if not calc_cross_pols:
            max_abs_imag = np.max(np.abs(power_data.imag))
            if not np.isclose(
                max_abs_imag,
                0,
                rtol=beam_object._data_array.tols[0],
                atol=beam_object._data_array.tols[1],
            ):  # pragma: no cover
                warnings.warn(
                    "The calculated power beam has a non-zero imaginary component "
                    f"(the maximum absolute imaginary component is {max_abs_imag}). "
                    "The power beam should be real because the crosspols are not "
                    "calculated. Setting the power beam equal to the real part of the "
                    "calculated power beam."
                )
            power_data = power_data.real

        beam_object.data_array = power_data
        beam_object.Nfeeds = None
        beam_object.feed_array = None
        if not keep_basis_vector:
            beam_object.basis_vector_array = None
            beam_object.Ncomponents_vec = None

        history_update_string = " Converted from efield to power using pyuvdata."

        beam_object.history = beam_object.history + history_update_string

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

    def _stokes_matrix(self, pol_index):
        """
        Calculate Pauli matrices for pseudo-Stokes conversion.

        Derived from https://arxiv.org/pdf/1401.2095.pdf, the Pauli
        indices are reordered from the quantum mechanical
        convention to an order which gives the ordering of the pseudo-Stokes vector
        ['pI', 'pQ', 'pU, 'pV'].

        Parameters
        ----------
        pol_index : int
            Polarization index for which the Pauli matrix is generated, the index
            must lie between 0 and 3 ('pI': 0, 'pQ': 1, 'pU': 2, 'pV':3).

        Returns
        -------
        array of float
            Pauli matrix for pol_index. Shape: (2, 2)
        """
        if pol_index < 0:
            raise ValueError("n must be positive integer.")
        if pol_index > 4:
            raise ValueError("n should lie between 0 and 3.")
        if pol_index == 0:
            pauli_mat = np.array([[1.0, 0.0], [0.0, 1.0]])
        if pol_index == 1:
            pauli_mat = np.array([[1.0, 0.0], [0.0, -1.0]])
        if pol_index == 2:
            pauli_mat = np.array([[0.0, 1.0], [1.0, 0.0]])
        if pol_index == 3:
            pauli_mat = np.array([[0.0, -1.0j], [1.0j, 0.0]])

        return pauli_mat

    def _construct_mueller(self, jones, pol_index1, pol_index2):
        """
        Generate Mueller components.

        Following https://arxiv.org/pdf/1802.04151.pdf. Using equation:

                Mij = Tr(J sigma_i J^* sigma_j)

        where sigma_i and sigma_j are Pauli matrices.

        Parameters
        ----------
        jones : array of float
            Jones matrices containing the electric field for the dipole arms
            or linear polarizations. Shape: (Npixels, 2, 2) for Healpix beams or
            (Naxes1 * Naxes2, 2, 2) otherwise.
        pol_index1 : int
            Polarization index referring to the first index of Mij (i).
        pol_index2 : int
            Polarization index referring to the second index of Mij (j).

        Returns
        -------
        array of float
            Mueller array containing the Mij values, shape: (Npixels,) for Healpix beams
            or (Naxes1 * Naxes2,) otherwise.
        """
        pauli_mat1 = self._stokes_matrix(pol_index1)
        pauli_mat2 = self._stokes_matrix(pol_index2)

        mueller = 0.5 * np.einsum(
            "...ab,...bc,...cd,...ad", pauli_mat1, jones, pauli_mat2, np.conj(jones)
        )
        mueller = np.abs(mueller)

        return mueller

    def efield_to_pstokes(
        self,
        inplace=True,
        run_check=True,
        check_extra=True,
        run_check_acceptability=True,
    ):
        """
        Convert E-field to pseudo-stokes power.

        Following https://arxiv.org/pdf/1802.04151.pdf, using the equation:

                M_ij = Tr(sigma_i J sigma_j J^*)

        where sigma_i and sigma_j are Pauli matrices.

        Parameters
        ----------
        inplace : bool
            Option to apply conversion directly on self or to return a new
            UVBeam object.
        run_check : bool
            Option to check for the existence and proper shapes of the required
            parameters after converting to power.
        run_check_acceptability : bool
            Option to check acceptable range of the values of required parameters
            after converting to power.
        check_extra : bool
            Option to check optional parameters as well as required ones.

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

        if beam_object.beam_type != "efield":
            raise ValueError("beam_type must be efield.")

        efield_data = beam_object.data_array
        _sh = beam_object.data_array.shape
        Nfreqs = beam_object.Nfreqs

        if self.pixel_coordinate_system != "healpix":
            Naxes2, Naxes1 = beam_object.Naxes2, beam_object.Naxes1
            npix = Naxes1 * Naxes2
            efield_data = efield_data.reshape(efield_data.shape[:-2] + (npix,))
            _sh = efield_data.shape

        # construct jones matrix containing the electric field

        pol_strings = ["pI", "pQ", "pU", "pV"]
        power_data = np.zeros(
            (1, 1, len(pol_strings), _sh[-2], _sh[-1]), dtype=np.complex128
        )
        beam_object.polarization_array = np.array(
            [
                uvutils.polstr2num(ps.upper(), x_orientation=self.x_orientation)
                for ps in pol_strings
            ]
        )

        for fq_i in range(Nfreqs):
            jones = np.zeros((_sh[-1], 2, 2), dtype=np.complex128)
            pol_strings = ["pI", "pQ", "pU", "pV"]
            jones[:, 0, 0] = efield_data[0, 0, 0, fq_i, :]
            jones[:, 0, 1] = efield_data[0, 0, 1, fq_i, :]
            jones[:, 1, 0] = efield_data[1, 0, 0, fq_i, :]
            jones[:, 1, 1] = efield_data[1, 0, 1, fq_i, :]

            for pol_i in range(len(pol_strings)):
                power_data[:, :, pol_i, fq_i, :] = self._construct_mueller(
                    jones, pol_i, pol_i
                )

        if self.pixel_coordinate_system != "healpix":
            power_data = power_data.reshape(power_data.shape[:-1] + (Naxes2, Naxes1))
        beam_object.data_array = power_data
        beam_object.polarization_array = np.array(
            [
                uvutils.polstr2num(ps.upper(), x_orientation=self.x_orientation)
                for ps in pol_strings
            ]
        )
        beam_object.Naxes_vec = 1
        beam_object._set_power()

        history_update_string = (
            " Converted from efield to pseudo-stokes power using pyuvdata."
        )
        beam_object.Npols = beam_object.Nfeeds ** 2
        beam_object.history = beam_object.history + history_update_string
        beam_object.Nfeeds = None
        beam_object.feed_array = None
        beam_object.basis_vector_array = None
        beam_object.Ncomponents_vec = None

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

    def _interp_freq(self, freq_array, kind="linear", tol=1.0):
        """
        Interpolate function along frequency axis.

        Parameters
        ----------
        freq_array : array_like of floats
            Frequency values to interpolate to.
        kind : str
            Interpolation method to use frequency.
            See scipy.interpolate.interp1d for details.

        Returns
        -------
        interp_data : array_like of float or complex
            The array of interpolated data values,
            shape: (Naxes_vec, Nspws, Nfeeds or Npols, freq_array.size,
            Npixels or (Naxis2, Naxis1))
        interp_bandpass : array_like of float
            The interpolated bandpass. shape: (Nspws, freq_array.size)

        """
        assert isinstance(freq_array, np.ndarray)
        assert freq_array.ndim == 1

        # use the beam at nearest neighbors if not kind is 'nearest'
        if kind == "nearest":
            freq_dists = np.abs(self.freq_array - freq_array.reshape(-1, 1))
            nearest_inds = np.argmin(freq_dists, axis=1)
            interp_arrays = [
                self.data_array[:, :, :, nearest_inds, :],
                self.bandpass_array[:, nearest_inds],
            ]

        # otherwise interpolate the beam
        else:
            if self.Nfreqs == 1:
                raise ValueError("Only one frequency in UVBeam so cannot interpolate.")

            if np.min(freq_array) < np.min(self.freq_array) or np.max(
                freq_array
            ) > np.max(self.freq_array):
                raise ValueError(
                    "at least one interpolation frequency is outside of "
                    "the UVBeam freq_array range."
                )

            def get_lambda(real_lut, imag_lut=None):
                # Returns function objects for interpolation reuse
                if imag_lut is None:
                    return lambda freqs: real_lut(freqs)
                else:
                    return lambda freqs: (real_lut(freqs) + 1j * imag_lut(freqs))

            interp_arrays = []
            for data, ax in zip([self.data_array, self.bandpass_array], [3, 1]):
                if np.iscomplexobj(data):
                    # interpolate real and imaginary parts separately
                    real_lut = interpolate.interp1d(
                        self.freq_array[0, :], data.real, kind=kind, axis=ax
                    )
                    imag_lut = interpolate.interp1d(
                        self.freq_array[0, :], data.imag, kind=kind, axis=ax
                    )
                    lut = get_lambda(real_lut, imag_lut)
                else:
                    lut = interpolate.interp1d(
                        self.freq_array[0, :], data, kind=kind, axis=ax
                    )
                    lut = get_lambda(lut)

                interp_arrays.append(lut(freq_array))

        return tuple(interp_arrays)

    def _interp_az_za_rect_spline(
        self,
        az_array,
        za_array,
        freq_array,
        freq_interp_kind="linear",
        freq_interp_tol=1.0,
        polarizations=None,
        reuse_spline=False,
        spline_opts=None,
    ):
        """
        Interpolate in az_za coordinate system with a simple spline.

        Parameters
        ----------
        az_array : array_like of floats
            Azimuth values to interpolate to in radians, specifying the azimuth
            positions for every interpolation point (same length as `za_array`).
        za_array : array_like of floats
            Zenith values to interpolate to in radians, specifying the zenith
            positions for every interpolation point (same length as `az_array`).
        freq_array : array_like of floats
            Frequency values to interpolate to.
        freq_interp_kind : str
            Interpolation method to use frequency.
            See scipy.interpolate.interp1d for details.
        polarizations : list of str
            polarizations to interpolate if beam_type is 'power'.
            Default is all polarizations in self.polarization_array.
        reuse_spline : bool
            Save the interpolation functions for reuse.
        spline_opts : dict
            Options (kx, ky, s) for numpy.RectBivariateSpline.

        Returns
        -------
        interp_data : array_like of float or complex
            The array of interpolated data values,
            shape: (Naxes_vec, Nspws, Nfeeds or Npols, Nfreqs, az_array.size)
        interp_basis_vector : array_like of float
            The array of interpolated basis vectors,
            shape: (Naxes_vec, Ncomponents_vec, az_array.size)
        interp_bandpass : array_like of float
            The interpolated bandpass. shape: (Nspws, freq_array.size)

        """
        if self.pixel_coordinate_system != "az_za":
            raise ValueError('pixel_coordinate_system must be "az_za"')

        if freq_array is not None:
            assert isinstance(freq_array, np.ndarray)
            input_data_array, interp_bandpass = self._interp_freq(
                freq_array, kind=freq_interp_kind, tol=freq_interp_tol
            )
            input_nfreqs = freq_array.size
        else:
            input_data_array = self.data_array
            input_nfreqs = self.Nfreqs
            freq_array = self.freq_array[0]
            interp_bandpass = self.bandpass_array[0]

        if az_array is None or za_array is None:
            return input_data_array, self.basis_vector_array, interp_bandpass

        assert isinstance(az_array, np.ndarray)
        assert isinstance(za_array, np.ndarray)
        assert az_array.ndim == 1
        assert az_array.shape == za_array.shape

        npoints = az_array.size

        axis1_diff = np.diff(self.axis1_array)[0]
        axis2_diff = np.diff(self.axis2_array)[0]
        max_axis_diff = np.max([axis1_diff, axis2_diff])

        phi_length = np.abs(self.axis1_array[0] - self.axis1_array[-1]) + axis1_diff

        phi_vals, theta_vals = np.meshgrid(self.axis1_array, self.axis2_array)

        assert input_data_array.shape[3] == input_nfreqs

        if np.iscomplexobj(input_data_array):
            data_type = np.complex128
        else:
            data_type = np.float64

        if np.isclose(phi_length, 2 * np.pi, atol=axis1_diff):
            # phi wraps around, extend array in each direction to improve interpolation
            extend_length = 3
            phi_use = np.concatenate(
                (
                    np.flip(phi_vals[:, :extend_length] * (-1) - axis1_diff, axis=1),
                    phi_vals,
                    phi_vals[:, -1 * extend_length :] + extend_length * axis1_diff,
                ),
                axis=1,
            )
            theta_use = np.concatenate(
                (
                    theta_vals[:, :extend_length],
                    theta_vals,
                    theta_vals[:, -1 * extend_length :],
                ),
                axis=1,
            )

            low_slice = input_data_array[:, :, :, :, :, :extend_length]
            high_slice = input_data_array[:, :, :, :, :, -1 * extend_length :]

            data_use = np.concatenate((high_slice, input_data_array, low_slice), axis=5)
        else:
            phi_use = phi_vals
            theta_use = theta_vals
            data_use = input_data_array

        if self.basis_vector_array is not None:
            if np.any(self.basis_vector_array[0, 1, :] > 0) or np.any(
                self.basis_vector_array[1, 0, :] > 0
            ):
                # Input basis vectors are not aligned to the native theta/phi
                # coordinate system
                raise NotImplementedError(
                    "interpolation for input basis "
                    "vectors that are not aligned to the "
                    "native theta/phi coordinate system "
                    "is not yet supported"
                )
            else:
                # The basis vector array comes in defined at the rectangular grid.
                # Redefine it for the interpolation points
                interp_basis_vector = np.zeros(
                    [self.Naxes_vec, self.Ncomponents_vec, npoints]
                )
                interp_basis_vector[0, 0, :] = np.ones(npoints)  # theta hat
                interp_basis_vector[1, 1, :] = np.ones(npoints)  # phi hat
        else:
            interp_basis_vector = None

        def get_lambda(real_lut, imag_lut=None):
            # Returns function objects for interpolation reuse
            if imag_lut is None:
                return lambda za, az: real_lut(za, az, grid=False)
            else:
                return lambda za, az: (
                    real_lut(za, az, grid=False) + 1j * imag_lut(za, az, grid=False)
                )

        # Npols is only defined for power beams.  For E-field beams need Nfeeds.
        if self.beam_type == "power":
            # get requested polarization indices
            if polarizations is None:
                Npol_feeds = self.Npols
                pol_inds = np.arange(Npol_feeds)
            else:
                pols = [
                    uvutils.polstr2num(p, x_orientation=self.x_orientation)
                    for p in polarizations
                ]
                pol_inds = []
                for pol in pols:
                    if pol not in self.polarization_array:
                        raise ValueError(
                            "Requested polarization {} not found "
                            "in self.polarization_array".format(pol)
                        )
                    pol_inds.append(np.where(self.polarization_array == pol)[0][0])
                pol_inds = np.asarray(pol_inds)
                Npol_feeds = len(pol_inds)

        else:
            Npol_feeds = self.Nfeeds
            pol_inds = np.arange(Npol_feeds)

        data_shape = (self.Naxes_vec, self.Nspws, Npol_feeds, input_nfreqs, npoints)
        interp_data = np.zeros(data_shape, dtype=data_type)

        if spline_opts is None or not isinstance(spline_opts, dict):
            spline_opts = {}
        if reuse_spline and not hasattr(self, "saved_interp_functions"):
            int_dict = {}
            self.saved_interp_functions = int_dict
        for index1 in range(self.Nspws):
            for index3 in range(input_nfreqs):
                freq = freq_array[index3]
                for index0 in range(self.Naxes_vec):
                    for pol_return_ind, index2 in enumerate(pol_inds):
                        do_interp = True
                        key = (index1, freq, index2, index0)
                        if reuse_spline:
                            if key in self.saved_interp_functions.keys():
                                do_interp = False
                                lut = self.saved_interp_functions[key]

                        if do_interp:
                            if np.iscomplexobj(data_use):
                                # interpolate real and imaginary parts separately
                                real_lut = interpolate.RectBivariateSpline(
                                    theta_use[:, 0],
                                    phi_use[0, :],
                                    data_use[index0, index1, index2, index3, :].real,
                                    **spline_opts,
                                )
                                imag_lut = interpolate.RectBivariateSpline(
                                    theta_use[:, 0],
                                    phi_use[0, :],
                                    data_use[index0, index1, index2, index3, :].imag,
                                    **spline_opts,
                                )
                                lut = get_lambda(real_lut, imag_lut)
                            else:
                                lut = interpolate.RectBivariateSpline(
                                    theta_use[:, 0],
                                    phi_use[0, :],
                                    data_use[index0, index1, index2, index3, :],
                                    **spline_opts,
                                )
                                lut = get_lambda(lut)
                            if reuse_spline:
                                self.saved_interp_functions[key] = lut
                        if index0 == 0 and index1 == 0 and index2 == 0 and index3 == 0:
                            for point_i in range(npoints):
                                pix_dists = np.sqrt(
                                    (theta_use - za_array[point_i]) ** 2.0
                                    + (phi_use - az_array[point_i]) ** 2.0
                                )
                                if np.min(pix_dists) > (max_axis_diff * 2.0):
                                    raise ValueError(
                                        "at least one interpolation location "
                                        "is outside of the UVBeam pixel coverage."
                                    )
                        interp_data[index0, index1, pol_return_ind, index3, :] = lut(
                            za_array, az_array
                        )

        return interp_data, interp_basis_vector, interp_bandpass

    def _interp_healpix_bilinear(
        self,
        az_array,
        za_array,
        freq_array,
        freq_interp_kind="linear",
        freq_interp_tol=1.0,
        polarizations=None,
        reuse_spline=False,
    ):
        """
        Interpolate in Healpix coordinate system with a simple bilinear function.

        Parameters
        ----------
        az_array : array_like of floats
            Azimuth values to interpolate to in radians, specifying the azimuth
            positions for every interpolation point (same length as `za_array`).
        za_array : array_like of floats
            Zenith values to interpolate to in radians, specifying the zenith
            positions for every interpolation point (same length as `az_array`).
        freq_array : array_like of floats
            Frequency values to interpolate to.
        freq_interp_kind : str
            Interpolation method to use frequency.
            See scipy.interpolate.interp1d for details.
        polarizations : list of str
            polarizations to interpolate if beam_type is 'power'.
            Default is all polarizations in self.polarization_array.

        Returns
        -------
        interp_data : array_like of float or complex
            The array of interpolated data values,
            shape: (Naxes_vec, Nspws, Nfeeds or Npols, Nfreqs, az_array.size)
        interp_basis_vector : array_like of float
            The array of interpolated basis vectors,
            shape: (Naxes_vec, Ncomponents_vec, az_array.size)
        interp_bandpass : array_like of float
            The interpolated bandpass. shape: (Nspws, freq_array.size)

        """
        try:
            from astropy_healpix import HEALPix
        except ImportError as e:  # pragma: no cover
            raise ImportError(
                "astropy_healpix is not installed but is "
                "required for healpix functionality. "
                'Install "astropy-healpix" using conda or pip.'
            ) from e

        if self.pixel_coordinate_system != "healpix":
            raise ValueError('pixel_coordinate_system must be "healpix"')

        if not self.Npixels == 12 * self.nside ** 2:
            raise ValueError(
                "simple healpix interpolation requires full sky healpix maps."
            )
        if not np.max(np.abs(np.diff(self.pixel_array))) == 1:
            raise ValueError(
                "simple healpix interpolation requires healpix pixels to be in order."
            )

        if freq_array is not None:
            assert isinstance(freq_array, np.ndarray)
            input_data_array, interp_bandpass = self._interp_freq(
                freq_array, kind=freq_interp_kind, tol=freq_interp_tol
            )
            input_nfreqs = freq_array.size
        else:
            input_data_array = self.data_array
            input_nfreqs = self.Nfreqs
            freq_array = self.freq_array[0]
            interp_bandpass = self.bandpass_array[0]

        if az_array is None or za_array is None:
            return input_data_array, self.basis_vector_array, interp_bandpass

        assert isinstance(az_array, np.ndarray)
        assert isinstance(za_array, np.ndarray)
        assert az_array.ndim == 1
        assert az_array.shape == za_array.shape

        npoints = az_array.size

        # Npols is only defined for power beams.  For E-field beams need Nfeeds.
        if self.beam_type == "power":
            # get requested polarization indices
            if polarizations is None:
                Npol_feeds = self.Npols
                pol_inds = np.arange(Npol_feeds)
            else:
                pols = [
                    uvutils.polstr2num(p, x_orientation=self.x_orientation)
                    for p in polarizations
                ]
                pol_inds = []
                for pol in pols:
                    if pol not in self.polarization_array:
                        raise ValueError(
                            f"Requested polarization {pol} not found "
                            "in self.polarization_array"
                        )
                    pol_inds.append(np.where(self.polarization_array == pol)[0][0])
                pol_inds = np.asarray(pol_inds)
                Npol_feeds = len(pol_inds)
        else:
            Npol_feeds = self.Nfeeds
            pol_inds = np.arange(Npol_feeds)

        if np.iscomplexobj(input_data_array):
            data_type = np.complex128
        else:
            data_type = np.float64
        interp_data = np.zeros(
            (self.Naxes_vec, self.Nspws, Npol_feeds, input_nfreqs, len(az_array)),
            dtype=data_type,
        )

        if self.basis_vector_array is not None:
            if np.any(self.basis_vector_array[0, 1, :] > 0) or np.any(
                self.basis_vector_array[1, 0, :] > 0
            ):
                """ Input basis vectors are not aligned to the native theta/phi
                coordinate system """
                raise NotImplementedError(
                    "interpolation for input basis "
                    "vectors that are not aligned to the "
                    "native theta/phi coordinate system "
                    "is not yet supported"
                )
            else:
                """ The basis vector array comes in defined at the rectangular grid.
                Redefine it for the interpolation points """
                interp_basis_vector = np.zeros(
                    [self.Naxes_vec, self.Ncomponents_vec, npoints]
                )
                interp_basis_vector[0, 0, :] = np.ones(npoints)  # theta hat
                interp_basis_vector[1, 1, :] = np.ones(npoints)  # phi hat
        else:
            interp_basis_vector = None

        hp_obj = HEALPix(nside=self.nside, order=self.ordering)
        lat_array = Angle(np.pi / 2, units.radian) - Angle(za_array, units.radian)
        lon_array = Angle(az_array, units.radian)
        for index1 in range(self.Nspws):
            for index3 in range(input_nfreqs):
                for index0 in range(self.Naxes_vec):
                    for index2 in range(Npol_feeds):
                        if np.iscomplexobj(input_data_array):
                            # interpolate real and imaginary parts separately
                            real_hmap = hp_obj.interpolate_bilinear_lonlat(
                                lon_array,
                                lat_array,
                                input_data_array[
                                    index0, index1, pol_inds[index2], index3, :
                                ].real,
                            )
                            imag_hmap = hp_obj.interpolate_bilinear_lonlat(
                                lon_array,
                                lat_array,
                                input_data_array[
                                    index0, index1, pol_inds[index2], index3, :
                                ].imag,
                            )

                            hmap = real_hmap + 1j * imag_hmap
                        else:
                            # interpolate once
                            hmap = hp_obj.interpolate_bilinear_lonlat(
                                lon_array,
                                lat_array,
                                input_data_array[
                                    index0, index1, pol_inds[index2], index3, :
                                ],
                            )

                        interp_data[index0, index1, index2, index3, :] = hmap

        return interp_data, interp_basis_vector, interp_bandpass

    def interp(
        self,
        az_array=None,
        za_array=None,
        az_za_grid=False,
        healpix_nside=None,
        healpix_inds=None,
        freq_array=None,
        freq_interp_tol=1.0,
        polarizations=None,
        return_bandpass=False,
        reuse_spline=False,
        spline_opts=None,
        new_object=False,
        run_check=True,
        check_extra=True,
        run_check_acceptability=True,
    ):
        """
        Interpolate beam to given frequency, az & za locations or Healpix pixel centers.

        Parameters
        ----------
        az_array : array_like of floats, optional
            Azimuth values to interpolate to in radians, either specifying the
            azimuth positions for every interpolation point or specifying the
            azimuth vector for a meshgrid if az_za_grid is True.
        za_array : array_like of floats, optional
            Zenith values to interpolate to in radians, either specifying the
            zenith positions for every interpolation point or specifying the
            zenith vector for a meshgrid if az_za_grid is True.
        az_za_grid : bool
            Option to treat the `az_array` and `za_array` as the input vectors
            for points on a mesh grid.
        healpix_nside : int, optional
            HEALPix nside parameter if interpolating to HEALPix pixels.
        healpix_inds : array_like of int, optional
            HEALPix indices to interpolate to. Defaults to all indices in the
            map if `healpix_nside` is set and `az_array` and `za_array` are None.
        freq_array : array_like of floats, optional
            Frequency values to interpolate to.
        freq_interp_tol : float
            Frequency distance tolerance [Hz] of nearest neighbors.
            If *all* elements in freq_array have nearest neighbor distances within
            the specified tolerance then return the beam at each nearest neighbor,
            otherwise interpolate the beam.
        polarizations : list of str
            polarizations to interpolate if beam_type is 'power'.
            Default is all polarizations in self.polarization_array.
        new_object : bool
            Option to return a new UVBeam object with the interpolated data,
            if possible. Note that this is only possible for Healpix pixels or
            if az_za_grid is True and `az_array` and `za_array` are evenly spaced
            or for frequency only interpolation.
        reuse_spline : bool
            Save the interpolation functions for reuse. Only applies for
            `az_za_simple` interpolation.
        spline_opts : dict
            Provide options to numpy.RectBivariateSpline. This includes spline
            order parameters `kx` and `ky`, and smoothing parameter `s`.
            Only applies for `az_za_simple` interpolation.
        run_check : bool
            Only used if new_object is True. Option to check for the existence
            and proper shapes of required parameters on the new object.
        check_extra : bool
            Only used if new_object is True. Option to check optional parameters
            as well as required ones on the new object.
        run_check_acceptability : bool
            Only used if new_object is True. Option to check acceptable range
            of the values of required parameters on the new object.

        Returns
        -------
        array_like of float or complex or a UVBeam object
            Either an array of interpolated values or a UVBeam object if
            `new_object` is True. The shape of the interpolated data will be:
            (Naxes_vec, Nspws, Nfeeds or Npols, Nfreqs or freq_array.size if
            freq_array is passed, Npixels/(Naxis1, Naxis2) or az_array.size if
            az/za_arrays are passed)
        interp_basis_vector : array_like of float, optional
            an array of interpolated basis vectors (or self.basis_vector_array
            if az/za_arrays are not passed), shape: (Naxes_vec, Ncomponents_vec,
            Npixels/(Naxis1, Naxis2) or az_array.size if az/za_arrays are passed)
        interp_bandpass : array_like of float, optional
            The interpolated bandpass, only returned if `return_bandpass` is True.
            shape: (Nspws, freq_array.size)

        """
        if self.interpolation_function is None:
            raise ValueError("interpolation_function must be set on object first")
        if self.freq_interp_kind is None:
            raise ValueError("freq_interp_kind must be set on object first")

        if new_object:
            if not az_za_grid and az_array is not None:
                raise ValueError(
                    "A new object can only be returned if "
                    "az_za_grid is True or for Healpix pixels or "
                    "for frequency only interpolation."
                )

        kind_use = self.freq_interp_kind
        if freq_array is not None:
            # get frequency distances
            freq_dists = np.abs(self.freq_array - freq_array.reshape(-1, 1))
            nearest_dist = np.min(freq_dists, axis=1)
            interp_bool = np.any(nearest_dist >= freq_interp_tol)

            # use the beam at nearest neighbors if not interp_bool
            if not interp_bool:
                kind_use = "nearest"

        if az_za_grid:
            az_array_use, za_array_use = np.meshgrid(az_array, za_array)
            az_array_use = az_array_use.flatten()
            za_array_use = za_array_use.flatten()
        else:
            az_array_use = copy.copy(az_array)
            za_array_use = copy.copy(za_array)

        if healpix_nside is not None or healpix_inds is not None:
            if healpix_nside is None:
                raise ValueError("healpix_nside must be set if healpix_inds is set.")
            if az_array is not None or za_array is not None:
                raise ValueError(
                    "healpix_nside and healpix_inds can not be "
                    "set if az_array or za_array is set."
                )
            try:
                from astropy_healpix import HEALPix
            except ImportError as e:  # pragma: no cover
                raise ImportError(
                    "astropy_healpix is not installed but is "
                    "required for healpix functionality. "
                    'Install "astropy-healpix" using conda or pip.'
                ) from e

            hp_obj = HEALPix(nside=healpix_nside)
            if healpix_inds is None:
                healpix_inds = np.arange(hp_obj.npix)

            hpx_lon, hpx_lat = hp_obj.healpix_to_lonlat(healpix_inds)

            za_array_use = (Angle(np.pi / 2, units.radian) - hpx_lat).radian
            az_array_use = hpx_lon.radian

        interp_func = self.interpolation_function_dict[self.interpolation_function][
            "func"
        ]

        extra_keyword_dict = {}
        if interp_func == "_interp_az_za_rect_spline":
            extra_keyword_dict["reuse_spline"] = reuse_spline
            extra_keyword_dict["spline_opts"] = spline_opts

        interp_data, interp_basis_vector, interp_bandpass = getattr(self, interp_func)(
            az_array_use,
            za_array_use,
            freq_array,
            freq_interp_kind=kind_use,
            polarizations=polarizations,
            **extra_keyword_dict,
        )

        # return just the interpolated arrays
        if not new_object:
            if return_bandpass:
                return interp_data, interp_basis_vector, interp_bandpass
            else:
                return interp_data, interp_basis_vector

        # return a new UVBeam object with interpolated data
        else:
            # make a new object
            new_uvb = self.copy()

            history_update_string = " Interpolated"
            if freq_array is not None:
                history_update_string += " in frequency"
                new_uvb.Nfreqs = freq_array.size
                new_uvb.freq_array = freq_array.reshape(1, -1)
                new_uvb.bandpass_array = interp_bandpass
                new_uvb.freq_interp_kind = kind_use

            if az_array is not None:
                if freq_array is not None:
                    history_update_string += " and"
                if new_uvb.pixel_coordinate_system != "az_za":
                    input_desc = self.coordinate_system_dict[
                        new_uvb.pixel_coordinate_system
                    ]["description"]
                    output_desc = self.coordinate_system_dict["az_za"]["description"]
                    history_update_string += (
                        " from " + input_desc + " to " + output_desc
                    )
                    new_uvb.pixel_coordinate_system = "az_za"
                    new_uvb.Npixels = None
                    new_uvb.pixel_array = None
                    new_uvb.nside = None
                    new_uvb.ordering = None
                else:
                    history_update_string += " to a new azimuth/zenith angle grid"

                interp_data = interp_data.reshape(
                    interp_data.shape[:-1] + (za_array.size, az_array.size)
                )
                if interp_basis_vector is not None:
                    interp_basis_vector = interp_basis_vector.reshape(
                        interp_basis_vector.shape[:-1] + (za_array.size, az_array.size)
                    )

                new_uvb.axis1_array = az_array
                new_uvb.axis2_array = za_array
                new_uvb.Naxes1 = new_uvb.axis1_array.size
                new_uvb.Naxes2 = new_uvb.axis2_array.size

            elif healpix_nside is not None:
                if freq_array is not None:
                    history_update_string += " and"
                if new_uvb.pixel_coordinate_system != "healpix":
                    input_desc = self.coordinate_system_dict[
                        new_uvb.pixel_coordinate_system
                    ]["description"]
                    output_desc = self.coordinate_system_dict["healpix"]["description"]
                    history_update_string += (
                        " from " + input_desc + " to " + output_desc
                    )
                    new_uvb.pixel_coordinate_system = "healpix"
                    new_uvb.Naxes1 = None
                    new_uvb.axis1_array = None
                    new_uvb.Naxes2 = None
                    new_uvb.axis2_array = None
                else:
                    history_update_string += " to a new healpix grid"

                new_uvb.pixel_array = healpix_inds
                new_uvb.Npixels = new_uvb.pixel_array.size
                new_uvb.nside = healpix_nside
                new_uvb.ordering = "ring"

            history_update_string += (
                " using pyuvdata with interpolation_function = "
                + new_uvb.interpolation_function
            )
            if freq_array is not None:
                history_update_string += (
                    " and freq_interp_kind = " + new_uvb.freq_interp_kind
                )
            history_update_string += "."
            new_uvb.history = new_uvb.history + history_update_string
            new_uvb.data_array = interp_data
            if new_uvb.basis_vector_array is not None:
                new_uvb.basis_vector_array = interp_basis_vector

            if hasattr(new_uvb, "saved_interp_functions"):
                delattr(new_uvb, "saved_interp_functions")

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

    def to_healpix(
        self,
        nside=None,
        run_check=True,
        check_extra=True,
        run_check_acceptability=True,
        inplace=True,
    ):
        """
        Convert beam to the healpix coordinate system.

        The interpolation is done using the interpolation method specified in
        self.interpolation_function.

        Note that this interpolation isn't perfect. Interpolating an Efield beam
        and then converting to power gives a different result than converting
        to power and then interpolating at about a 5% level.

        Parameters
        ----------
        nside : int
            The nside to use for the Healpix map. If not specified, use
            the nside that gives the closest resolution that is higher than the
            input resolution.
        run_check : bool
            Option to check for the existence and proper shapes of required
            parameters after converting to healpix.
        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 required parameters
            after combining objects
        inplace : bool
            Option to perform the select directly on self or return a new UVBeam
            object.

        """
        try:
            from astropy_healpix import HEALPix
        except ImportError as e:  # pragma: no cover
            raise ImportError(
                "astropy_healpix is not installed but is "
                "required for healpix functionality. "
                'Install "astropy-healpix" using conda or pip.'
            ) from e

        if nside is None:
            min_res = np.min(
                np.array([np.diff(self.axis1_array)[0], np.diff(self.axis2_array)[0]])
            )
            nside_min_res = np.sqrt(3 / np.pi) * np.radians(60.0) / min_res
            nside = int(2 ** np.ceil(np.log2(nside_min_res)))
            hp_obj = HEALPix(nside=nside)
            assert hp_obj.pixel_resolution.to_value(units.radian) < min_res
        else:
            hp_obj = HEALPix(nside=nside)

        pixels = np.arange(hp_obj.npix)
        hpx_lon, hpx_lat = hp_obj.healpix_to_lonlat(pixels)

        hpx_theta = (Angle(np.pi / 2, units.radian) - hpx_lat).radian
        hpx_phi = hpx_lon.radian

        phi_vals, theta_vals = np.meshgrid(self.axis1_array, self.axis2_array)

        # Don't ask for interpolation to pixels that aren't inside the beam area
        pix_dists = np.sqrt(
            (theta_vals.ravel() - hpx_theta.reshape(-1, 1)) ** 2
            + (phi_vals.ravel() - hpx_phi.reshape(-1, 1)) ** 2
        )

        inds_to_use = np.argwhere(
            np.min(pix_dists, axis=1)
            < hp_obj.pixel_resolution.to_value(units.radian) * 2
        ).squeeze(1)

        if inds_to_use.size < hp_obj.npix:
            pixels = pixels[inds_to_use]

        beam_object = self.interp(
            healpix_nside=nside,
            healpix_inds=pixels,
            new_object=True,
            run_check=run_check,
            check_extra=check_extra,
            run_check_acceptability=run_check_acceptability,
        )

        if not inplace:
            return beam_object
        else:
            for p in beam_object:
                param = getattr(beam_object, p)
                setattr(self, p, param)

    def _get_beam(self, pol):
        """
        Get the healpix power beam map corresponding to the specififed polarization.

        pseudo-stokes I: 'pI', Q: 'pQ', U: 'pU' and V: 'pV' or linear dipole
        polarization: 'XX', 'YY', etc.

        Parameters
        ----------
        pol : str or int
            polarization string or integer, Ex. a pseudo-stokes pol 'pI', or
            a linear pol 'XX'.

        Returns
        -------
        UVBeam
            healpix beam
        """
        # assert map is in healpix coords
        assert (
            self.pixel_coordinate_system == "healpix"
        ), "pixel_coordinate_system must be healpix"
        # assert beam_type is power
        assert self.beam_type == "power", "beam_type must be power"
        if isinstance(pol, (str, np.str_)):
            pol = uvutils.polstr2num(pol, x_orientation=self.x_orientation)
        pol_array = self.polarization_array
        if pol in pol_array:
            stokes_p_ind = np.where(np.isin(pol_array, pol))[0][0]
            beam = self.data_array[0, 0, stokes_p_ind]
        else:
            raise ValueError("Do not have the right polarization information")

        return beam

    def get_beam_area(self, pol="pI"):
        """
        Compute the integral of the beam in units of steradians.

        Pseudo-Stokes 'pI' (I), 'pQ'(Q), 'pU'(U), 'pV'(V) beam and linear
        dipole 'XX', 'XY', 'YX' and 'YY' are supported.
        See Equations 4 and 5 of Moore et al. (2017) ApJ 836, 154
        or arxiv:1502.05072 and Kohn et al. (2018) or
        https://arxiv.org/pdf/1802.04151.pdf for details.

        Parameters
        ----------
        pol : str or int
            polarization string or integer, Ex. a pseudo-stokes pol 'pI', or a
            linear pol 'XX'.

        Returns
        -------
        omega : float
            Integral of the beam across the sky, units: steradians.

        """
        if isinstance(pol, (str, np.str_)):
            pol = uvutils.polstr2num(pol, x_orientation=self.x_orientation)
        if self.beam_type != "power":
            raise ValueError("beam_type must be power")
        if self.Naxes_vec > 1:
            raise ValueError("Expect scalar for power beam, found vector")
        if self._data_normalization.value != "peak":
            raise ValueError("beam must be peak normalized")
        if self.pixel_coordinate_system != "healpix":
            raise ValueError("Currently only healpix format supported")

        nside = self.nside

        # get beam
        beam = self._get_beam(pol)

        # get integral
        omega = np.sum(beam, axis=-1) * np.pi / (3.0 * nside ** 2)

        return omega

    def get_beam_sq_area(self, pol="pI"):
        """
        Compute the integral of the beam^2 in units of steradians.

        Pseudo-Stokes 'pI' (I), 'pQ'(Q), 'pU'(U), 'pV'(V) beam and
        linear dipole 'XX', 'XY', 'YX' and 'YY' are supported.
        See Equations 4 and 5 of Moore et al. (2017) ApJ 836, 154
        or arxiv:1502.05072 for details.

        Parameters
        ----------
        pol : str or int
            polarization string or integer, Ex. a pseudo-stokes pol 'pI', or a
            linear pol 'XX'.

        Returns
        -------
        omega : float
            Integral of the beam^2 across the sky, units: steradians.

        """
        if isinstance(pol, (str, np.str_)):
            pol = uvutils.polstr2num(pol, x_orientation=self.x_orientation)
        if self.beam_type != "power":
            raise ValueError("beam_type must be power")
        if self.Naxes_vec > 1:
            raise ValueError("Expect scalar for power beam, found vector")
        if self._data_normalization.value != "peak":
            raise ValueError("beam must be peak normalized")
        if self.pixel_coordinate_system != "healpix":
            raise ValueError("Currently only healpix format supported")

        nside = self.nside

        # get beam
        beam = self._get_beam(pol)

        # get integral
        omega = np.sum(beam ** 2, axis=-1) * np.pi / (3.0 * nside ** 2)

        return omega

    def __add__(
        self,
        other,
        verbose_history=False,
        inplace=False,
        run_check=True,
        check_extra=True,
        run_check_acceptability=True,
    ):
        """
        Combine two UVBeam objects.

        Objects can be added along frequency, feed or polarization
        (for efield or power beams), and/or pixel axes.

        Parameters
        ----------
        other : UVBeam object
            UVBeam object to add to self.
        inplace : bool
            Option to overwrite self as we go, otherwise create a third object
            as the sum of the two.
        verbose_history : bool
            Option to allow more verbose history. If True and if the histories for the
            two objects are different, the combined object will keep all the history of
            both input objects (if many objects are combined in succession this can
            lead to very long histories). If False and if the histories for the two
            objects are different, the combined object will have the history of the
            first object and only the parts of the second object history that are unique
            (this is done word by word and can result in hard to interpret histories).
        run_check : bool
            Option to check for the existence and proper shapes of
            required 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
            required parameters after combining objects.

        """
        if inplace:
            this = self
        else:
            this = self.copy()
        # Check that both objects are UVBeam and valid
        this.check(check_extra=check_extra, run_check_acceptability=False)
        if not issubclass(other.__class__, this.__class__):
            if not issubclass(this.__class__, other.__class__):
                raise ValueError(
                    "Only UVBeam (or subclass) objects can be added "
                    "to a UVBeam (or subclass) object"
                )
        other.check(check_extra=check_extra, run_check_acceptability=False)

        # Check objects are compatible
        compatibility_params = [
            "_beam_type",
            "_data_normalization",
            "_telescope_name",
            "_feed_name",
            "_feed_version",
            "_model_name",
            "_model_version",
            "_pixel_coordinate_system",
            "_Naxes_vec",
            "_nside",
            "_ordering",
        ]
        for a in compatibility_params:
            if getattr(this, a) != getattr(other, a):
                msg = (
                    "UVParameter " + a[1:] + " does not match. Cannot combine objects."
                )
                raise ValueError(msg)

        # check for presence of optional parameters with a frequency axis in
        # both objects
        optional_freq_params = [
            "_receiver_temperature_array",
            "_loss_array",
            "_mismatch_array",
            "_s_parameters",
        ]
        for attr in optional_freq_params:
            this_attr = getattr(this, attr)
            other_attr = getattr(other, attr)
            if (
                this_attr.value is None or other_attr.value is None
            ) and this_attr != other_attr:
                warnings.warn(
                    "Only one of the UVBeam objects being combined "
                    "has optional parameter {attr}. After the sum the "
                    "final object will not have {attr}".format(attr=attr)
                )
                if this_attr.value is not None:
                    this_attr.value = None
                    setattr(this, attr, this_attr)

        # Build up history string
        history_update_string = " Combined data along "
        n_axes = 0

        # Check we don't have overlapping data
        if this.beam_type == "power":
            both_pol = np.intersect1d(this.polarization_array, other.polarization_array)
        else:
            both_pol = np.intersect1d(this.feed_array, other.feed_array)

        both_freq = np.intersect1d(this.freq_array[0, :], other.freq_array[0, :])

        if this.pixel_coordinate_system == "healpix":
            both_pixels = np.intersect1d(this.pixel_array, other.pixel_array)
        else:
            both_axis1 = np.intersect1d(this.axis1_array, other.axis1_array)
            both_axis2 = np.intersect1d(this.axis2_array, other.axis2_array)

        if len(both_pol) > 0:
            if len(both_freq) > 0:
                if self.pixel_coordinate_system == "healpix":
                    if len(both_pixels) > 0:
                        raise ValueError(
                            "These objects have overlapping data and"
                            " cannot be combined."
                        )
                else:
                    if len(both_axis1) > 0:
                        if len(both_axis2) > 0:
                            raise ValueError(
                                "These objects have overlapping data and"
                                " cannot be combined."
                            )

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

        if this.pixel_coordinate_system == "healpix":
            temp = np.nonzero(~np.in1d(other.pixel_array, this.pixel_array))[0]
            if len(temp) > 0:
                pix_new_inds = temp
                history_update_string += "healpix pixel"
                n_axes += 1
            else:
                pix_new_inds = []
        else:
            temp = np.nonzero(~np.in1d(other.axis1_array, this.axis1_array))[0]
            if len(temp) > 0:
                ax1_new_inds = temp
                history_update_string += "first image"
                n_axes += 1
            else:
                ax1_new_inds = []

            temp = np.nonzero(~np.in1d(other.axis2_array, this.axis2_array))[0]
            if len(temp) > 0:
                ax2_new_inds = temp
                if n_axes > 0:
                    history_update_string += ", second image"
                else:
                    history_update_string += "second image"
                n_axes += 1
            else:
                ax2_new_inds = []

        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 this.beam_type == "power":
            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 = []
        else:
            temp = np.nonzero(~np.in1d(other.feed_array, this.feed_array))[0]
            if len(temp) > 0:
                pnew_inds = temp
                if n_axes > 0:
                    history_update_string += ", feed"
                else:
                    history_update_string += "feed"
                n_axes += 1
            else:
                pnew_inds = []

        # Pad out self to accommodate new data
        if this.pixel_coordinate_system == "healpix":
            if len(pix_new_inds) > 0:
                data_pix_axis = 4
                data_pad_dims = tuple(
                    list(this.data_array.shape[0:data_pix_axis])
                    + [len(pix_new_inds)]
                    + list(this.data_array.shape[data_pix_axis + 1 :])
                )
                data_zero_pad = np.zeros(data_pad_dims)

                this.pixel_array = np.concatenate(
                    [this.pixel_array, other.pixel_array[pix_new_inds]]
                )
                order = np.argsort(this.pixel_array)
                this.pixel_array = this.pixel_array[order]

                this.data_array = np.concatenate(
                    [this.data_array, data_zero_pad], axis=data_pix_axis
                )[:, :, :, :, order]

                if this.beam_type == "efield":
                    basisvec_pix_axis = 2
                    basisvec_pad_dims = tuple(
                        list(this.basis_vector_array.shape[0:basisvec_pix_axis])
                        + [len(pix_new_inds)]
                        + list(this.basis_vector_array.shape[basisvec_pix_axis + 1 :])
                    )
                    basisvec_zero_pad = np.zeros(basisvec_pad_dims)

                    this.basis_vector_array = np.concatenate(
                        [this.basis_vector_array, basisvec_zero_pad],
                        axis=basisvec_pix_axis,
                    )[:, :, order]
        else:
            if len(ax1_new_inds) > 0:
                data_ax1_axis = 5
                data_pad_dims = tuple(
                    list(this.data_array.shape[0:data_ax1_axis])
                    + [len(ax1_new_inds)]
                    + list(this.data_array.shape[data_ax1_axis + 1 :])
                )
                data_zero_pad = np.zeros(data_pad_dims)

                this.axis1_array = np.concatenate(
                    [this.axis1_array, other.axis1_array[ax1_new_inds]]
                )
                order = np.argsort(this.axis1_array)
                this.axis1_array = this.axis1_array[order]
                this.data_array = np.concatenate(
                    [this.data_array, data_zero_pad], axis=data_ax1_axis
                )[:, :, :, :, :, order]

                if this.beam_type == "efield":
                    basisvec_ax1_axis = 3
                    basisvec_pad_dims = tuple(
                        list(this.basis_vector_array.shape[0:basisvec_ax1_axis])
                        + [len(ax1_new_inds)]
                        + list(this.basis_vector_array.shape[basisvec_ax1_axis + 1 :])
                    )
                    basisvec_zero_pad = np.zeros(basisvec_pad_dims)

                    this.basis_vector_array = np.concatenate(
                        [this.basis_vector_array, basisvec_zero_pad],
                        axis=basisvec_ax1_axis,
                    )[:, :, :, order]

            if len(ax2_new_inds) > 0:
                data_ax2_axis = 4
                data_pad_dims = tuple(
                    list(this.data_array.shape[0:data_ax2_axis])
                    + [len(ax2_new_inds)]
                    + list(this.data_array.shape[data_ax2_axis + 1 :])
                )
                data_zero_pad = np.zeros(data_pad_dims)

                this.axis2_array = np.concatenate(
                    [this.axis2_array, other.axis2_array[ax2_new_inds]]
                )
                order = np.argsort(this.axis2_array)
                this.axis2_array = this.axis2_array[order]

                this.data_array = np.concatenate(
                    [this.data_array, data_zero_pad], axis=data_ax2_axis
                )[:, :, :, :, order, ...]

                if this.beam_type == "efield":
                    basisvec_ax2_axis = 2
                    basisvec_pad_dims = tuple(
                        list(this.basis_vector_array.shape[0:basisvec_ax2_axis])
                        + [len(ax2_new_inds)]
                        + list(this.basis_vector_array.shape[basisvec_ax2_axis + 1 :])
                    )
                    basisvec_zero_pad = np.zeros(basisvec_pad_dims)

                    this.basis_vector_array = np.concatenate(
                        [this.basis_vector_array, basisvec_zero_pad],
                        axis=basisvec_ax2_axis,
                    )[:, :, order, ...]

        if len(fnew_inds) > 0:
            faxis = 3
            data_pad_dims = tuple(
                list(this.data_array.shape[0:faxis])
                + [len(fnew_inds)]
                + list(this.data_array.shape[faxis + 1 :])
            )
            data_zero_pad = np.zeros(data_pad_dims)

            this.freq_array = np.concatenate(
                [this.freq_array, other.freq_array[:, fnew_inds]], axis=1
            )
            order = np.argsort(this.freq_array[0, :])
            this.freq_array = this.freq_array[:, order]

            this.bandpass_array = np.concatenate(
                [this.bandpass_array, np.zeros((1, len(fnew_inds)))], axis=1
            )[:, order]

            this.data_array = np.concatenate(
                [this.data_array, data_zero_pad], axis=faxis
            )[:, :, :, order, ...]
            if this.receiver_temperature_array is not None:
                this.receiver_temperature_array = np.concatenate(
                    [this.receiver_temperature_array, np.zeros((1, len(fnew_inds)))],
                    axis=1,
                )[:, order]
            if this.loss_array is not None:
                this.loss_array = np.concatenate(
                    [this.loss_array, np.zeros((1, len(fnew_inds)))], axis=1
                )[:, order]
            if this.mismatch_array is not None:
                this.mismatch_array = np.concatenate(
                    [this.mismatch_array, np.zeros((1, len(fnew_inds)))], axis=1
                )[:, order]
            if this.s_parameters is not None:
                this.s_parameters = np.concatenate(
                    [this.s_parameters, np.zeros((4, 1, len(fnew_inds)))], axis=2
                )[:, :, order]

        if len(pnew_inds) > 0:
            paxis = 2
            data_pad_dims = tuple(
                list(this.data_array.shape[0:paxis])
                + [len(pnew_inds)]
                + list(this.data_array.shape[paxis + 1 :])
            )
            data_zero_pad = np.zeros(data_pad_dims)

            if this.beam_type == "power":
                this.polarization_array = np.concatenate(
                    [this.polarization_array, other.polarization_array[pnew_inds]]
                )
                order = np.argsort(np.abs(this.polarization_array))
                this.polarization_array = this.polarization_array[order]
            else:
                this.feed_array = np.concatenate(
                    [this.feed_array, other.feed_array[pnew_inds]]
                )
                order = np.argsort(this.feed_array)
                this.feed_array = this.feed_array[order]

            this.data_array = np.concatenate(
                [this.data_array, data_zero_pad], axis=paxis
            )[:, :, order, ...]

        # Now populate the data
        if this.beam_type == "power":
            this.Npols = this.polarization_array.shape[0]
            pol_t2o = np.nonzero(
                np.in1d(this.polarization_array, other.polarization_array)
            )[0]
        else:
            this.Nfeeds = this.feed_array.shape[0]
            pol_t2o = np.nonzero(np.in1d(this.feed_array, other.feed_array))[0]

        freq_t2o = np.nonzero(np.in1d(this.freq_array[0, :], other.freq_array[0, :]))[0]

        if this.pixel_coordinate_system == "healpix":
            this.Npixels = this.pixel_array.shape[0]
            pix_t2o = np.nonzero(np.in1d(this.pixel_array, other.pixel_array))[0]
            this.data_array[
                np.ix_(np.arange(this.Naxes_vec), [0], pol_t2o, freq_t2o, pix_t2o)
            ] = other.data_array
            if this.beam_type == "efield":
                this.basis_vector_array[
                    np.ix_(np.arange(this.Naxes_vec), np.arange(2), pix_t2o)
                ] = other.basis_vector_array
        else:
            this.Naxes1 = this.axis1_array.shape[0]
            this.Naxes2 = this.axis2_array.shape[0]
            ax1_t2o = np.nonzero(np.in1d(this.axis1_array, other.axis1_array))[0]
            ax2_t2o = np.nonzero(np.in1d(this.axis2_array, other.axis2_array))[0]
            this.data_array[
                np.ix_(
                    np.arange(this.Naxes_vec), [0], pol_t2o, freq_t2o, ax2_t2o, ax1_t2o
                )
            ] = other.data_array
            if this.beam_type == "efield":
                this.basis_vector_array[
                    np.ix_(np.arange(this.Naxes_vec), np.arange(2), ax2_t2o, ax1_t2o)
                ] = other.basis_vector_array

        this.bandpass_array[np.ix_([0], freq_t2o)] = other.bandpass_array

        if this.receiver_temperature_array is not None:
            this.receiver_temperature_array[
                np.ix_([0], freq_t2o)
            ] = other.receiver_temperature_array
        if this.loss_array is not None:
            this.loss_array[np.ix_([0], freq_t2o)] = other.loss_array
        if this.mismatch_array is not None:
            this.mismatch_array[np.ix_([0], freq_t2o)] = other.mismatch_array
        if this.s_parameters is not None:
            this.s_parameters[np.ix_(np.arange(4), [0], freq_t2o)] = other.s_parameters

        this.Nfreqs = this.freq_array.shape[1]

        # Check specific requirements
        if this.Nfreqs > 1:
            freq_separation = np.diff(this.freq_array[0, :])
            if not np.isclose(
                np.min(freq_separation),
                np.max(freq_separation),
                rtol=this._freq_array.tols[0],
                atol=this._freq_array.tols[1],
            ):
                warnings.warn(
                    "Combined frequencies are not evenly spaced. This will "
                    "make it impossible to write this data out to some file types."
                )

        if self.beam_type == "power" and this.Npols > 2:
            pol_separation = np.diff(this.polarization_array)
            if np.min(pol_separation) < np.max(pol_separation):
                warnings.warn(
                    "Combined polarizations are not evenly spaced. This will "
                    "make it impossible to write this data out to some file types."
                )

        if n_axes > 0:
            history_update_string += " axis using pyuvdata."
            histories_match = uvutils._check_histories(this.history, other.history)

            this.history += history_update_string
            if not histories_match:
                if verbose_history:
                    this.history += " Next object history follows. " + other.history
                else:
                    extra_history = uvutils._combine_history_addition(
                        this.history, other.history
                    )
                    if extra_history is not None:
                        this.history += (
                            " Unique part of next object history follows. "
                            + extra_history
                        )

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

        if not inplace:
            return this

    def __iadd__(self, other):
        """
        Add in place.

        Parameters
        ----------
        other : UVBeam object
            Another UVBeam object to adding to self.
        """
        self.__add__(other, inplace=True)
        return self

    def select(
        self,
        axis1_inds=None,
        axis2_inds=None,
        pixels=None,
        frequencies=None,
        freq_chans=None,
        feeds=None,
        polarizations=None,
        inplace=True,
        run_check=True,
        check_extra=True,
        run_check_acceptability=True,
    ):
        """
        Downselect data to keep on the object along various axes.

        Axes that can be selected along include image axis indices or pixels
        (if healpix), frequencies and feeds or polarizations (if power).

        The history attribute on the object will be updated to identify the
        operations performed.

        Parameters
        ----------
        axis1_indss : array_like of int, optional
            The indices along the first image axis to keep in the object.
            Cannot be set if pixel_coordinate_system is "healpix".
        axis2_inds : array_like of int, optional
            The indices along the second image axis to keep in the object.
            Cannot be set if pixel_coordinate_system is "healpix".
        pixels : array_like of int, optional
            The healpix pixels to keep in the object.
            Cannot be set if pixel_coordinate_system is not "healpix".
        frequencies : array_like of float, optional
            The frequencies to keep in the object.
        freq_chans : array_like of int, optional
            The frequency channel numbers to keep in the object.
        feeds : array_like of str, optional
            The feeds to keep in the object. If the `x_orientation` attribute is set,
            the physical dipole strings (e.g. "n", "e") are also supported.
            Cannot be set if the beam_type is "power".
        polarizations : array_like of int or str, optional
            The polarizations to keep in the object.
            Cannot be set if the beam_type is "efield". 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.
        inplace : bool
            Option to perform the select directly on self or return
            a new UVBeam object, which is a subselection of self.
        run_check : bool
            Option to check for the existence and proper shapes of
            required parameters after downselecting data on this object.
        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
            required parameters after  downselecting data on this object.

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

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

        if axis1_inds is not None:
            if beam_object.pixel_coordinate_system == "healpix":
                raise ValueError(
                    "axis1_inds cannot be used with healpix coordinate system"
                )

            history_update_string += "parts of first image axis"
            n_selects += 1

            axis1_inds = sorted(set(axis1_inds))
            if min(axis1_inds) < 0 or max(axis1_inds) > beam_object.Naxes1 - 1:
                raise ValueError("axis1_inds must be > 0 and < Naxes1")
            beam_object.Naxes1 = len(axis1_inds)
            beam_object.axis1_array = beam_object.axis1_array[axis1_inds]

            if beam_object.Naxes1 > 1:
                axis1_spacing = np.diff(beam_object.axis1_array)
                if not np.isclose(
                    np.min(axis1_spacing),
                    np.max(axis1_spacing),
                    rtol=beam_object._axis1_array.tols[0],
                    atol=beam_object._axis1_array.tols[1],
                ):
                    warnings.warn(
                        "Selected values along first image axis are "
                        "not evenly spaced. This is not supported by "
                        "the regularly gridded beam fits format"
                    )

            beam_object.data_array = beam_object.data_array[:, :, :, :, :, axis1_inds]
            if beam_object.beam_type == "efield":
                beam_object.basis_vector_array = beam_object.basis_vector_array[
                    :, :, :, axis1_inds
                ]

        if axis2_inds is not None:
            if beam_object.pixel_coordinate_system == "healpix":
                raise ValueError(
                    "axis2_inds cannot be used with healpix coordinate system"
                )

            if n_selects > 0:
                history_update_string += ", parts of second image axis"
            else:
                history_update_string += "parts of second image axis"
            n_selects += 1

            axis2_inds = sorted(set(axis2_inds))
            if min(axis2_inds) < 0 or max(axis2_inds) > beam_object.Naxes2 - 1:
                raise ValueError("axis2_inds must be > 0 and < Naxes2")
            beam_object.Naxes2 = len(axis2_inds)
            beam_object.axis2_array = beam_object.axis2_array[axis2_inds]

            if beam_object.Naxes2 > 1:
                axis2_spacing = np.diff(beam_object.axis2_array)
                if not np.isclose(
                    np.min(axis2_spacing),
                    np.max(axis2_spacing),
                    rtol=beam_object._axis2_array.tols[0],
                    atol=beam_object._axis2_array.tols[1],
                ):
                    warnings.warn(
                        "Selected values along second image axis are "
                        "not evenly spaced. This is not supported by "
                        "the regularly gridded beam fits format"
                    )

            beam_object.data_array = beam_object.data_array[:, :, :, :, axis2_inds, :]
            if beam_object.beam_type == "efield":
                beam_object.basis_vector_array = beam_object.basis_vector_array[
                    :, :, axis2_inds, :
                ]

        if pixels is not None:
            if beam_object.pixel_coordinate_system != "healpix":
                raise ValueError(
                    "pixels can only be used with healpix coordinate system"
                )

            history_update_string += "healpix pixels"
            n_selects += 1

            pix_inds = np.zeros(0, dtype=np.int64)
            for p in pixels:
                if p in beam_object.pixel_array:
                    pix_inds = np.append(
                        pix_inds, np.where(beam_object.pixel_array == p)[0]
                    )
                else:
                    raise ValueError(
                        "Pixel {p} is not present in the pixel_array".format(p=p)
                    )

            pix_inds = sorted(set(pix_inds))
            beam_object.Npixels = len(pix_inds)
            beam_object.pixel_array = beam_object.pixel_array[pix_inds]

            beam_object.data_array = beam_object.data_array[:, :, :, :, pix_inds]
            if beam_object.beam_type == "efield":
                beam_object.basis_vector_array = beam_object.basis_vector_array[
                    :, :, pix_inds
                ]

        if freq_chans is not None:
            freq_chans = uvutils._get_iterable(freq_chans)
            if frequencies is None:
                frequencies = beam_object.freq_array[0, freq_chans]
            else:
                frequencies = uvutils._get_iterable(frequencies)
                frequencies = np.sort(
                    list(set(frequencies) | set(beam_object.freq_array[0, freq_chans]))
                )

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

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

            freq_inds = sorted(set(freq_inds))
            beam_object.Nfreqs = len(freq_inds)
            beam_object.freq_array = beam_object.freq_array[:, freq_inds]
            beam_object.bandpass_array = beam_object.bandpass_array[:, freq_inds]

            if beam_object.Nfreqs > 1:
                freq_separation = (
                    beam_object.freq_array[0, 1:] - beam_object.freq_array[0, :-1]
                )
                if not np.isclose(
                    np.min(freq_separation),
                    np.max(freq_separation),
                    rtol=beam_object._freq_array.tols[0],
                    atol=beam_object._freq_array.tols[1],
                ):
                    warnings.warn(
                        "Selected frequencies are not evenly spaced. This "
                        "is not supported by the regularly gridded beam fits format"
                    )

            if beam_object.pixel_coordinate_system == "healpix":
                beam_object.data_array = beam_object.data_array[:, :, :, freq_inds, :]
            else:
                beam_object.data_array = beam_object.data_array[
                    :, :, :, freq_inds, :, :
                ]

            if beam_object.antenna_type == "phased_array":
                beam_object.coupling_matrix = beam_object.coupling_matrix[
                    :, :, :, :, :, freq_inds
                ]

            if beam_object.receiver_temperature_array is not None:
                rx_temp_array = beam_object.receiver_temperature_array
                beam_object.receiver_temperature_array = rx_temp_array[:, freq_inds]

            if beam_object.loss_array is not None:
                beam_object.loss_array = beam_object.loss_array[:, freq_inds]

            if beam_object.mismatch_array is not None:
                beam_object.mismatch_array = beam_object.mismatch_array[:, freq_inds]

            if beam_object.s_parameters is not None:
                beam_object.s_parameters = beam_object.s_parameters[:, :, freq_inds]

        if feeds is not None:
            if beam_object.beam_type == "power":
                raise ValueError("feeds cannot be used with power beams")
            x_orient_dict = {}
            if beam_object.x_orientation is not None:
                for key, value in uvutils._x_orientation_rep_dict(
                    beam_object.x_orientation
                ).items():
                    if key in beam_object.feed_array:
                        x_orient_dict[value] = key

            feeds = uvutils._get_iterable(feeds)
            feeds = [f.lower() for f in feeds]
            if n_selects > 0:
                history_update_string += ", feeds"
            else:
                history_update_string += "feeds"
            n_selects += 1

            feed_inds = np.zeros(0, dtype=np.int64)
            for f in feeds:
                if f in beam_object.feed_array:
                    feed_inds = np.append(
                        feed_inds, np.where(beam_object.feed_array == f)[0]
                    )
                elif f in x_orient_dict.keys():
                    feed_inds = np.append(
                        feed_inds,
                        np.where(beam_object.feed_array == x_orient_dict[f])[0],
                    )
                else:
                    raise ValueError(
                        "Feed {f} is not present in the feed_array".format(f=f)
                    )

            feed_inds = sorted(set(feed_inds))
            beam_object.Nfeeds = len(feed_inds)
            beam_object.feed_array = beam_object.feed_array[feed_inds]

            if beam_object.pixel_coordinate_system == "healpix":
                beam_object.data_array = beam_object.data_array[:, :, feed_inds, :, :]
            else:
                beam_object.data_array = beam_object.data_array[
                    :, :, feed_inds, :, :, :
                ]

        if polarizations is not None:
            if beam_object.beam_type == "efield":
                raise ValueError("polarizations cannot be used with efield beams")

            polarizations = uvutils._get_iterable(polarizations)
            if np.array(polarizations).ndim > 1:
                polarizations = np.array(polarizations).flatten()

            if n_selects > 0:
                history_update_string += ", polarizations"
            else:
                history_update_string += "polarizations"
            n_selects += 1

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

            pol_inds = sorted(set(pol_inds))
            beam_object.Npols = len(pol_inds)
            beam_object.polarization_array = beam_object.polarization_array[pol_inds]

            if len(pol_inds) > 2:
                pol_separation = (
                    beam_object.polarization_array[1:]
                    - beam_object.polarization_array[:-1]
                )
                if np.min(pol_separation) < np.max(pol_separation):
                    warnings.warn(
                        "Selected polarizations are not evenly spaced. This "
                        "is not supported by the regularly gridded beam fits format"
                    )

            if beam_object.pixel_coordinate_system == "healpix":
                beam_object.data_array = beam_object.data_array[:, :, pol_inds, :, :]
            else:
                beam_object.data_array = beam_object.data_array[:, :, pol_inds, :, :, :]

        history_update_string += " using pyuvdata."
        beam_object.history = beam_object.history + history_update_string

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

        if not inplace:
            return beam_object

    def _convert_from_filetype(self, other):
        for p in other:
            param = getattr(other, p)
            setattr(self, p, param)

    def _convert_to_filetype(self, filetype):
        if filetype == "beamfits":
            from . import beamfits

            other_obj = beamfits.BeamFITS()
        else:
            raise ValueError("filetype must be beamfits")
        for p in self:
            param = getattr(self, p)
            setattr(other_obj, p, param)
        return other_obj

    def read_beamfits(
        self, filename, run_check=True, check_extra=True, run_check_acceptability=True
    ):
        """
        Read in data from a beamfits file.

        Parameters
        ----------
        filename : str or list of str
            The beamfits file or list of files to read from.
        run_check : bool
            Option to check for the existence and proper shapes of
            required parameters after reading in the file.
        check_extra : bool
            Option to check optional parameters as well as required ones.
        run_check_acceptabilit : bool
            Option to check acceptable range of the values of
            required parameters after reading in the file.

        """
        from . import beamfits

        if isinstance(filename, (list, tuple)):
            self.read_beamfits(
                filename[0],
                run_check=run_check,
                check_extra=check_extra,
                run_check_acceptability=run_check_acceptability,
            )
            if len(filename) > 1:
                for f in filename[1:]:
                    beam2 = UVBeam()
                    beam2.read_beamfits(
                        f,
                        run_check=run_check,
                        check_extra=check_extra,
                        run_check_acceptability=run_check_acceptability,
                    )
                    self += beam2
                del beam2
        else:
            beamfits_obj = beamfits.BeamFITS()
            beamfits_obj.read_beamfits(
                filename,
                run_check=run_check,
                check_extra=check_extra,
                run_check_acceptability=run_check_acceptability,
            )
            self._convert_from_filetype(beamfits_obj)
            del beamfits_obj

    def _read_cst_beam_yaml(self, filename):
        """
        Parse a CST beam yaml file.

        Paramters
        ---------
        filename : str
            Filename to parse.

        Returns
        -------
        dict
            Containing all the info from the yaml file.

        """
        import yaml

        with open(filename, "r") as file:
            settings_dict = yaml.safe_load(file)

        required_keys = [
            "telescope_name",
            "feed_name",
            "feed_version",
            "model_name",
            "model_version",
            "history",
            "frequencies",
            "filenames",
            "feed_pol",
        ]

        for key in required_keys:
            if key not in settings_dict:
                raise ValueError(
                    "{key} is a required key in CST settings files "
                    "but is not present.".format(key=key)
                )

        return settings_dict

    def read_cst_beam(
        self,
        filename,
        beam_type="power",
        feed_pol=None,
        rotate_pol=None,
        frequency=None,
        telescope_name=None,
        feed_name=None,
        feed_version=None,
        model_name=None,
        model_version=None,
        history=None,
        x_orientation=None,
        reference_impedance=None,
        extra_keywords=None,
        frequency_select=None,
        run_check=True,
        check_extra=True,
        run_check_acceptability=True,
    ):
        """
        Read in data from a cst file.

        Parameters
        ----------
        filename : str
            Either a settings yaml file or a cst text file or
            list of cst text files to read from. If a list is passed,
            the files are combined along the appropriate axes.

            Settings yaml files must include the following keywords:

                |  - telescope_name (str)
                |  - feed_name (str)
                |  - feed_version (str)
                |  - model_name (str)
                |  - model_version (str)
                |  - history (str)
                |  - frequencies (list(float))
                |  - cst text filenames (list(str)) -- path relative to yaml file
                |  - feed_pol (str) or (list(str))

            and they may include the following optional keywords:

                |  - x_orientation (str): Optional but strongly encouraged!
                |  - ref_imp (float): beam model reference impedance
                |  - sim_beam_type (str): e.g. 'E-farfield'
                |  - all other fields will go into the extra_keywords attribute

            More details and an example are available in the docs
            (cst_settings_yaml.rst).
            Specifying any of the associated keywords to this function will
            override the values in the settings file.
        beam_type : str
            What beam_type to read in ('power' or 'efield').
        feed_pol : str
            The feed or polarization or list of feeds or polarizations the
            files correspond to.
            Defaults to 'x' (meaning x for efield or xx for power beams).
        rotate_pol : bool
            If True, assume the structure in the simulation is symmetric under
            90 degree rotations about the z-axis (so that the y polarization can be
            constructed by rotating the x polarization or vice versa).
            Default: True if feed_pol is a single value or a list with all
            the same values in it, False if it is a list with varying values.
        frequency : float or list of float, optional
            The frequency or list of frequencies corresponding to the filename(s).
            This is assumed to be in the same order as the files.
            If not passed, the code attempts to parse it from the filenames.
        telescope_name : str, optional
            The name of the telescope corresponding to the filename(s).
        feed_name : str, optional
            The name of the feed corresponding to the filename(s).
        feed_version : str, optional
            The version of the feed corresponding to the filename(s).
        model_name : str, optional
            The name of the model corresponding to the filename(s).
        model_version : str, optional
            The version of the model corresponding to the filename(s).
        history : str, optional
            A string detailing the history of the filename(s).
        x_orientation : str, optional
            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)
        reference_impedance : float, optional
            The reference impedance of the model(s).
        extra_keywords : dict, optional
            A dictionary containing any extra_keywords.
        frequency_select : list of float, optional
            Only used if the file is a yaml file. Indicates which frequencies
            to include (only read in files for those frequencies)
        run_check : bool
            Option to check for the existence and proper shapes of
            required parameters after reading in the file.
        check_extra : bool
            Option to check optional parameters as well as
            required ones.
        run_check_acceptability : bool
            Option to check acceptable range of the values of
            required parameters after reading in the file.

        """
        from . import cst_beam

        if isinstance(filename, np.ndarray):
            if len(filename.shape) > 1:
                raise ValueError("filename can not be a multi-dimensional array")
            filename = filename.tolist()
        if isinstance(filename, (list, tuple)):
            if len(filename) == 1:
                filename = filename[0]

        if not isinstance(filename, (list, tuple)) and filename.endswith("yaml"):
            settings_dict = self._read_cst_beam_yaml(filename)
            if not isinstance(settings_dict["filenames"], list):
                raise ValueError("filenames in yaml file must be a list.")
            if not isinstance(settings_dict["frequencies"], list):
                raise ValueError("frequencies in yaml file must be a list.")
            yaml_dir = os.path.dirname(filename)
            cst_filename = [
                os.path.join(yaml_dir, f) for f in settings_dict["filenames"]
            ]

            overriding_keywords = {
                "feed_pol": feed_pol,
                "frequency": frequency,
                "telescope_name": telescope_name,
                "feed_name": feed_name,
                "feed_version": feed_version,
                "model_name": model_name,
                "model_version": model_version,
                "history": history,
            }
            if "ref_imp" in settings_dict:
                overriding_keywords["reference_impedance"] = reference_impedance
            if "x_orientation" in settings_dict:
                overriding_keywords["x_orientation"] = reference_impedance
            for key, val in overriding_keywords.items():
                if val is not None:
                    warnings.warn(
                        "The {key} keyword is set, overriding the "
                        "value in the settings yaml file.".format(key=key)
                    )

            if feed_pol is None:
                feed_pol = settings_dict["feed_pol"]
            if frequency is None:
                frequency = settings_dict["frequencies"]
            if telescope_name is None:
                telescope_name = settings_dict["telescope_name"]
            if feed_name is None:
                feed_name = settings_dict["feed_name"]
            if feed_version is None:
                feed_version = str(settings_dict["feed_version"])
            if model_name is None:
                model_name = settings_dict["model_name"]
            if model_version is None:
                model_version = str(settings_dict["model_version"])
            if history is None:
                history = settings_dict["history"]
            if reference_impedance is None and "ref_imp" in settings_dict:
                reference_impedance = float(settings_dict["ref_imp"])
            if x_orientation is None and "x_orientation" in settings_dict:
                x_orientation = settings_dict["x_orientation"]

            if extra_keywords is None:
                extra_keywords = {}

            known_keys = [
                "telescope_name",
                "feed_name",
                "feed_version",
                "model_name",
                "model_version",
                "history",
                "frequencies",
                "filenames",
                "feed_pol",
                "ref_imp",
                "x_orientation",
            ]
            # One of the standard paramters in the settings yaml file is
            # longer than 8 characters.
            # This causes warnings and straight truncation when writing to
            # beamfits files
            # To avoid these, this defines a standard renaming of that paramter
            rename_extra_keys_map = {"sim_beam_type": "sim_type"}
            for key, value in settings_dict.items():
                if key not in known_keys:
                    if key in rename_extra_keys_map.keys():
                        extra_keywords[rename_extra_keys_map[key]] = value
                    else:
                        extra_keywords[key] = value

            if frequency_select is not None:
                freq_inds = []
                for freq in frequency_select:
                    freq_array = np.array(frequency, dtype=np.float64)
                    close_inds = np.where(
                        np.isclose(
                            freq_array,
                            freq,
                            rtol=self._freq_array.tols[0],
                            atol=self._freq_array.tols[1],
                        )
                    )[0]
                    if close_inds.size > 0:
                        for ind in close_inds:
                            freq_inds.append(ind)
                    else:
                        raise ValueError(f"frequency {freq} not in frequency list")
                freq_inds = np.array(freq_inds)
                frequency = freq_array[freq_inds].tolist()
                cst_filename = np.array(cst_filename)[freq_inds].tolist()
                if len(cst_filename) == 1:
                    cst_filename = cst_filename[0]
                if isinstance(feed_pol, list):
                    if rotate_pol is None:
                        # if a mix of feed pols, don't rotate by default
                        # do this here in case selections confuse this test
                        if np.any(np.array(feed_pol) != feed_pol[0]):
                            rotate_pol = False
                        else:
                            rotate_pol = True
                    feed_pol = np.array(feed_pol)[freq_inds].tolist()

        else:
            cst_filename = filename

        if feed_pol is None:
            # default to x (done here in case it's specified in a yaml file)
            feed_pol = "x"
        if history is None:
            # default to empty (done here in case it's specified in a yaml file)
            history = ""

        if isinstance(frequency, np.ndarray):
            if len(frequency.shape) > 1:
                raise ValueError("frequency can not be a multi-dimensional array")
            frequency = frequency.tolist()
        if isinstance(frequency, (list, tuple)):
            if len(frequency) == 1:
                frequency = frequency[0]

        if isinstance(feed_pol, np.ndarray):
            if len(feed_pol.shape) > 1:
                raise ValueError("frequency can not be a multi-dimensional array")
            feed_pol = feed_pol.tolist()
        if isinstance(feed_pol, (list, tuple)):
            if len(feed_pol) == 1:
                feed_pol = feed_pol[0]

        if isinstance(cst_filename, (list, tuple)):
            if frequency is not None:
                if isinstance(frequency, (list, tuple)):
                    if not len(frequency) == len(cst_filename):
                        raise ValueError(
                            "If frequency and filename are both "
                            "lists they need to be the same length"
                        )
                    freq = frequency[0]
                else:
                    freq = frequency
            else:
                freq = None

            if isinstance(feed_pol, (list, tuple)):
                if not len(feed_pol) == len(cst_filename):
                    raise ValueError(
                        "If feed_pol and filename are both "
                        "lists they need to be the same length"
                    )
                pol = feed_pol[0]
                if rotate_pol is None:
                    # if a mix of feed pols, don't rotate by default
                    if np.any(np.array(feed_pol) != feed_pol[0]):
                        rotate_pol = False
                    else:
                        rotate_pol = True
            else:
                pol = feed_pol
                if rotate_pol is None:
                    rotate_pol = True
            if isinstance(freq, (list, tuple)):
                raise ValueError("frequency can not be a nested list")
            if isinstance(pol, (list, tuple)):
                raise ValueError("feed_pol can not be a nested list")
            self.read_cst_beam(
                cst_filename[0],
                beam_type=beam_type,
                feed_pol=pol,
                rotate_pol=rotate_pol,
                frequency=freq,
                telescope_name=telescope_name,
                feed_name=feed_name,
                feed_version=feed_version,
                model_name=model_name,
                model_version=model_version,
                history=history,
                x_orientation=x_orientation,
                reference_impedance=reference_impedance,
                extra_keywords=extra_keywords,
                run_check=run_check,
                check_extra=check_extra,
                run_check_acceptability=run_check_acceptability,
            )
            for file_i, f in enumerate(cst_filename[1:]):
                if isinstance(f, (list, tuple)):
                    raise ValueError("filename can not be a nested list")

                if isinstance(frequency, (list, tuple)):
                    freq = frequency[file_i + 1]
                elif frequency is not None:
                    freq = frequency
                else:
                    freq = None
                if isinstance(feed_pol, (list, tuple)):
                    pol = feed_pol[file_i + 1]
                else:
                    pol = feed_pol
                beam2 = UVBeam()
                beam2.read_cst_beam(
                    f,
                    beam_type=beam_type,
                    feed_pol=pol,
                    rotate_pol=rotate_pol,
                    frequency=freq,
                    telescope_name=telescope_name,
                    feed_name=feed_name,
                    feed_version=feed_version,
                    model_name=model_name,
                    model_version=model_version,
                    history=history,
                    x_orientation=x_orientation,
                    reference_impedance=reference_impedance,
                    extra_keywords=extra_keywords,
                    run_check=run_check,
                    check_extra=check_extra,
                    run_check_acceptability=run_check_acceptability,
                )
                self += beam2
            if len(cst_filename) > 1:
                del beam2
        else:
            if isinstance(frequency, (list, tuple)):
                raise ValueError("Too many frequencies specified")
            if isinstance(feed_pol, (list, tuple)):
                raise ValueError("Too many feed_pols specified")
            if rotate_pol is None:
                rotate_pol = True
            cst_beam_obj = cst_beam.CSTBeam()
            cst_beam_obj.read_cst_beam(
                cst_filename,
                beam_type=beam_type,
                feed_pol=feed_pol,
                rotate_pol=rotate_pol,
                frequency=frequency,
                telescope_name=telescope_name,
                feed_name=feed_name,
                feed_version=feed_version,
                model_name=model_name,
                model_version=model_version,
                history=history,
                x_orientation=x_orientation,
                reference_impedance=reference_impedance,
                extra_keywords=extra_keywords,
                run_check=run_check,
                check_extra=check_extra,
                run_check_acceptability=run_check_acceptability,
            )
            self._convert_from_filetype(cst_beam_obj)
            del cst_beam_obj

        if not isinstance(filename, (list, tuple)) and filename.endswith("yaml"):
            # update filelist
            basename = os.path.basename(filename)
            self.filename = uvutils._combine_filenames(self.filename, [basename])
            self._filename.form = (len(self.filename),)

    def read_mwa_beam(
        self,
        h5filepath,
        delays=None,
        amplitudes=None,
        pixels_per_deg=5,
        freq_range=None,
        run_check=True,
        check_extra=True,
        run_check_acceptability=True,
    ):
        """
        Read in the full embedded element MWA beam.

        Note that the azimuth convention in for the UVBeam object is different than the
        azimuth convention in the mwa_pb repo. In that repo, the azimuth convention is
        changed from the native FEKO convention (the FEKO convention is the same as the
        UVBeam convention). The convention in the mwa_pb repo has a different zero point
        and a different direction (so it is in a left handed coordinate system).

        Parameters
        ----------
        h5filepath : str
            path to input h5 file containing the MWA full embedded element spherical
            harmonic modes. Download via
            `wget http://cerberus.mwa128t.org/mwa_full_embedded_element_pattern.h5`
        delays : array of ints
            Array of MWA beamformer delay steps. Should be shape (n_pols, n_dipoles).
        amplitudes : array of floats
            Array of dipole amplitudes, these are absolute values
            (i.e. relatable to physical units).
            Should be shape (n_pols, n_dipoles).
        pixels_per_deg : float
            Number of theta/phi pixels per degree. Sets the resolution of the beam.
        freq_range : array_like of float
            Range of frequencies to include in Hz, defaults to all available
            frequencies. Must be length 2.
        run_check : bool
            Option to check for the existence and proper shapes of
            required parameters after reading in the file.
        check_extra : bool
            Option to check optional parameters as well as required ones.
        run_check_acceptability : bool
            Option to check acceptable range of the values of
            required parameters after reading in the file.

        """
        from . import mwa_beam

        mwabeam_obj = mwa_beam.MWABeam()
        mwabeam_obj.read_mwa_beam(
            h5filepath,
            delays=delays,
            amplitudes=amplitudes,
            pixels_per_deg=pixels_per_deg,
            freq_range=freq_range,
            run_check=run_check,
            check_extra=check_extra,
            run_check_acceptability=run_check_acceptability,
        )
        self._convert_from_filetype(mwabeam_obj)
        del mwabeam_obj

    def read(
        self,
        filename,
        file_type=None,
        skip_bad_files=False,
        # cst beam settings
        beam_type="power",
        feed_pol=None,
        rotate_pol=None,
        frequency=None,
        telescope_name=None,
        feed_name=None,
        feed_version=None,
        model_name=None,
        model_version=None,
        history=None,
        x_orientation=None,
        reference_impedance=None,
        extra_keywords=None,
        frequency_select=None,
        # mwa beam settings
        delays=None,
        amplitudes=None,
        pixels_per_deg=5,
        freq_range=None,
        # generic checks
        run_check=True,
        check_extra=True,
        run_check_acceptability=True,
    ):
        """
        Read a generic file into a UVBeam object.

        Some parameters only apply to certain file types.

        Parameters
        ----------
        filename : str or array_like of str
            The file(s) or list(s) (or array(s)) of files to read from.

            For cst yaml files only:

            Settings yaml files must include the following keywords:

                |  - telescope_name (str)
                |  - feed_name (str)
                |  - feed_version (str)
                |  - model_name (str)
                |  - model_version (str)
                |  - history (str)
                |  - frequencies (list(float))
                |  - cst text filenames (list(str)) -- path relative to yaml file
                |  - feed_pol (str) or (list(str))

            and they may include the following optional keywords:

                |  - x_orientation (str): Optional but strongly encouraged!
                |  - ref_imp (float): beam model reference impedance
                |  - sim_beam_type (str): e.g. 'E-farfield'
                |  - all other fields will go into the extra_keywords attribute

            More details and an example are available in the docs
            (cst_settings_yaml.rst).
            Specifying any of the associated keywords to this function will
            override the values in the settings file.

        file_type : str
            One of ['mwa_beam', 'beamfits', 'cst'] or None.
            If None, the code attempts to guess what the file type is.
            based on file extensions
            (mwa_beam: .hdf5, .h5; cst: .yaml, .txt; beamfits: .fits, .beamfits).
            Note that if a list of datasets is passed, the file type is
            determined from the first dataset.
        beam_type : str
            What beam_type to read in ('power' or 'efield').
            Only applies to cst file types.
        feed_pol : str
            The feed or polarization or list of feeds or polarizations the
            files correspond to.
            Defaults to 'x' (meaning x for efield or xx for power beams).
            Only applies to cst file types.
        rotate_pol : bool
            If True, assume the structure in the simulation is symmetric under
            90 degree rotations about the z-axis (so that the y polarization can be
            constructed by rotating the x polarization or vice versa).
            Default: True if feed_pol is a single value or a list with all
            the same values in it, False if it is a list with varying values.
            Only applies to cst file types.
        frequency : float or list of float, optional
            The frequency or list of frequencies corresponding to the filename(s).
            This is assumed to be in the same order as the files.
            If not passed, the code attempts to parse it from the filenames.
            Only applies to cst file types.
        telescope_name : str, optional
            The name of the telescope corresponding to the filename(s).
            Only applies to cst file types.
        feed_name : str, optional
            The name of the feed corresponding to the filename(s).
            Only applies to cst file types.
        feed_version : str, optional
            The version of the feed corresponding to the filename(s).
            Only applies to cst file types.
        model_name : str, optional
            The name of the model corresponding to the filename(s).
            Only applies to cst file types.
        model_version : str, optional
            The version of the model corresponding to the filename(s).
            Only applies to cst file types.
        history : str, optional
            A string detailing the history of the filename(s).
            Only applies to cst file types.
        x_orientation : str, optional
            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)
            Only applies to cst file types.
        reference_impedance : float, optional
            The reference impedance of the model(s).
            Only applies to cst file types.
        extra_keywords : dict, optional
            A dictionary containing any extra_keywords.
            Only applies to cst file types.
        frequency_select : list of float, optional
            Only used if the file is a yaml file. Indicates which frequencies
            to include (only read in files for those frequencies)
            Only applies to cst file types.
        delays : array of ints
            Array of MWA beamformer delay steps. Should be shape (n_pols, n_dipoles).
            Only applies to mwa_beam type files.
        amplitudes : array of floats
            Array of dipole amplitudes, these are absolute values
            (i.e. relatable to physical units).
            Should be shape (n_pols, n_dipoles).
            Only applies to mwa_beam type files.
        pixels_per_deg : float
            Number of theta/phi pixels per degree. Sets the resolution of the beam.
            Only applies to mwa_beam type files.
        freq_range : array_like of float
            Range of frequencies to include in Hz, defaults to all available
            frequencies. Must be length 2.
            Only applies to mwa_beam type files.
        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.

        Raises
        ------
        ValueError
            If the file_type is not set and cannot be determined from the file name.
        """
        if isinstance(filename, (list, tuple, np.ndarray)):
            multi = True
        else:
            multi = False

        if file_type is None:
            if multi:
                test_file = filename[0]
            else:
                test_file = filename

            basename, extension = os.path.splitext(test_file)
            extension = extension.lower()
            if extension == ".fits" or extension == ".beamfits":
                file_type = "beamfits"
            elif extension == ".hdf5" or extension == ".h5":
                file_type = "mwa_beam"
            elif extension == ".txt" or extension == ".yaml":
                file_type = "cst"

        if file_type is None:
            raise ValueError(
                "File type could not be determined, use the "
                "file_type keyword to specify the type."
            )

        if file_type == "cst":
            # cst beams are already set up for multi
            # beam reading. Let it handle the hard work.
            self.read_cst_beam(
                filename,
                beam_type=beam_type,
                feed_pol=feed_pol,
                rotate_pol=rotate_pol,
                frequency=frequency,
                telescope_name=telescope_name,
                feed_name=feed_name,
                feed_version=feed_version,
                model_name=model_name,
                model_version=model_version,
                history=history,
                x_orientation=x_orientation,
                reference_impedance=reference_impedance,
                extra_keywords=extra_keywords,
                frequency_select=frequency_select,
                run_check=run_check,
                check_extra=check_extra,
                run_check_acceptability=run_check_acceptability,
            )
        else:
            if multi:
                file_num = 0
                file_warnings = ""
                unread = True

                while unread and file_num < len(filename):

                    try:
                        self.read(
                            filename[file_num],
                            file_type=file_type,
                            skip_bad_files=skip_bad_files,
                            # cst beam parameters
                            # leave these in case we restructure the multi
                            # reading later
                            beam_type=beam_type,
                            feed_pol=feed_pol,
                            rotate_pol=rotate_pol,
                            frequency=frequency,
                            telescope_name=telescope_name,
                            feed_name=feed_name,
                            feed_version=feed_version,
                            model_name=model_name,
                            model_version=model_version,
                            history=history,
                            x_orientation=x_orientation,
                            reference_impedance=reference_impedance,
                            extra_keywords=extra_keywords,
                            frequency_select=frequency_select,
                            # mwa_beam parameters
                            delays=delays,
                            amplitudes=amplitudes,
                            pixels_per_deg=pixels_per_deg,
                            freq_range=freq_range,
                            # standard checking
                            run_check=run_check,
                            check_extra=check_extra,
                            run_check_acceptability=run_check_acceptability,
                        )
                        unread = False
                    except ValueError as err:
                        file_warnings += (
                            f"Failed to read {filename[file_num]} "
                            f"due to ValueError {err}\n"
                        )
                        file_num += 1
                        if skip_bad_files is False:
                            raise
                beam_list = []
                if len(filename) >= file_num + 1:
                    for fname in filename[file_num + 1 :]:
                        beam2 = UVBeam()
                        try:
                            beam2.read(
                                fname,
                                file_type=file_type,
                                skip_bad_files=skip_bad_files,
                                # cst beam parameters
                                # leave these in case we restructure the multi
                                # reading later
                                beam_type=beam_type,
                                feed_pol=feed_pol,
                                rotate_pol=rotate_pol,
                                frequency=frequency,
                                telescope_name=telescope_name,
                                feed_name=feed_name,
                                feed_version=feed_version,
                                model_name=model_name,
                                model_version=model_version,
                                history=history,
                                x_orientation=x_orientation,
                                reference_impedance=reference_impedance,
                                extra_keywords=extra_keywords,
                                frequency_select=frequency_select,
                                # mwa_beam parameters
                                delays=delays,
                                amplitudes=amplitudes,
                                pixels_per_deg=pixels_per_deg,
                                freq_range=freq_range,
                                # standard checking
                                run_check=run_check,
                                check_extra=check_extra,
                                run_check_acceptability=run_check_acceptability,
                            )
                            beam_list.append(beam2)
                        except ValueError as err:
                            file_warnings += (
                                f"Failed to read {filename[file_num]} "
                                f"due to ValueError {err}\n"
                            )
                            if skip_bad_files:
                                continue
                            else:
                                raise
                if unread is True:
                    warnings.warn(
                        "########################################################\n"
                        "ALL FILES FAILED ON READ - NO READABLE FILES IN FILENAME\n"
                        "########################################################"
                    )
                elif len(file_warnings) > 0:
                    warnings.warn(file_warnings)

                # Too much work to rewrite __add__ to operate on lists
                # of files, so instead doing a binary tree merge
                beam_list = [self] + beam_list
                while len(beam_list) > 1:
                    for beam1, beam2 in zip(beam_list[0::2], beam_list[1::2]):
                        beam1.__iadd__(beam2,)
                    beam_list = beam_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 == "mwa_beam":
                    self.read_mwa_beam(
                        filename,
                        delays=delays,
                        amplitudes=amplitudes,
                        pixels_per_deg=pixels_per_deg,
                        freq_range=freq_range,
                        run_check=run_check,
                        check_extra=check_extra,
                        run_check_acceptability=run_check_acceptability,
                    )
                elif file_type == "beamfits":

                    self.read_beamfits(
                        filename,
                        run_check=run_check,
                        check_extra=check_extra,
                        run_check_acceptability=run_check_acceptability,
                    )

    @classmethod
    def from_file(
        cls,
        filename,
        file_type=None,
        skip_bad_files=False,
        # cst beam settings
        beam_type="power",
        feed_pol=None,
        rotate_pol=None,
        frequency=None,
        telescope_name=None,
        feed_name=None,
        feed_version=None,
        model_name=None,
        model_version=None,
        history=None,
        x_orientation=None,
        reference_impedance=None,
        extra_keywords=None,
        frequency_select=None,
        # mwa beam settings
        delays=None,
        amplitudes=None,
        pixels_per_deg=5,
        freq_range=None,
        # generic checks
        run_check=True,
        check_extra=True,
        run_check_acceptability=True,
    ):
        """
        Initialize a new UVBeam object by reading the input file(s).

        Some parameters only apply to certain file types.

        Parameters
        ----------
        filename : str or array_like of str
            The file(s) or list(s) (or array(s)) of files to read from.

            For cst yaml files only:

            Settings yaml files must include the following keywords:

                |  - telescope_name (str)
                |  - feed_name (str)
                |  - feed_version (str)
                |  - model_name (str)
                |  - model_version (str)
                |  - history (str)
                |  - frequencies (list(float))
                |  - cst text filenames (list(str)) -- path relative to yaml file
                |  - feed_pol (str) or (list(str))

            and they may include the following optional keywords:

                |  - x_orientation (str): Optional but strongly encouraged!
                |  - ref_imp (float): beam model reference impedance
                |  - sim_beam_type (str): e.g. 'E-farfield'
                |  - all other fields will go into the extra_keywords attribute

            More details and an example are available in the docs
            (cst_settings_yaml.rst).
            Specifying any of the associated keywords to this function will
            override the values in the settings file.

        file_type : str
            One of ['mwa_beam', 'beamfits', 'cst'] or None.
            If None, the code attempts to guess what the file type is.
            based on file extensions
            (mwa_beam: .hdf5, .h5; cst: .yaml, .txt; beamfits: .fits, .beamfits).
            Note that if a list of datasets is passed, the file type is
            determined from the first dataset.
        beam_type : str
            What beam_type to read in ('power' or 'efield').
            Only applies to cst file types.
        feed_pol : str
            The feed or polarization or list of feeds or polarizations the
            files correspond to.
            Defaults to 'x' (meaning x for efield or xx for power beams).
            Only applies to cst file types.
        rotate_pol : bool
            If True, assume the structure in the simulation is symmetric under
            90 degree rotations about the z-axis (so that the y polarization can be
            constructed by rotating the x polarization or vice versa).
            Default: True if feed_pol is a single value or a list with all
            the same values in it, False if it is a list with varying values.
            Only applies to cst file types.
        frequency : float or list of float, optional
            The frequency or list of frequencies corresponding to the filename(s).
            This is assumed to be in the same order as the files.
            If not passed, the code attempts to parse it from the filenames.
            Only applies to cst file types.
        telescope_name : str, optional
            The name of the telescope corresponding to the filename(s).
            Only applies to cst file types.
        feed_name : str, optional
            The name of the feed corresponding to the filename(s).
            Only applies to cst file types.
        feed_version : str, optional
            The version of the feed corresponding to the filename(s).
            Only applies to cst file types.
        model_name : str, optional
            The name of the model corresponding to the filename(s).
            Only applies to cst file types.
        model_version : str, optional
            The version of the model corresponding to the filename(s).
            Only applies to cst file types.
        history : str, optional
            A string detailing the history of the filename(s).
            Only applies to cst file types.
        x_orientation : str, optional
            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)
            Only applies to cst file types.
        reference_impedance : float, optional
            The reference impedance of the model(s).
            Only applies to cst file types.
        extra_keywords : dict, optional
            A dictionary containing any extra_keywords.
            Only applies to cst file types.
        frequency_select : list of float, optional
            Only used if the file is a yaml file. Indicates which frequencies
            to include (only read in files for those frequencies)
            Only applies to cst file types.
        delays : array of ints
            Array of MWA beamformer delay steps. Should be shape (n_pols, n_dipoles).
            Only applies to mwa_beam type files.
        amplitudes : array of floats
            Array of dipole amplitudes, these are absolute values
            (i.e. relatable to physical units).
            Should be shape (n_pols, n_dipoles).
            Only applies to mwa_beam type files.
        pixels_per_deg : float
            Number of theta/phi pixels per degree. Sets the resolution of the beam.
            Only applies to mwa_beam type files.
        freq_range : array_like of float
            Range of frequencies to include in Hz, defaults to all available
            frequencies. Must be length 2.
            Only applies to mwa_beam type files.
        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.

        Raises
        ------
        ValueError
            If the file_type is not set and cannot be determined from the file name.
        """
        uvbeam = cls()
        uvbeam.read(
            filename,
            file_type=file_type,
            skip_bad_files=skip_bad_files,
            # cst beam settings
            beam_type=beam_type,
            feed_pol=feed_pol,
            rotate_pol=rotate_pol,
            frequency=frequency,
            telescope_name=telescope_name,
            feed_name=feed_name,
            feed_version=feed_version,
            model_name=model_name,
            model_version=model_version,
            history=history,
            x_orientation=x_orientation,
            reference_impedance=reference_impedance,
            extra_keywords=extra_keywords,
            frequency_select=frequency_select,
            # mwa beam settings
            delays=delays,
            amplitudes=amplitudes,
            pixels_per_deg=pixels_per_deg,
            freq_range=freq_range,
            # generic checks
            run_check=run_check,
            check_extra=check_extra,
            run_check_acceptability=run_check_acceptability,
        )
        return uvbeam

    def write_beamfits(
        self,
        filename,
        run_check=True,
        check_extra=True,
        run_check_acceptability=True,
        clobber=False,
    ):
        """
        Write the data to a beamfits file.

        Parameters
        ----------
        filename : str
            The beamfits file to write to.
        run_check : bool
            Option to check for the existence and proper shapes of
            required 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
            required parameters before writing the file.
        clobber : bool
            Option to overwrite the filename if the file already exists.

        """
        beamfits_obj = self._convert_to_filetype("beamfits")
        beamfits_obj.write_beamfits(
            filename,
            run_check=run_check,
            check_extra=check_extra,
            run_check_acceptability=run_check_acceptability,
            clobber=clobber,
        )
        del beamfits_obj


# pyuvdata/uvbeam/tests/__init__.py

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


# pyuvdata/uvbeam/tests/test_beamfits.py

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

"""Tests for BeamFits object.

"""
import os

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

from pyuvdata import UVBeam
import pyuvdata.tests as uvtest
from pyuvdata.data import DATA_PATH
import pyuvdata.utils as uvutils

filenames = ["HERA_NicCST_150MHz.txt", "HERA_NicCST_123MHz.txt"]
cst_folder = "NicCSTbeams"
cst_files = [os.path.join(DATA_PATH, cst_folder, f) for f in filenames]


@pytest.fixture(scope="module")
def cst_power_1freq(cst_efield_1freq_main):
    beam_in = cst_efield_1freq_main.copy()
    beam_in.efield_to_power()
    return beam_in.copy()


@pytest.fixture(scope="module")
def cst_power_1freq_cut_healpix(cst_efield_1freq_cut_healpix_main):
    beam_in = cst_efield_1freq_cut_healpix_main.copy()
    beam_in.efield_to_power()
    return beam_in.copy()


@pytest.fixture(scope="function")
def hera_beam_casa():
    beam_in = UVBeam()
    casa_file = os.path.join(DATA_PATH, "HERABEAM.FITS")
    beam_in.read_beamfits(casa_file, run_check=False)

    # fill in missing parameters
    beam_in.data_normalization = "peak"
    beam_in.feed_name = "casa_ideal"
    beam_in.feed_version = "v0"
    beam_in.model_name = "casa_airy"
    beam_in.model_version = "v0"

    # this file is actually in an orthoslant projection RA/DEC at zenith at a
    # particular time.
    # For now pretend it's in a zenith orthoslant projection
    beam_in.pixel_coordinate_system = "orthoslant_zenith"

    return beam_in


def test_read_cst_write_read_fits_efield(cst_efield_1freq, tmp_path):
    beam_in = cst_efield_1freq.copy()
    assert beam_in.filename == ["HERA_NicCST_150MHz.txt"]

    beam_out = UVBeam()

    # add optional parameters for testing purposes
    beam_in.extra_keywords = {"KEY1": "test_keyword"}
    beam_in.x_orientation = "east"
    beam_in.reference_impedance = 340.0
    beam_in.receiver_temperature_array = np.random.normal(
        50.0, 5, size=(beam_in.Nspws, beam_in.Nfreqs)
    )
    beam_in.loss_array = np.random.normal(50.0, 5, size=(beam_in.Nspws, beam_in.Nfreqs))
    beam_in.mismatch_array = np.random.normal(
        0.0, 1.0, size=(beam_in.Nspws, beam_in.Nfreqs)
    )
    beam_in.s_parameters = np.random.normal(
        0.0, 0.3, size=(4, beam_in.Nspws, beam_in.Nfreqs)
    )
    beam_in.interpolation_function = "az_za_simple"
    beam_in.freq_interp_kind = "linear"

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

    beam_in.write_beamfits(write_file, clobber=True)
    beam_out.read_beamfits(write_file)

    assert beam_in == beam_out

    return


def test_read_cst_write_read_fits_power(cst_power_1freq, tmp_path):
    # redo for power beam
    beam_in = cst_power_1freq
    beam_out = UVBeam()

    # add optional parameters for testing purposes
    beam_in.extra_keywords = {"KEY1": "test_keyword"}
    beam_in.x_orientation = "east"
    beam_in.reference_impedance = 340.0
    beam_in.receiver_temperature_array = np.random.normal(
        50.0, 5, size=(beam_in.Nspws, beam_in.Nfreqs)
    )
    beam_in.loss_array = np.random.normal(50.0, 5, size=(beam_in.Nspws, beam_in.Nfreqs))
    beam_in.mismatch_array = np.random.normal(
        0.0, 1.0, size=(beam_in.Nspws, beam_in.Nfreqs)
    )
    beam_in.s_parameters = np.random.normal(
        0.0, 0.3, size=(4, beam_in.Nspws, beam_in.Nfreqs)
    )

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

    beam_in.write_beamfits(write_file, clobber=True)
    beam_out.read_beamfits(write_file)
    assert beam_in == beam_out

    return


def test_read_cst_write_read_fits_intensity(cst_power_1freq, tmp_path):
    # set up power beam
    beam_in = cst_power_1freq
    beam_out = UVBeam()

    write_file = str(tmp_path / "outtest_beam.fits")
    write_file2 = str(tmp_path / "outtest_beam2.fits")
    beam_in.write_beamfits(write_file, clobber=True)

    # now replace 'power' with 'intensity' for btype
    with fits.open(write_file) as fname:
        data = fname[0].data
        primary_hdr = fname[0].header
        primary_hdr["BTYPE"] = "Intensity"
        hdunames = uvutils._fits_indexhdus(fname)
        bandpass_hdu = fname[hdunames["BANDPARM"]]

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

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

    beam_out.read_beamfits(write_file2)
    assert beam_in == beam_out

    return


def test_read_cst_write_read_fits_no_coordsys(cst_power_1freq, tmp_path):
    # set up power beam
    beam_in = cst_power_1freq
    beam_out = UVBeam()

    write_file = str(tmp_path / "outtest_beam.fits")
    write_file2 = str(tmp_path / "outtest_beam2.fits")
    beam_in.write_beamfits(write_file, clobber=True)

    # now remove coordsys but leave ctypes 1 & 2
    with fits.open(write_file) as fname:
        data = fname[0].data
        primary_hdr = fname[0].header
        primary_hdr.pop("COORDSYS")
        hdunames = uvutils._fits_indexhdus(fname)
        bandpass_hdu = fname[hdunames["BANDPARM"]]

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

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

    beam_out.read_beamfits(write_file2)
    assert beam_in == beam_out

    return


def test_read_cst_write_read_fits_change_freq_units(cst_power_1freq, tmp_path):
    # set up power beam
    beam_in = cst_power_1freq
    beam_out = UVBeam()

    write_file = str(tmp_path / "outtest_beam.fits")
    write_file2 = str(tmp_path / "outtest_beam2.fits")
    beam_in.write_beamfits(write_file, clobber=True)

    # now change frequency units
    with fits.open(write_file) as fname:
        data = fname[0].data
        primary_hdr = fname[0].header
        primary_hdr["CUNIT3"] = "MHz"
        primary_hdr["CRVAL3"] = primary_hdr["CRVAL3"] / 1e6
        primary_hdr["CDELT3"] = primary_hdr["CRVAL3"] / 1e6
        hdunames = uvutils._fits_indexhdus(fname)
        bandpass_hdu = fname[hdunames["BANDPARM"]]

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

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

    beam_out.read_beamfits(write_file2)
    assert beam_in == beam_out

    return


def test_writeread_healpix(cst_efield_1freq_cut_healpix, tmp_path):
    beam_in = cst_efield_1freq_cut_healpix.copy()
    beam_out = UVBeam()

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

    beam_in.write_beamfits(write_file, clobber=True)
    beam_out.read_beamfits(write_file)

    assert beam_in == beam_out

    return


def test_writeread_healpix_power(cst_power_1freq_cut_healpix, tmp_path):
    # redo for power beam
    beam_in = cst_power_1freq_cut_healpix
    beam_out = UVBeam()

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

    # add optional parameters for testing purposes
    beam_in.extra_keywords = {"KEY1": "test_keyword"}
    beam_in.x_orientation = "east"
    beam_in.reference_impedance = 340.0
    beam_in.receiver_temperature_array = np.random.normal(
        50.0, 5, size=(beam_in.Nspws, beam_in.Nfreqs)
    )
    beam_in.loss_array = np.random.normal(50.0, 5, size=(beam_in.Nspws, beam_in.Nfreqs))
    beam_in.mismatch_array = np.random.normal(
        0.0, 1.0, size=(beam_in.Nspws, beam_in.Nfreqs)
    )
    beam_in.s_parameters = np.random.normal(
        0.0, 0.3, size=(4, beam_in.Nspws, beam_in.Nfreqs)
    )

    # check that data_array is complex
    assert np.iscomplexobj(np.real_if_close(beam_in.data_array, tol=10))

    beam_in.write_beamfits(write_file, clobber=True)
    beam_out.read_beamfits(write_file)

    assert beam_in == beam_out

    return


def test_writeread_healpix_no_corrdsys(cst_power_1freq_cut_healpix, tmp_path):
    beam_in = cst_power_1freq_cut_healpix
    beam_out = UVBeam()

    write_file = str(tmp_path / "outtest_beam.fits")
    write_file2 = str(tmp_path / "outtest_beam2.fits")
    beam_in.write_beamfits(write_file, clobber=True)

    # now remove coordsys but leave ctype 1
    with fits.open(write_file) as fname:
        data = fname[0].data
        primary_hdr = fname[0].header
        primary_hdr.pop("COORDSYS")
        hdunames = uvutils._fits_indexhdus(fname)
        hpx_hdu = fname[hdunames["HPX_INDS"]]
        bandpass_hdu = fname[hdunames["BANDPARM"]]

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

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

    beam_out.read_beamfits(write_file2)
    assert beam_in == beam_out

    return


def test_error_beam_type(cst_efield_1freq, tmp_path):
    beam_in = cst_efield_1freq
    beam_in.beam_type = "foo"

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

    # make sure writing fails
    with pytest.raises(
        ValueError, match="UVParameter _beam_type has unacceptable values"
    ):
        beam_in.write_beamfits(write_file, clobber=True)

    # make sure it fails even if check is off
    with pytest.raises(ValueError, match="Unknown beam_type: foo"):
        beam_in.write_beamfits(write_file, clobber=True, run_check=False)

    return


def test_error_antenna_type(cst_efield_1freq, tmp_path):
    beam_in = cst_efield_1freq
    beam_in.antenna_type = "phased_array"

    write_file = str(tmp_path / "outtest_beam.fits")
    with pytest.raises(
        ValueError, match="This beam fits writer currently only supports"
    ):
        beam_in.write_beamfits(write_file, clobber=True)

    return


@pytest.mark.parametrize(
    "header_dict,error_msg",
    [
        ({"BTYPE": "foo"}, "Unknown beam_type: foo"),
        ({"COORDSYS": "orthoslant_zenith"}, "Coordinate axis list does not match"),
        ({"NAXIS": ""}, "beam_type is efield and data dimensionality"),
        ({"CUNIT1": "foo"}, 'Units of first axis array are not "deg" or "rad"'),
        ({"CUNIT2": "foo"}, 'Units of second axis array are not "deg" or "rad"'),
        ({"CUNIT3": "foo"}, "Frequency units not recognized"),
    ],
)
def test_header_val_errors(cst_efield_1freq, tmp_path, header_dict, error_msg):
    beam_in = cst_efield_1freq
    beam_out = UVBeam()

    write_file = str(tmp_path / "outtest_beam.fits")
    write_file2 = str(tmp_path / "outtest_beam2.fits")

    # now change values for various items in primary hdu to test errors
    beam_in.write_beamfits(write_file, clobber=True)

    keyword = list(header_dict.keys())[0]
    new_val = header_dict[keyword]
    with fits.open(write_file) as fname:
        data = fname[0].data
        primary_hdr = fname[0].header
        hdunames = uvutils._fits_indexhdus(fname)
        basisvec_hdu = fname[hdunames["BASISVEC"]]
        bandpass_hdu = fname[hdunames["BANDPARM"]]

        if "NAXIS" in keyword:
            ax_num = keyword.split("NAXIS")[1]
            if ax_num != "":
                ax_num = int(ax_num)
                ax_use = len(data.shape) - ax_num
                new_arrays = np.split(data, primary_hdr[keyword], axis=ax_use)
                data = new_arrays[0]
            else:
                data = np.squeeze(
                    np.split(data, primary_hdr["NAXIS1"], axis=len(data.shape) - 1)[0]
                )
        else:
            primary_hdr[keyword] = new_val

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

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

    with pytest.raises(ValueError, match=error_msg):
        beam_out.read_beamfits(write_file2)

    return


@pytest.mark.parametrize(
    "header_dict,error_msg",
    [
        ({"COORDSYS": "foo"}, "Pixel coordinate system in BASISVEC"),
        ({"CTYPE1": "foo"}, "Pixel coordinate list in BASISVEC"),
        ({"CTYPE2": "foo"}, "Pixel coordinate list in BASISVEC"),
        ({"CDELT1": "foo"}, "First image axis in BASISVEC"),
        ({"CDELT2": "foo"}, "Second image axis in BASISVEC"),
        ({"NAXIS4": ""}, "Number of vector coordinate axes in BASISVEC"),
        ({"CUNIT1": "foo"}, "Units of first axis array in BASISVEC"),
        ({"CUNIT2": "foo"}, "Units of second axis array in BASISVEC"),
    ],
)
def test_basisvec_hdu_errors(cst_efield_1freq, tmp_path, header_dict, error_msg):
    beam_in = cst_efield_1freq
    beam_out = UVBeam()

    write_file = str(tmp_path / "outtest_beam.fits")
    write_file2 = str(tmp_path / "outtest_beam2.fits")

    # now change values for various items in basisvec hdu to not match primary hdu
    beam_in.write_beamfits(write_file, clobber=True)

    keyword = list(header_dict.keys())[0]
    # hacky treatment of CDELT b/c we need the object to be defined already
    if keyword == "CDELT1":
        new_val = np.diff(beam_in.axis1_array)[0] * 2
    elif keyword == "CDELT2":
        new_val = np.diff(beam_in.axis2_array)[0] * 2
    else:
        new_val = header_dict[keyword]

    with fits.open(write_file) as fname:
        data = fname[0].data
        primary_hdr = fname[0].header
        hdunames = uvutils._fits_indexhdus(fname)
        basisvec_hdu = fname[hdunames["BASISVEC"]]
        basisvec_hdr = basisvec_hdu.header
        basisvec_data = basisvec_hdu.data
        bandpass_hdu = fname[hdunames["BANDPARM"]]

        if "NAXIS" in keyword:
            ax_num = keyword.split("NAXIS")[1]
            if ax_num != "":
                ax_num = int(ax_num)
                ax_use = len(basisvec_data.shape) - ax_num
                new_arrays = np.split(basisvec_data, basisvec_hdr[keyword], axis=ax_use)
                basisvec_data = new_arrays[0]
            else:
                basisvec_data = np.split(
                    basisvec_data,
                    basisvec_hdr["NAXIS1"],
                    axis=len(basisvec_data.shape) - 1,
                )[0]
        else:
            basisvec_hdr[keyword] = new_val

        prihdu = fits.PrimaryHDU(data=data, header=primary_hdr)
        basisvec_hdu = fits.ImageHDU(data=basisvec_data, header=basisvec_hdr)
        hdulist = fits.HDUList([prihdu, basisvec_hdu, bandpass_hdu])

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

    with pytest.raises(ValueError, match=error_msg):
        beam_out.read_beamfits(write_file2)

    return


@pytest.mark.parametrize(
    "header_dict,error_msg",
    [
        ({"CTYPE1": "foo"}, "Coordinate axis list does not match"),
        ({"NAXIS1": ""}, "Number of pixels in HPX_IND extension"),
    ],
)
def test_healpix_errors(cst_efield_1freq_cut_healpix, tmp_path, header_dict, error_msg):
    beam_in = cst_efield_1freq_cut_healpix
    beam_out = UVBeam()
    write_file = str(tmp_path / "outtest_beam_hpx.fits")
    write_file2 = str(tmp_path / "outtest_beam_hpx2.fits")

    beam_in.write_beamfits(write_file, clobber=True)

    # now change values for various items in primary hdu to test errors
    keyword = list(header_dict.keys())[0]
    new_val = header_dict[keyword]
    with fits.open(write_file) as fname:
        data = fname[0].data
        primary_hdr = fname[0].header
        hdunames = uvutils._fits_indexhdus(fname)
        basisvec_hdu = fname[hdunames["BASISVEC"]]
        hpx_hdu = fname[hdunames["HPX_INDS"]]
        bandpass_hdu = fname[hdunames["BANDPARM"]]

        if "NAXIS" in keyword:
            ax_num = keyword.split("NAXIS")[1]
            if ax_num != "":
                ax_num = int(ax_num)
                ax_use = len(data.shape) - ax_num
                new_arrays = np.split(data, primary_hdr[keyword], axis=ax_use)
                data = new_arrays[0]
            else:
                data = np.squeeze(
                    np.split(data, primary_hdr["NAXIS1"], axis=len(data.shape) - 1)[0]
                )
        else:
            primary_hdr[keyword] = new_val

        prihdu = fits.PrimaryHDU(data=data, header=primary_hdr)
        hdulist = fits.HDUList([prihdu, basisvec_hdu, hpx_hdu, bandpass_hdu])

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

    with pytest.raises(ValueError, match=error_msg):
        beam_out.read_beamfits(write_file2)

    return


@pytest.mark.parametrize(
    "header_dict,error_msg",
    [
        ({"CTYPE1": "foo"}, "First axis in BASISVEC HDU"),
        ({"NAXIS1": ""}, "Number of pixels in BASISVEC HDU"),
    ],
)
def test_healpix_basisvec_hdu_errors(
    cst_efield_1freq_cut_healpix, tmp_path, header_dict, error_msg
):
    beam_in = cst_efield_1freq_cut_healpix
    beam_out = UVBeam()
    write_file = str(tmp_path / "outtest_beam_hpx.fits")
    write_file2 = str(tmp_path / "outtest_beam_hpx2.fits")

    beam_in.write_beamfits(write_file, clobber=True)

    # now change values for various items in basisvec hdu to not match primary hdu
    keyword = list(header_dict.keys())[0]
    new_val = header_dict[keyword]
    with fits.open(write_file) as fname:
        data = fname[0].data
        primary_hdr = fname[0].header
        hdunames = uvutils._fits_indexhdus(fname)
        basisvec_hdu = fname[hdunames["BASISVEC"]]
        basisvec_hdr = basisvec_hdu.header
        basisvec_data = basisvec_hdu.data
        hpx_hdu = fname[hdunames["HPX_INDS"]]
        bandpass_hdu = fname[hdunames["BANDPARM"]]

        if "NAXIS" in keyword:
            ax_num = keyword.split("NAXIS")[1]
            if ax_num != "":
                ax_num = int(ax_num)
                ax_use = len(basisvec_data.shape) - ax_num
                new_arrays = np.split(basisvec_data, basisvec_hdr[keyword], axis=ax_use)
                basisvec_data = new_arrays[0]
            else:
                basisvec_data = np.split(
                    basisvec_data,
                    basisvec_hdr["NAXIS1"],
                    axis=len(basisvec_data.shape) - 1,
                )[0]
        else:
            basisvec_hdr[keyword] = new_val

        prihdu = fits.PrimaryHDU(data=data, header=primary_hdr)
        basisvec_hdu = fits.ImageHDU(data=basisvec_data, header=basisvec_hdr)
        hdulist = fits.HDUList([prihdu, basisvec_hdu, hpx_hdu, bandpass_hdu])

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

    with pytest.raises(ValueError, match=error_msg):
        beam_out.read_beamfits(write_file2)

    return


def test_casa_beam(tmp_path, hera_beam_casa):
    # test reading in CASA power beam. Some header items are missing...
    beam_in = hera_beam_casa
    beam_out = UVBeam()
    write_file = str(tmp_path / "outtest_beam.fits")

    expected_extra_keywords = [
        "OBSERVER",
        "OBSDEC",
        "DATAMIN",
        "OBJECT",
        "INSTRUME",
        "DATAMAX",
        "OBSRA",
        "ORIGIN",
        "DATE-MAP",
        "DATE",
        "EQUINOX",
        "DATE-OBS",
        "COMMENT",
    ]
    assert expected_extra_keywords.sort() == list(beam_in.extra_keywords.keys()).sort()

    beam_in.write_beamfits(write_file, clobber=True)
    beam_out.read_beamfits(write_file)

    assert beam_in == beam_out


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

    # fill in missing parameters
    beam_in.data_normalization = "peak"
    beam_in.feed_name = "casa_ideal"
    beam_in.feed_version = "v0"
    beam_in.model_name = "casa_airy"
    beam_in.model_version = "v0"

    # this file is actually in an orthoslant projection RA/DEC at zenith at a
    # particular time.
    # For now pretend it's in a zenith orthoslant projection
    beam_in.pixel_coordinate_system = "orthoslant_zenith"

    # check for warnings & errors with extra_keywords that are dicts, lists or arrays
    keyword = list(ex_val.keys())[0]
    val = ex_val[keyword]
    beam_in.extra_keywords[keyword] = val
    with uvtest.check_warnings(
        UserWarning, f"{keyword} in extra_keywords is a list, array or dict"
    ):
        beam_in.check()

    with pytest.raises(TypeError, match=error_msg):
        beam_in.write_beamfits(testfile, run_check=False)

    return


def test_extra_keywords_warnings(tmp_path, hera_beam_casa):
    beam_in = hera_beam_casa
    testfile = str(tmp_path / "outtest_beam.fits")

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

    return


def test_extra_keywords_boolean(tmp_path, hera_beam_casa):
    beam_in = hera_beam_casa
    beam_out = UVBeam()
    testfile = str(tmp_path / "outtest_beam.fits")

    # check handling of boolean keywords
    beam_in.extra_keywords["bool"] = True
    beam_in.extra_keywords["bool2"] = False
    beam_in.write_beamfits(testfile, clobber=True)
    beam_out.read_beamfits(testfile, run_check=False)

    assert beam_in == beam_out

    return


def test_extra_keywords_int(tmp_path, hera_beam_casa):
    beam_in = hera_beam_casa
    beam_out = UVBeam()
    testfile = str(tmp_path / "outtest_beam.fits")

    # check handling of int-like keywords
    beam_in.extra_keywords["int1"] = np.int64(5)
    beam_in.extra_keywords["int2"] = 7
    beam_in.write_beamfits(testfile, clobber=True)
    beam_out.read_beamfits(testfile, run_check=False)

    assert beam_in == beam_out

    return


def test_extra_keywords_float(tmp_path, hera_beam_casa):
    beam_in = hera_beam_casa
    beam_out = UVBeam()
    testfile = str(tmp_path / "outtest_beam.fits")

    # check handling of float-like keywords
    beam_in.extra_keywords["float1"] = np.int64(5.3)
    beam_in.extra_keywords["float2"] = 6.9
    beam_in.write_beamfits(testfile, clobber=True)
    beam_out.read_beamfits(testfile, run_check=False)

    assert beam_in == beam_out

    return


def test_extra_keywords_complex(tmp_path, hera_beam_casa):
    beam_in = hera_beam_casa
    beam_out = UVBeam()
    testfile = str(tmp_path / "outtest_beam.fits")

    # check handling of complex-like keywords
    beam_in.extra_keywords["complex1"] = np.complex64(5.3 + 1.2j)
    beam_in.extra_keywords["complex2"] = 6.9 + 4.6j
    beam_in.write_beamfits(testfile, clobber=True)
    beam_out.read_beamfits(testfile, run_check=False)

    assert beam_in == beam_out

    return


def test_multi_files(cst_efield_2freq, tmp_path):
    """
    Reading multiple files at once.
    """
    beam_full = cst_efield_2freq

    # add optional parameters for testing purposes
    beam_full.extra_keywords = {"KEY1": "test_keyword"}
    beam_full.x_orientation = "east"
    beam_full.reference_impedance = 340.0
    beam_full.receiver_temperature_array = np.random.normal(
        50.0, 5, size=(beam_full.Nspws, beam_full.Nfreqs)
    )
    beam_full.loss_array = np.random.normal(
        50.0, 5, size=(beam_full.Nspws, beam_full.Nfreqs)
    )
    beam_full.mismatch_array = np.random.normal(
        0.0, 1.0, size=(beam_full.Nspws, beam_full.Nfreqs)
    )
    beam_full.s_parameters = np.random.normal(
        0.0, 0.3, size=(4, beam_full.Nspws, beam_full.Nfreqs)
    )

    testfile1 = str(tmp_path / "outtest_beam1.fits")
    testfile2 = str(tmp_path / "outtest_beam2.fits")

    beam1 = beam_full.select(freq_chans=0, inplace=False)
    beam2 = beam_full.select(freq_chans=1, inplace=False)
    beam1.write_beamfits(testfile1, clobber=True)
    beam2.write_beamfits(testfile2, clobber=True)
    beam1.read_beamfits([testfile1, testfile2])
    # Check history is correct, before replacing and doing a full object check
    assert uvutils._check_histories(
        beam_full.history + "  Downselected "
        "to specific frequencies using pyuvdata. "
        "Combined data along frequency axis using"
        " pyuvdata.",
        beam1.history,
    )

    beam1.history = beam_full.history
    assert beam1 == beam_full


# pyuvdata/uvbeam/tests/test_cst_beam.py

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

import pytest
import numpy as np

from pyuvdata.data import DATA_PATH
from pyuvdata import UVBeam
from pyuvdata.uvbeam.cst_beam import CSTBeam
import pyuvdata.tests as uvtest

filenames = ["HERA_NicCST_150MHz.txt", "HERA_NicCST_123MHz.txt"]
cst_folder = "NicCSTbeams"
cst_files = [os.path.join(DATA_PATH, cst_folder, f) for f in filenames]
cst_yaml_file = os.path.join(DATA_PATH, cst_folder, "NicCSTbeams.yaml")
cst_yaml_vivaldi = os.path.join(DATA_PATH, cst_folder, "HERA_Vivaldi_CST_beams.yaml")


def test_basic_frequencyparse():
    beam1 = CSTBeam()

    parsed_freqs = [beam1.name2freq(f) for f in cst_files]
    assert parsed_freqs == [150e6, 123e6]


def test_frequencyparse_extra_numbers():
    beam1 = CSTBeam()
    test_path = os.path.join(
        "pyuvdata_1510194907049",
        "_t_env",
        "lib",
        "python2.7",
        "site-packages",
        "pyuvdata",
        "data",
    )
    test_files = [os.path.join(test_path, f) for f in filenames]
    parsed_freqs = [beam1.name2freq(f) for f in test_files]
    assert parsed_freqs == [150e6, 123e6]


def test_frequencyparse_nicf_path():
    beam1 = CSTBeam()
    test_path = os.path.join(
        "Simulations",
        "Radiation_patterns",
        "E-field pattern-Rigging height4.9m",
        "HERA_4.9m_E-pattern_100-200MHz",
    )
    test_files = [os.path.join(test_path, f) for f in filenames]
    parsed_freqs = [beam1.name2freq(f) for f in test_files]
    assert parsed_freqs == [150e6, 123e6]


def test_frequencyparse_decimal_non_mhz():
    beam1 = CSTBeam()
    test_path = os.path.join(
        "Simulations",
        "Radiation_patterns",
        "E-field pattern-Rigging height4.9m",
        "HERA_4.9m_E-pattern_100-200MHz",
    )
    test_names = [
        "HERA_Sim_120.87kHz.txt",
        "HERA_Sim_120.87GHz.txt",
        "HERA_Sim_120.87Hz.txt",
    ]
    test_files = [os.path.join(test_path, f) for f in test_names]
    parsed_freqs = [beam1.name2freq(f) for f in test_files]
    assert parsed_freqs == [120.87e3, 120.87e9, 120.87]


def test_read_yaml(cst_efield_2freq):
    pytest.importorskip("yaml")
    beam1 = UVBeam()
    beam2 = UVBeam()

    extra_keywords = {
        "software": "CST 2016",
        "sim_type": "E-farfield",
        "layout": "1 antenna",
        "port_num": 1,
    }

    beam1 = cst_efield_2freq
    assert beam1.filename == ["HERA_NicCST_123MHz.txt", "HERA_NicCST_150MHz.txt"]

    beam2.read_cst_beam(cst_yaml_file, beam_type="efield")
    assert beam2.filename == sorted(
        [
            os.path.basename(cst_yaml_file),
            "HERA_NicCST_123MHz.txt",
            "HERA_NicCST_150MHz.txt",
        ]
    )
    assert beam1 == beam2

    assert beam2.reference_impedance == 100
    assert beam2.extra_keywords == extra_keywords


def test_read_yaml_onefile(cst_efield_1freq, tmp_path):
    pytest.importorskip("yaml")
    import yaml

    # copy the beam files to the tmp directory so that it can read them
    # when the yaml is stored there
    for fname in cst_files:
        shutil.copy2(src=fname, dst=tmp_path)

    test_yaml_file = os.path.join(tmp_path, "test_cst_settings.yaml")
    with open(cst_yaml_file, "r") as file:
        settings_dict = yaml.safe_load(file)

    settings_dict["filenames"] = [settings_dict["filenames"][0]]
    settings_dict["frequencies"] = [settings_dict["frequencies"][0]]

    with open(test_yaml_file, "w") as outfile:
        yaml.dump(settings_dict, outfile, default_flow_style=False)

    beam1 = UVBeam()
    beam2 = UVBeam()

    extra_keywords = {
        "software": "CST 2016",
        "sim_type": "E-farfield",
        "layout": "1 antenna",
        "port_num": 1,
    }

    beam1 = cst_efield_1freq

    beam2.read_cst_beam(test_yaml_file, beam_type="efield")
    assert beam1 == beam2

    assert beam2.reference_impedance == 100
    assert beam2.extra_keywords == extra_keywords


def test_read_yaml_override(cst_efield_2freq):
    pytest.importorskip("yaml")
    beam1 = UVBeam()
    beam2 = UVBeam()

    extra_keywords = {
        "software": "CST 2016",
        "sim_type": "E-farfield",
        "layout": "1 antenna",
        "port_num": 1,
    }

    beam1 = cst_efield_2freq
    beam1.telescope_name = "test"

    with uvtest.check_warnings(
        UserWarning,
        match=(
            "The telescope_name keyword is set, overriding "
            "the value in the settings yaml file."
        ),
    ):
        beam2.read_cst_beam(cst_yaml_file, beam_type="efield", telescope_name="test"),

    assert beam1 == beam2

    assert beam2.reference_impedance == 100
    assert beam2.extra_keywords == extra_keywords


def test_read_yaml_freq_select(cst_efield_1freq):
    pytest.importorskip("yaml")
    # test frequency_select
    beam1 = UVBeam()
    beam2 = UVBeam()

    beam1 = cst_efield_1freq

    beam2.read_cst_beam(cst_yaml_file, beam_type="efield", frequency_select=[150e6])

    assert beam1 == beam2

    # test error with using frequency_select where no such frequency
    freq = 180e6
    with pytest.raises(ValueError, match=f"frequency {freq} not in frequency list"):
        beam2.read_cst_beam(cst_yaml_file, beam_type="power", frequency_select=[freq])


def test_read_yaml_feed_pol_list(cst_efield_2freq, cst_efield_1freq):
    pytest.importorskip("yaml")
    # make yaml with a list of (the same) feed_pols
    import yaml

    test_yaml_file = os.path.join(DATA_PATH, cst_folder, "test_cst_settings.yaml")
    with open(cst_yaml_file, "r") as file:
        settings_dict = yaml.safe_load(file)

    settings_dict["feed_pol"] = ["x", "x"]

    with open(test_yaml_file, "w") as outfile:
        yaml.dump(settings_dict, outfile, default_flow_style=False)

    beam1 = UVBeam()
    beam2 = UVBeam()

    extra_keywords = {
        "software": "CST 2016",
        "sim_type": "E-farfield",
        "layout": "1 antenna",
        "port_num": 1,
    }

    beam1 = cst_efield_2freq

    beam2.read_cst_beam(test_yaml_file, beam_type="efield")
    assert beam1 == beam2

    assert beam2.reference_impedance == 100
    assert beam2.extra_keywords == extra_keywords

    # also test with frequency_select
    beam1 = cst_efield_1freq

    beam2.read_cst_beam(test_yaml_file, beam_type="efield", frequency_select=[150e6])
    assert beam1 == beam2

    os.remove(test_yaml_file)


def test_read_yaml_multi_pol(tmp_path):
    pytest.importorskip("yaml")
    # make yaml for one freq, 2 pols
    import yaml

    # copy the beam files to the tmp directory so that it can read them
    # when the yaml is stored there
    for fname in cst_files:
        shutil.copy2(src=fname, dst=tmp_path)
    test_yaml_file = os.path.join(tmp_path, "test_cst_settings.yaml")

    with open(cst_yaml_file, "r") as file:
        settings_dict = yaml.safe_load(file)

    settings_dict["feed_pol"] = ["x", "y"]
    first_file = settings_dict["filenames"][0]
    settings_dict["filenames"] = [first_file, first_file]
    first_freq = settings_dict["frequencies"][0]
    settings_dict["frequencies"] = [first_freq, first_freq]

    with open(test_yaml_file, "w") as outfile:
        yaml.dump(settings_dict, outfile, default_flow_style=False)

    beam1 = UVBeam()
    beam2 = UVBeam()

    extra_keywords = {
        "software": "CST 2016",
        "sim_type": "E-farfield",
        "layout": "1 antenna",
        "port_num": 1,
    }

    with uvtest.check_warnings(
        UserWarning, "No frequency provided. Detected frequency is", nwarnings=2,
    ):
        beam1.read_cst_beam(
            [cst_files[0], cst_files[0]],
            beam_type="efield",
            feed_pol=["x", "y"],
            telescope_name="HERA",
            feed_name="Dipole",
            feed_version="1.0",
            model_name="Dipole - Rigging height 4.9 m",
            model_version="1.0",
            x_orientation="east",
            reference_impedance=100,
            history="Derived from https://github.com/Nicolas-Fagnoni/Simulations."
            "\nOnly 2 files included to keep test data volume low.",
            extra_keywords=extra_keywords,
        )

    beam2.read_cst_beam(test_yaml_file, beam_type="efield")
    assert beam1 == beam2

    # also test with frequency_select
    beam2.read_cst_beam(test_yaml_file, beam_type="efield", frequency_select=[150e6])
    assert beam2.feed_array.tolist() == ["x", "y"]
    assert beam1 == beam2

    os.remove(test_yaml_file)


def test_read_yaml_errors(tmp_path):
    pytest.importorskip("yaml")
    # test error if required key is not present in yaml file
    import yaml

    test_yaml_file = os.path.join(tmp_path, "test_cst_settings.yaml")
    with open(cst_yaml_file, "r") as file:
        settings_dict = yaml.safe_load(file)

    settings_dict.pop("telescope_name")

    with open(test_yaml_file, "w") as outfile:
        yaml.dump(settings_dict, outfile, default_flow_style=False)

    beam1 = UVBeam()
    with pytest.raises(
        ValueError,
        match=(
            "telescope_name is a required key in CST settings files but is "
            "not present."
        ),
    ):
        beam1.read_cst_beam(test_yaml_file, beam_type="power")

    with open(cst_yaml_file, "r") as file:
        settings_dict = yaml.safe_load(file)
    settings_dict["filenames"] = settings_dict["filenames"][0]

    with open(test_yaml_file, "w") as outfile:
        yaml.dump(settings_dict, outfile, default_flow_style=False)

    beam1 = UVBeam()
    with pytest.raises(
        ValueError, match=("filenames in yaml file must be a list."),
    ):
        beam1.read_cst_beam(test_yaml_file, beam_type="power")

    with open(cst_yaml_file, "r") as file:
        settings_dict = yaml.safe_load(file)
    settings_dict["frequencies"] = settings_dict["frequencies"][0]

    with open(test_yaml_file, "w") as outfile:
        yaml.dump(settings_dict, outfile, default_flow_style=False)

    beam1 = UVBeam()
    with pytest.raises(
        ValueError, match=("frequencies in yaml file must be a list."),
    ):
        beam1.read_cst_beam(test_yaml_file, beam_type="power")

    os.remove(test_yaml_file)


def test_read_power(cst_power_2freq):
    beam2 = UVBeam()

    beam1 = cst_power_2freq

    assert beam1.pixel_coordinate_system == "az_za"
    assert beam1.beam_type == "power"
    assert beam1.data_array.shape == (1, 1, 2, 2, 181, 360)
    assert np.max(beam1.data_array) == 8275.5409

    assert np.allclose(
        beam1.data_array[:, :, 0, :, :, np.where(beam1.axis1_array == 0)[0]],
        beam1.data_array[:, :, 1, :, :, np.where(beam1.axis1_array == np.pi / 2.0)[0]],
    )

    # test passing in other polarization
    beam2.read_cst_beam(
        np.array(cst_files),
        beam_type="power",
        frequency=np.array([150e6, 123e6]),
        feed_pol="y",
        telescope_name="TEST",
        feed_name="bob",
        feed_version="0.1",
        model_name="E-field pattern - Rigging height 4.9m",
        model_version="1.0",
    )

    assert np.allclose(beam1.freq_array, beam2.freq_array)

    assert np.allclose(beam2.polarization_array, np.array([-6, -5]))
    assert np.allclose(
        beam1.data_array[:, :, 0, :, :, :], beam2.data_array[:, :, 0, :, :, :]
    )


def test_read_power_single_freq(cst_power_1freq):
    # test single frequency
    beam2 = UVBeam()

    beam1 = cst_power_1freq

    assert beam1.freq_array == [150e6]
    assert beam1.pixel_coordinate_system == "az_za"
    assert beam1.beam_type == "power"
    assert beam1.data_array.shape == (1, 1, 2, 1, 181, 360)

    # test single frequency and not rotating the polarization
    with uvtest.check_warnings(
        UserWarning, "No frequency provided. Detected frequency is"
    ):
        beam2.read_cst_beam(
            cst_files[0],
            beam_type="power",
            telescope_name="TEST",
            feed_name="bob",
            feed_version="0.1",
            model_name="E-field pattern - Rigging height 4.9m",
            model_version="1.0",
            rotate_pol=False,
        )

    assert beam2.freq_array == [150e6]
    assert beam2.pixel_coordinate_system == "az_za"
    assert beam2.beam_type == "power"
    assert beam2.polarization_array == np.array([-5])
    assert beam2.data_array.shape == (1, 1, 1, 1, 181, 360)
    assert np.allclose(beam1.data_array[:, :, 0, :, :, :], beam2.data_array)


def test_read_power_multi_pol():
    # test reading in multiple polarization files
    beam1 = UVBeam()
    beam2 = UVBeam()

    beam1.read_cst_beam(
        [cst_files[0], cst_files[0]],
        beam_type="power",
        frequency=[150e6],
        feed_pol=np.array(["xx", "yy"]),
        telescope_name="TEST",
        feed_name="bob",
        feed_version="0.1",
        model_name="E-field pattern - Rigging height 4.9m",
        model_version="1.0",
    )
    assert beam1.data_array.shape == (1, 1, 2, 1, 181, 360)
    assert np.allclose(
        beam1.data_array[:, :, 0, :, :, :], beam1.data_array[:, :, 1, :, :, :]
    )

    # test reading in cross polarization files
    beam2.read_cst_beam(
        [cst_files[0]],
        beam_type="power",
        frequency=[150e6],
        feed_pol=np.array(["xy"]),
        telescope_name="TEST",
        feed_name="bob",
        feed_version="0.1",
        model_name="E-field pattern - Rigging height 4.9m",
        model_version="1.0",
    )
    assert np.allclose(beam2.polarization_array, np.array([-7, -8]))
    assert beam2.data_array.shape == (1, 1, 2, 1, 181, 360)
    assert np.allclose(
        beam1.data_array[:, :, 0, :, :, :], beam2.data_array[:, :, 0, :, :, :]
    )


def test_read_errors():
    # test errors
    beam1 = UVBeam()

    pytest.raises(
        ValueError,
        beam1.read_cst_beam,
        cst_files,
        beam_type="power",
        frequency=[150e6, 123e6, 100e6],
        telescope_name="TEST",
        feed_name="bob",
        feed_version="0.1",
        model_name="E-field pattern - Rigging height 4.9m",
        model_version="1.0",
    )

    pytest.raises(
        ValueError,
        beam1.read_cst_beam,
        cst_files[0],
        beam_type="power",
        frequency=[150e6, 123e6],
        telescope_name="TEST",
        feed_name="bob",
        feed_version="0.1",
        model_name="E-field pattern - Rigging height 4.9m",
        model_version="1.0",
    )

    pytest.raises(
        ValueError,
        beam1.read_cst_beam,
        [cst_files[0], cst_files[0], cst_files[0]],
        beam_type="power",
        feed_pol=["x", "y"],
        telescope_name="TEST",
        feed_name="bob",
        feed_version="0.1",
        model_name="E-field pattern - Rigging height 4.9m",
        model_version="1.0",
    )

    pytest.raises(
        ValueError,
        beam1.read_cst_beam,
        cst_files[0],
        beam_type="power",
        feed_pol=["x", "y"],
        telescope_name="TEST",
        feed_name="bob",
        feed_version="0.1",
        model_name="E-field pattern - Rigging height 4.9m",
        model_version="1.0",
    )

    pytest.raises(
        ValueError,
        beam1.read_cst_beam,
        [[cst_files[0]], [cst_files[1]]],
        beam_type="power",
        frequency=[150e6, 123e6],
        feed_pol=["x"],
        telescope_name="TEST",
        feed_name="bob",
        feed_version="0.1",
        model_name="E-field pattern - Rigging height 4.9m",
        model_version="1.0",
    )

    pytest.raises(
        ValueError,
        beam1.read_cst_beam,
        np.array([[cst_files[0]], [cst_files[1]]]),
        beam_type="power",
        frequency=[150e6, 123e6],
        feed_pol=["x"],
        telescope_name="TEST",
        feed_name="bob",
        feed_version="0.1",
        model_name="E-field pattern - Rigging height 4.9m",
        model_version="1.0",
    )

    pytest.raises(
        ValueError,
        beam1.read_cst_beam,
        cst_files,
        beam_type="power",
        frequency=[[150e6], [123e6]],
        telescope_name="TEST",
        feed_name="bob",
        feed_version="0.1",
        model_name="E-field pattern - Rigging height 4.9m",
        model_version="1.0",
    )

    pytest.raises(
        ValueError,
        beam1.read_cst_beam,
        cst_files,
        beam_type="power",
        frequency=np.array([[150e6], [123e6]]),
        telescope_name="TEST",
        feed_name="bob",
        feed_version="0.1",
        model_name="E-field pattern - Rigging height 4.9m",
        model_version="1.0",
    )

    pytest.raises(
        ValueError,
        beam1.read_cst_beam,
        cst_files,
        beam_type="power",
        feed_pol=[["x"], ["y"]],
        frequency=150e6,
        telescope_name="TEST",
        feed_name="bob",
        feed_version="0.1",
        model_name="E-field pattern - Rigging height 4.9m",
        model_version="1.0",
    )

    pytest.raises(
        ValueError,
        beam1.read_cst_beam,
        cst_files,
        beam_type="power",
        feed_pol=np.array([["x"], ["y"]]),
        frequency=150e6,
        telescope_name="TEST",
        feed_name="bob",
        feed_version="0.1",
        model_name="E-field pattern - Rigging height 4.9m",
        model_version="1.0",
    )


def test_read_efield(cst_efield_2freq):
    beam1 = cst_efield_2freq
    beam2 = UVBeam()

    assert beam1.pixel_coordinate_system == "az_za"
    assert beam1.beam_type == "efield"
    assert beam1.data_array.shape == (2, 1, 2, 2, 181, 360)
    assert np.max(np.abs(beam1.data_array)) == 90.97

    # test passing in other polarization
    beam2.read_cst_beam(
        cst_files,
        beam_type="efield",
        frequency=[150e6, 123e6],
        feed_pol="y",
        telescope_name="TEST",
        feed_name="bob",
        feed_version="0.1",
        model_name="E-field pattern - Rigging height 4.9m",
        model_version="1.0",
    )
    assert beam2.feed_array[0] == "y"
    assert beam2.feed_array[1] == "x"
    assert beam1.data_array.shape == (2, 1, 2, 2, 181, 360)
    assert np.allclose(
        beam1.data_array[:, :, 0, :, :, :], beam2.data_array[:, :, 0, :, :, :]
    )

    # test single frequency and not rotating the polarization
    with uvtest.check_warnings(
        UserWarning, "No frequency provided. Detected frequency is"
    ):
        beam2.read_cst_beam(
            cst_files[0],
            beam_type="efield",
            telescope_name="TEST",
            feed_name="bob",
            feed_version="0.1",
            model_name="E-field pattern - Rigging height 4.9m",
            model_version="1.0",
            rotate_pol=False,
        )

    assert beam2.pixel_coordinate_system == "az_za"
    assert beam2.beam_type == "efield"
    assert beam2.feed_array == np.array(["x"])
    assert beam2.data_array.shape == (2, 1, 1, 1, 181, 360)

    assert np.allclose(
        beam1.data_array[:, :, 0, 1, :, :], beam2.data_array[:, :, 0, 0, :, :]
    )

    # test reading in multiple polarization files
    beam1.read_cst_beam(
        [cst_files[0], cst_files[0]],
        beam_type="efield",
        frequency=[150e6],
        feed_pol=["x", "y"],
        telescope_name="TEST",
        feed_name="bob",
        feed_version="0.1",
        model_name="E-field pattern - Rigging height 4.9m",
        model_version="1.0",
    )
    assert beam1.data_array.shape == (2, 1, 2, 1, 181, 360)
    assert np.allclose(
        beam1.data_array[:, :, 0, :, :, :], beam1.data_array[:, :, 1, :, :, :]
    )


def test_no_deg_units(tmp_path):
    # need to write a modified file to test headers not in degrees
    testfile = os.path.join(tmp_path, "HERA_NicCST_150MHz_modified.txt")
    with open(cst_files[0], "r") as file:
        line1 = file.readline()
        line2 = file.readline()

    data = np.loadtxt(cst_files[0], skiprows=2)

    raw_names = line1.split("]")
    raw_names = [raw_name for raw_name in raw_names if "\n" not in raw_name]
    column_names = []
    column_names_simple = []
    units = []
    for raw_name in raw_names:
        column_name, unit = tuple(raw_name.split("["))
        column_names.append(column_name)
        column_names_simple.append("".join(column_name.lower().split(" ")))
        if unit != "deg.":
            units.append(unit)
        else:
            units.append("    ")

    new_column_headers = []
    for index, name in enumerate(column_names):
        new_column_headers.append(name + "[" + units[index] + "]")

    new_header = ""
    for col in new_column_headers:
        new_header += "{:12}".format(col)

    beam1 = UVBeam()
    beam2 = UVBeam()

    # format to match existing file
    existing_format = [
        "%8.3f",
        "%15.3f",
        "%20.3e",
        "%19.3e",
        "%19.3f",
        "%19.3e",
        "%19.3f",
        "%19.3e",
    ]
    np.savetxt(
        testfile,
        data,
        fmt=existing_format,
        header=new_header + "\n" + line2,
        comments="",
    )
    # this errors because the phi 2pi rotation doesn't work
    # (because they are degrees but the code thinks they're radians)
    pytest.raises(
        ValueError,
        beam1.read_cst_beam,
        testfile,
        beam_type="efield",
        frequency=np.array([150e6]),
        feed_pol="y",
        telescope_name="TEST",
        feed_name="bob",
        feed_version="0.1",
        model_name="E-field pattern - Rigging height 4.9m",
        model_version="1.0",
    )

    theta_col = np.where(np.array(column_names_simple) == "theta")[0][0]
    phi_col = np.where(np.array(column_names_simple) == "phi")[0][0]
    theta_phase_col = np.where(np.array(column_names_simple) == "phase(theta)")[0][0]
    phi_phase_col = np.where(np.array(column_names_simple) == "phase(phi)")[0][0]

    data[:, theta_col] = np.radians(data[:, theta_col])
    data[:, phi_col] = np.radians(data[:, phi_col])
    data[:, theta_phase_col] = np.radians(data[:, theta_phase_col])
    data[:, phi_phase_col] = np.radians(data[:, phi_phase_col])

    np.savetxt(
        testfile,
        data,
        fmt=existing_format,
        header=new_header + "\n" + line2,
        comments="",
    )
    # this errors because theta isn't regularly gridded (too few sig figs)
    pytest.raises(
        ValueError,
        beam1.read_cst_beam,
        testfile,
        beam_type="efield",
        frequency=np.array([150e6]),
        feed_pol="y",
        telescope_name="TEST",
        feed_name="bob",
        feed_version="0.1",
        model_name="E-field pattern - Rigging height 4.9m",
        model_version="1.0",
    )

    # use more decimal places for theta so that it is regularly gridded
    new_format = [
        "%15.12e",
        "%15.3e",
        "%20.3e",
        "%19.3e",
        "%19.3f",
        "%19.3e",
        "%19.3f",
        "%19.3e",
    ]
    np.savetxt(
        testfile, data, fmt=new_format, header=new_header + "\n" + line2, comments=""
    )
    # this errors because phi isn't regularly gridded (too few sig figs)
    pytest.raises(
        ValueError,
        beam1.read_cst_beam,
        testfile,
        beam_type="efield",
        frequency=np.array([150e6]),
        feed_pol="y",
        telescope_name="TEST",
        feed_name="bob",
        feed_version="0.1",
        model_name="E-field pattern - Rigging height 4.9m",
        model_version="1.0",
    )

    # use more decimal places so that it is regularly gridded and matches data
    new_format = [
        "%15.12e",
        "%15.12e",
        "%20.3e",
        "%19.3e",
        "%19.12f",
        "%19.3e",
        "%19.12f",
        "%19.3e",
    ]
    np.savetxt(
        testfile, data, fmt=new_format, header=new_header + "\n" + line2, comments=""
    )

    with uvtest.check_warnings(
        UserWarning, "No frequency provided. Detected frequency is"
    ):
        beam1.read_cst_beam(
            cst_files[0],
            beam_type="efield",
            telescope_name="TEST",
            feed_name="bob",
            feed_version="0.1",
            model_name="E-field pattern - Rigging height 4.9m",
            model_version="1.0",
        )

    with uvtest.check_warnings(
        UserWarning, "No frequency provided. Detected frequency is"
    ):
        beam2.read_cst_beam(
            testfile,
            beam_type="efield",
            telescope_name="TEST",
            feed_name="bob",
            feed_version="0.1",
            model_name="E-field pattern - Rigging height 4.9m",
            model_version="1.0",
        )

    assert beam1 == beam2

    # remove a row to make data not on a grid to catch that error
    data = data[1:, :]

    np.savetxt(
        testfile, data, fmt=new_format, header=new_header + "\n" + line2, comments=""
    )
    # this errors because theta & phi aren't on a strict grid
    pytest.raises(
        ValueError,
        beam1.read_cst_beam,
        testfile,
        beam_type="efield",
        frequency=np.array([150e6]),
        feed_pol="y",
        telescope_name="TEST",
        feed_name="bob",
        feed_version="0.1",
        model_name="E-field pattern - Rigging height 4.9m",
        model_version="1.0",
    )


def test_wrong_column_names(tmp_path):
    # need to write modified files to test headers with wrong column names
    testfile = os.path.join(tmp_path, "HERA_NicCST_150MHz_modified.txt")
    with open(cst_files[0], "r") as file:
        line1 = file.readline()
        line2 = file.readline()

    data = np.loadtxt(cst_files[0], skiprows=2)

    raw_names = line1.split("]")
    raw_names = [raw_name for raw_name in raw_names if "\n" not in raw_name]
    column_names = []
    missing_power_column_names = []
    extra_power_column_names = []
    column_names_simple = []
    units = []
    for raw_name in raw_names:
        column_name, unit = tuple(raw_name.split("["))
        column_names.append(column_name)
        column_names_simple.append("".join(column_name.lower().split(" ")))
        units.append(unit)
        if column_name.strip() == "Abs(V   )":
            missing_power_column_names.append("Power")
        else:
            missing_power_column_names.append(column_name)
        if column_name.strip() == "Abs(Theta)":
            extra_power_column_names.append("Abs(E   )")
        else:
            extra_power_column_names.append(column_name)

    missing_power_column_headers = []
    for index, name in enumerate(missing_power_column_names):
        missing_power_column_headers.append(name + "[" + units[index] + "]")

    extra_power_column_headers = []
    for index, name in enumerate(extra_power_column_names):
        extra_power_column_headers.append(name + "[" + units[index] + "]")

    missing_power_header = ""
    for col in missing_power_column_headers:
        missing_power_header += "{:12}".format(col)

    beam1 = UVBeam()

    # format to match existing file
    existing_format = [
        "%8.3f",
        "%15.3f",
        "%20.3e",
        "%19.3e",
        "%19.3f",
        "%19.3e",
        "%19.3f",
        "%19.3e",
    ]
    np.savetxt(
        testfile,
        data,
        fmt=existing_format,
        header=missing_power_header + "\n" + line2,
        comments="",
    )
    # this errors because there's no recognized power column
    pytest.raises(
        ValueError,
        beam1.read_cst_beam,
        testfile,
        beam_type="power",
        frequency=np.array([150e6]),
        telescope_name="TEST",
        feed_name="bob",
        feed_version="0.1",
        model_name="E-field pattern - Rigging height 4.9m",
        model_version="1.0",
    )

    extra_power_header = ""
    for col in extra_power_column_headers:
        extra_power_header += "{:12}".format(col)
    np.savetxt(
        testfile,
        data,
        fmt=existing_format,
        header=extra_power_header + "\n" + line2,
        comments="",
    )
    # this errors because there's multiple recognized power columns
    pytest.raises(
        ValueError,
        beam1.read_cst_beam,
        testfile,
        beam_type="power",
        frequency=np.array([150e6]),
        telescope_name="TEST",
        feed_name="bob",
        feed_version="0.1",
        model_name="E-field pattern - Rigging height 4.9m",
        model_version="1.0",
    )


def test_hera_yaml():
    pytest.importorskip("yaml")
    beam1 = UVBeam()
    beam2 = UVBeam()

    beam1.read_cst_beam(cst_yaml_vivaldi, beam_type="efield", frequency_select=[150e6])

    assert beam1.reference_impedance == 100
    extra_keywords = {
        "software": "CST 2016",
        "sim_type": "E-farfield",
        "layout": "1 antenna",
        "port_num": 1,
    }
    assert beam1.extra_keywords == extra_keywords

    beam2.read_cst_beam(cst_yaml_vivaldi, beam_type="power", frequency_select=[150e6])

    beam1.efield_to_power(calc_cross_pols=False)

    # The values in the beam file only have 4 sig figs, so they don't match precisely
    diff = np.abs(beam1.data_array - beam2.data_array)
    assert np.max(diff) < 2
    reldiff = diff / beam2.data_array
    assert np.max(reldiff) < 0.002

    # set data_array tolerances higher to test the rest of the object
    # tols are (relative, absolute)
    tols = [0.002, 0]
    beam1._data_array.tols = tols

    assert beam1.history != beam2.history
    beam1.history = beam2.history

    assert beam1 == beam2


# pyuvdata/uvbeam/tests/test_mwa_beam.py

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

import pytest
import numpy as np

from pyuvdata.data import DATA_PATH
from pyuvdata import UVBeam
from pyuvdata.uvbeam.mwa_beam import P1sin, P1sin_array
import pyuvdata.tests as uvtest
import pyuvdata.utils as uvutils

filename = os.path.join(DATA_PATH, "mwa_full_EE_test.h5")


@pytest.fixture(scope="module")
def mwa_beam_1ppd_main():
    beam = UVBeam()
    beam.read_mwa_beam(filename, pixels_per_deg=1)

    return beam


@pytest.fixture(scope="function")
def mwa_beam_1ppd(mwa_beam_1ppd_main):
    return mwa_beam_1ppd_main.copy()


def test_read_write_mwa(mwa_beam_1ppd, tmp_path):
    """Basic read/write test."""
    beam1 = mwa_beam_1ppd
    beam2 = UVBeam()

    beam1.read_mwa_beam(filename, pixels_per_deg=1)
    assert beam1.filename == ["mwa_full_EE_test.h5"]

    assert beam1.pixel_coordinate_system == "az_za"
    assert beam1.beam_type == "efield"
    assert beam1.data_array.shape == (2, 1, 2, 3, 91, 360)

    # this is entirely empirical, just to prevent unexpected changes.
    # The actual values have been validated through external tests against
    # the mwa_pb repo.
    assert np.isclose(np.max(np.abs(beam1.data_array)), 0.6823676193472403)

    assert "x" in beam1.feed_array
    assert "y" in beam1.feed_array
    assert beam1.x_orientation == "east"

    outfile_name = str(tmp_path / "mwa_beam_out.fits")
    beam1.write_beamfits(outfile_name, clobber=True)

    beam2.read_beamfits(outfile_name)

    assert beam1 == beam2


def test_freq_range(mwa_beam_1ppd):
    beam1 = mwa_beam_1ppd
    beam2 = UVBeam()

    # include all
    beam2.read_mwa_beam(filename, pixels_per_deg=1, freq_range=[100e6, 200e6])
    assert beam1 == beam2

    beam2.read_mwa_beam(filename, pixels_per_deg=1, freq_range=[100e6, 150e6])
    beam1.select(freq_chans=[0, 1])
    assert beam1.history != beam2.history
    beam1.history = beam2.history
    assert beam1 == beam2

    with uvtest.check_warnings(
        UserWarning, match="Only one available frequency in freq_range"
    ):
        beam1.read_mwa_beam(filename, pixels_per_deg=1, freq_range=[100e6, 130e6])

    with pytest.raises(ValueError, match="No frequencies available in freq_range"):
        beam2.read_mwa_beam(filename, pixels_per_deg=1, freq_range=[100e6, 110e6])

    with pytest.raises(ValueError, match="freq_range must have 2 elements."):
        beam2.read_mwa_beam(filename, pixels_per_deg=1, freq_range=[100e6])


def test_p1sin_array():
    pixels_per_deg = 5
    nmax = 10
    n_theta = np.floor(90 * pixels_per_deg) + 1
    theta_arr = np.deg2rad(np.arange(0, n_theta) / pixels_per_deg)
    (P_sin, P1) = P1sin_array(nmax, theta_arr)

    P_sin_orig = np.zeros((nmax ** 2 + 2 * nmax, np.size(theta_arr)))
    P1_orig = np.zeros((nmax ** 2 + 2 * nmax, np.size(theta_arr)))
    for theta_i, theta in enumerate(theta_arr):
        P_sin_temp, P1_temp = P1sin(nmax, theta)
        P_sin_orig[:, theta_i] = P_sin_temp
        P1_orig[:, theta_i] = P1_temp

    assert np.allclose(P1_orig, P1.T)
    assert np.allclose(P_sin_orig, P_sin.T)


def test_bad_amps():
    beam1 = UVBeam()

    amps = np.ones([2, 8])
    with pytest.raises(ValueError) as cm:
        beam1.read_mwa_beam(filename, pixels_per_deg=1, amplitudes=amps)
    assert str(cm.value).startswith("amplitudes must be shape")


def test_bad_delays():
    beam1 = UVBeam()

    delays = np.zeros([2, 8], dtype="int")
    with pytest.raises(ValueError) as cm:
        beam1.read_mwa_beam(filename, pixels_per_deg=1, delays=delays)
    assert str(cm.value).startswith("delays must be shape")

    delays = np.zeros((2, 16), dtype="int")
    delays = delays + 64
    with pytest.raises(ValueError) as cm:
        beam1.read_mwa_beam(filename, pixels_per_deg=1, delays=delays)
    assert str(cm.value).startswith("There are delays greater than 32")

    delays = np.zeros((2, 16), dtype="float")
    with pytest.raises(ValueError) as cm:
        beam1.read_mwa_beam(filename, pixels_per_deg=1, delays=delays)
    assert str(cm.value).startswith("Delays must be integers.")


def test_dead_dipoles():
    beam1 = UVBeam()

    delays = np.zeros((2, 16), dtype="int")
    delays[:, 0] = 32

    with uvtest.check_warnings(UserWarning, "There are some terminated dipoles"):
        beam1.read_mwa_beam(filename, pixels_per_deg=1, delays=delays)

    delay_str = (
        "[[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], "
        "[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]"
    )
    gain_str = (
        "[[0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, "
        "1.0, 1.0, 1.0, 1.0, 1.0], "
        "[0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, "
        "1.0, 1.0, 1.0, 1.0]]"
    )
    history_str = (
        "Sujito et al. full embedded element beam, derived from "
        "https://github.com/MWATelescope/mwa_pb/"
        + "  delays set to "
        + delay_str
        + "  gains set to "
        + gain_str
        + beam1.pyuvdata_version_str
    )
    assert uvutils._check_histories(history_str, beam1.history)


# pyuvdata/uvbeam/tests/test_uvbeam.py

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

"""Tests for uvbeam object.

"""
import os
import copy

import numpy as np
from astropy import units
from astropy.io import fits
from astropy.coordinates import Angle
import pytest

from pyuvdata import UVBeam
import pyuvdata.tests as uvtest
import pyuvdata.utils as uvutils
from pyuvdata.data import DATA_PATH

from pyuvdata.uvbeam.tests.test_mwa_beam import filename as mwa_beam_file
from pyuvdata.uvbeam.tests.test_cst_beam import cst_files, cst_yaml_file


try:
    from astropy_healpix import HEALPix

    healpix_installed = True
except (ImportError):
    healpix_installed = False

casa_beamfits = os.path.join(DATA_PATH, "HERABEAM.FITS")


@pytest.fixture(scope="function")
def uvbeam_data():
    """Setup and teardown for basic parameter, property and iterator tests."""
    required_properties = [
        "beam_type",
        "Nfreqs",
        "Naxes_vec",
        "Nspws",
        "pixel_coordinate_system",
        "freq_array",
        "spw_array",
        "data_normalization",
        "data_array",
        "bandpass_array",
        "telescope_name",
        "feed_name",
        "feed_version",
        "model_name",
        "model_version",
        "history",
        "antenna_type",
    ]
    required_parameters = ["_" + prop for prop in required_properties]

    extra_properties = [
        "Naxes1",
        "Naxes2",
        "Npixels",
        "Nfeeds",
        "Npols",
        "Ncomponents_vec",
        "axis1_array",
        "axis2_array",
        "nside",
        "ordering",
        "pixel_array",
        "feed_array",
        "polarization_array",
        "basis_vector_array",
        "extra_keywords",
        "Nelements",
        "element_coordinate_system",
        "element_location_array",
        "delay_array",
        "x_orientation",
        "interpolation_function",
        "freq_interp_kind",
        "gain_array",
        "coupling_matrix",
        "reference_impedance",
        "receiver_temperature_array",
        "loss_array",
        "mismatch_array",
        "s_parameters",
        "filename",
    ]
    extra_parameters = ["_" + prop for prop in extra_properties]

    other_properties = ["pyuvdata_version_str"]

    beam_obj = UVBeam()

    class DataHolder:
        def __init__(
            self,
            beam_obj,
            required_parameters,
            required_properties,
            extra_parameters,
            extra_properties,
            other_properties,
        ):
            self.beam_obj = beam_obj
            self.required_parameters = required_parameters
            self.required_properties = required_properties
            self.extra_parameters = extra_parameters
            self.extra_properties = extra_properties
            self.other_properties = other_properties

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

    # some post-test object cleanup
    del uvbeam_data

    return


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


def test_required_parameter_iter(uvbeam_data):
    """Test expected required parameters."""
    required = []
    for prop in uvbeam_data.beam_obj.required():
        required.append(prop)
    for a in uvbeam_data.required_parameters:
        assert a in required, (
            "expected attribute " + a + " not returned in required iterator"
        )


def test_extra_parameter_iter(uvbeam_data):
    """Test expected optional parameters."""
    extra = []
    for prop in uvbeam_data.beam_obj.extra():
        extra.append(prop)
    for a in uvbeam_data.extra_parameters:
        assert a in extra, "expected attribute " + a + " not returned in extra iterator"


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


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


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


def test_set_cs_params(cst_efield_2freq):
    """
    Test _set_cs_params.
    """
    efield_beam = cst_efield_2freq
    efield_beam2 = efield_beam.copy()

    efield_beam2._set_cs_params()
    assert efield_beam2 == efield_beam


def test_set_efield(cst_efield_2freq):
    """
    Test _set_efield parameter settings.
    """
    efield_beam = cst_efield_2freq
    efield_beam2 = efield_beam.copy()

    efield_beam2._set_efield()

    assert efield_beam2 == efield_beam


def test_set_power(cst_power_2freq):
    """
    Test _set_power parameter settings.
    """
    power_beam = cst_power_2freq
    power_beam2 = power_beam.copy()

    power_beam2._set_power()

    assert power_beam2 == power_beam


def test_set_antenna_type(cst_efield_2freq):
    """
    Test set_simple and set_phased_array parameter settings.
    """
    efield_beam = cst_efield_2freq
    efield_beam2 = efield_beam.copy()

    efield_beam2._set_simple()

    assert efield_beam2 == efield_beam

    efield_beam._set_phased_array()
    assert efield_beam2 != efield_beam


def test_errors():
    beam_obj = UVBeam()
    with pytest.raises(ValueError, match="filetype must be beamfits"):
        beam_obj._convert_to_filetype("foo")


def test_peak_normalize(cst_efield_2freq, cst_power_2freq):
    efield_beam = cst_efield_2freq

    orig_bandpass_array = copy.deepcopy(efield_beam.bandpass_array)
    maxima = np.zeros(efield_beam.Nfreqs)
    for freq_i in range(efield_beam.Nfreqs):
        maxima[freq_i] = np.amax(abs(efield_beam.data_array[:, :, :, freq_i]))
    efield_beam.peak_normalize()
    assert np.amax(abs(efield_beam.data_array)) == 1
    assert np.sum(abs(efield_beam.bandpass_array - orig_bandpass_array * maxima)) == 0
    assert efield_beam.data_normalization == "peak"

    power_beam = cst_power_2freq

    orig_bandpass_array = copy.deepcopy(power_beam.bandpass_array)
    maxima = np.zeros(efield_beam.Nfreqs)
    for freq_i in range(efield_beam.Nfreqs):
        maxima[freq_i] = np.amax(power_beam.data_array[:, :, :, freq_i])
    power_beam.peak_normalize()
    assert np.amax(abs(power_beam.data_array)) == 1
    assert np.sum(abs(power_beam.bandpass_array - orig_bandpass_array * maxima)) == 0
    assert power_beam.data_normalization == "peak"

    power_beam.data_normalization = "solid_angle"
    with pytest.raises(
        NotImplementedError,
        match="Conversion from solid_angle to peak "
        "normalization is not yet implemented",
    ):
        power_beam.peak_normalize()


def test_stokes_matrix():
    beam = UVBeam()
    with pytest.raises(ValueError, match="n must be positive integer."):
        beam._stokes_matrix(-2)
    with pytest.raises(ValueError, match="n should lie between 0 and 3."):
        beam._stokes_matrix(5)


def test_efield_to_pstokes(cst_efield_2freq_cut, cst_efield_2freq_cut_healpix):
    pstokes_beam_2 = cst_efield_2freq_cut_healpix
    # convert to pstokes after interpolating
    beam_return = pstokes_beam_2.efield_to_pstokes(inplace=False)

    pstokes_beam = cst_efield_2freq_cut

    # interpolate after converting to pstokes
    pstokes_beam.interpolation_function = "az_za_simple"
    pstokes_beam.efield_to_pstokes()
    pstokes_beam.to_healpix()

    pstokes_beam.peak_normalize()
    beam_return.peak_normalize()

    # NOTE:  So far, the following doesn't hold unless the beams are
    # peak_normalized again.
    # This seems to be the fault of interpolation
    assert np.allclose(pstokes_beam.data_array, beam_return.data_array, atol=1e-2)


def test_efield_to_pstokes_error(cst_power_2freq_cut):
    power_beam = cst_power_2freq_cut

    with pytest.raises(ValueError, match="beam_type must be efield."):
        power_beam.efield_to_pstokes()


def test_efield_to_power(cst_efield_2freq_cut, cst_power_2freq_cut):
    efield_beam = cst_efield_2freq_cut
    power_beam = cst_power_2freq_cut

    new_power_beam = efield_beam.efield_to_power(calc_cross_pols=False, inplace=False)

    # The values in the beam file only have 4 sig figs, so they don't match precisely
    diff = np.abs(new_power_beam.data_array - power_beam.data_array)
    assert np.max(diff) < 2
    reldiff = diff / power_beam.data_array
    assert np.max(reldiff) < 0.002

    # set data_array tolerances higher to test the rest of the object
    # tols are (relative, absolute)
    tols = [0.002, 0]
    power_beam._data_array.tols = tols
    # modify the history to match
    power_beam.history += " Converted from efield to power using pyuvdata."
    assert power_beam == new_power_beam


def test_efield_to_power_1feed(cst_efield_2freq_cut, cst_power_2freq_cut):
    efield_beam = cst_efield_2freq_cut
    efield_beam.select(feeds=["x"])

    power_beam = cst_power_2freq_cut
    power_beam.select(polarizations=["xx"])

    new_power_beam = efield_beam.efield_to_power(calc_cross_pols=True, inplace=False)

    # The values in the beam file only have 4 sig figs, so they don't match precisely
    diff = np.abs(new_power_beam.data_array - power_beam.data_array)
    assert np.max(diff) < 2
    reldiff = diff / power_beam.data_array
    assert np.max(reldiff) < 0.002

    # set data_array tolerances higher to test the rest of the object
    # tols are (relative, absolute)
    tols = [0.002, 0]
    power_beam._data_array.tols = tols
    # modify the history to match
    power_beam.history = new_power_beam.history

    assert power_beam == new_power_beam


def test_efield_to_power_nonorthogonal(cst_efield_2freq_cut):
    efield_beam = cst_efield_2freq_cut

    new_power_beam = efield_beam.efield_to_power(calc_cross_pols=False, inplace=False)

    # test with non-orthogonal basis vectors
    # first construct a beam with non-orthogonal basis vectors
    new_basis_vecs = np.zeros_like(efield_beam.basis_vector_array)
    new_basis_vecs[0, 0, :, :] = np.sqrt(0.5)
    new_basis_vecs[0, 1, :, :] = np.sqrt(0.5)
    new_basis_vecs[1, :, :, :] = efield_beam.basis_vector_array[1, :, :, :]
    new_data = np.zeros_like(efield_beam.data_array)
    # drop all the trailing colons in the slicing below
    new_data[0] = np.sqrt(2) * efield_beam.data_array[0]
    new_data[1] = efield_beam.data_array[1] - efield_beam.data_array[0]
    efield_beam2 = efield_beam.copy()
    efield_beam2.basis_vector_array = new_basis_vecs
    efield_beam2.data_array = new_data
    efield_beam2.check()
    # now convert to power. Should get the same result
    new_power_beam2 = efield_beam2.copy()
    new_power_beam2.efield_to_power(calc_cross_pols=False)

    assert new_power_beam == new_power_beam2

    if healpix_installed:
        # check that this raises an error if trying to convert to HEALPix:
        efield_beam2.interpolation_function = "az_za_simple"
        with pytest.raises(
            NotImplementedError,
            match="interpolation for input basis vectors that are not aligned to the "
            "native theta/phi coordinate system is not yet supported",
        ):
            efield_beam2.to_healpix(inplace=False)

    # now try a different rotation to non-orthogonal basis vectors
    new_basis_vecs = np.zeros_like(efield_beam.basis_vector_array)
    new_basis_vecs[0, :, :, :] = efield_beam.basis_vector_array[0, :, :, :]
    new_basis_vecs[1, 0, :, :] = np.sqrt(0.5)
    new_basis_vecs[1, 1, :, :] = np.sqrt(0.5)
    new_data = np.zeros_like(efield_beam.data_array)
    new_data[0, :, :, :, :, :] = (
        efield_beam.data_array[0, :, :, :, :, :]
        - efield_beam.data_array[1, :, :, :, :, :]
    )
    new_data[1, :, :, :, :, :] = np.sqrt(2) * efield_beam.data_array[1, :, :, :, :, :]
    efield_beam2 = efield_beam.copy()
    efield_beam2.basis_vector_array = new_basis_vecs
    efield_beam2.data_array = new_data
    efield_beam2.check()
    # now convert to power. Should get the same result
    new_power_beam2 = efield_beam2.copy()
    new_power_beam2.efield_to_power(calc_cross_pols=False)

    assert new_power_beam == new_power_beam2


def test_efield_to_power_rotated(cst_efield_2freq_cut):
    efield_beam = cst_efield_2freq_cut

    new_power_beam = efield_beam.efield_to_power(calc_cross_pols=False, inplace=False)

    # now construct a beam with  orthogonal but rotated basis vectors
    new_basis_vecs = np.zeros_like(efield_beam.basis_vector_array)
    new_basis_vecs[0, 0, :, :] = np.sqrt(0.5)
    new_basis_vecs[0, 1, :, :] = np.sqrt(0.5)
    new_basis_vecs[1, 0, :, :] = -1 * np.sqrt(0.5)
    new_basis_vecs[1, 1, :, :] = np.sqrt(0.5)
    new_data = np.zeros_like(efield_beam.data_array)
    new_data[0, :, :, :, :, :] = np.sqrt(0.5) * (
        efield_beam.data_array[0, :, :, :, :, :]
        + efield_beam.data_array[1, :, :, :, :, :]
    )
    new_data[1, :, :, :, :, :] = np.sqrt(0.5) * (
        -1 * efield_beam.data_array[0, :, :, :, :, :]
        + efield_beam.data_array[1, :, :, :, :, :]
    )
    efield_beam2 = efield_beam.copy()
    efield_beam2.basis_vector_array = new_basis_vecs
    efield_beam2.data_array = new_data
    efield_beam2.check()
    # now convert to power. Should get the same result
    new_power_beam2 = efield_beam2.copy()
    new_power_beam2.efield_to_power(calc_cross_pols=False)

    assert new_power_beam == new_power_beam2


def test_efield_to_power_crosspol(cst_efield_2freq_cut, tmp_path):
    efield_beam = cst_efield_2freq_cut

    # test calculating cross pols
    new_power_beam = efield_beam.efield_to_power(calc_cross_pols=True, inplace=False)
    assert np.all(
        np.abs(
            new_power_beam.data_array[
                :, :, 0, :, :, np.where(new_power_beam.axis1_array == 0)[0]
            ]
        )
        > np.abs(
            new_power_beam.data_array[
                :, :, 2, :, :, np.where(new_power_beam.axis1_array == 0)[0]
            ]
        )
    )
    assert np.all(
        np.abs(
            new_power_beam.data_array[
                :, :, 0, :, :, np.where(new_power_beam.axis1_array == np.pi / 2.0)[0]
            ]
        )
        > np.abs(
            new_power_beam.data_array[
                :, :, 2, :, :, np.where(new_power_beam.axis1_array == np.pi / 2.0)[0]
            ]
        )
    )
    # test writing out & reading back in power files (with cross pols which are complex)
    write_file = str(tmp_path / "outtest_beam.fits")
    new_power_beam.write_beamfits(write_file, clobber=True)
    new_power_beam2 = UVBeam()
    new_power_beam2.read_beamfits(write_file)
    assert new_power_beam == new_power_beam2

    # test keeping basis vectors
    new_power_beam = efield_beam.efield_to_power(
        calc_cross_pols=False, keep_basis_vector=True, inplace=False
    )
    assert np.allclose(new_power_beam.data_array, np.abs(efield_beam.data_array) ** 2)


def test_efield_to_power_errors(cst_efield_2freq_cut, cst_power_2freq_cut):
    efield_beam = cst_efield_2freq_cut
    power_beam = cst_power_2freq_cut

    # test raises error if beam is already a power beam
    with pytest.raises(ValueError, match="beam_type must be efield"):
        power_beam.efield_to_power()

    # test raises error if input efield beam has Naxes_vec=3
    efield_beam.Naxes_vec = 3
    with pytest.raises(
        ValueError,
        match="Conversion to power with 3-vector efields is not currently supported",
    ):
        efield_beam.efield_to_power()


def test_freq_interpolation(cst_power_2freq):
    power_beam = cst_power_2freq

    power_beam.interpolation_function = "az_za_simple"

    # test frequency interpolation returns data arrays for small and large tolerances
    freq_orig_vals = np.array([123e6, 150e6])
    interp_data, interp_basis_vector, interp_bandpass = power_beam.interp(
        freq_array=freq_orig_vals, freq_interp_tol=0.0, return_bandpass=True
    )
    assert isinstance(interp_data, np.ndarray)
    assert isinstance(interp_bandpass, np.ndarray)
    np.testing.assert_array_almost_equal(power_beam.bandpass_array, interp_bandpass)
    np.testing.assert_array_almost_equal(power_beam.data_array, interp_data)
    assert interp_basis_vector is None

    interp_data, interp_basis_vector, interp_bandpass = power_beam.interp(
        freq_array=freq_orig_vals, freq_interp_tol=1.0, return_bandpass=True
    )
    assert isinstance(interp_data, np.ndarray)
    assert isinstance(interp_bandpass, np.ndarray)
    np.testing.assert_array_almost_equal(power_beam.bandpass_array, interp_bandpass)
    np.testing.assert_array_almost_equal(power_beam.data_array, interp_data)
    assert interp_basis_vector is None

    # test frequency interpolation returns new UVBeam for small and large tolerances
    power_beam.saved_interp_functions = {}
    new_beam_obj = power_beam.interp(
        freq_array=freq_orig_vals, freq_interp_tol=0.0, new_object=True
    )
    assert isinstance(new_beam_obj, UVBeam)
    np.testing.assert_array_almost_equal(new_beam_obj.freq_array[0], freq_orig_vals)
    assert new_beam_obj.freq_interp_kind == "linear"
    # test that saved functions are erased in new obj
    assert not hasattr(new_beam_obj, "saved_interp_functions")
    assert power_beam.history != new_beam_obj.history
    new_beam_obj.history = power_beam.history
    assert power_beam == new_beam_obj

    new_beam_obj = power_beam.interp(
        freq_array=freq_orig_vals, freq_interp_tol=1.0, new_object=True
    )
    assert isinstance(new_beam_obj, UVBeam)
    np.testing.assert_array_almost_equal(new_beam_obj.freq_array[0], freq_orig_vals)
    # assert interp kind is 'nearest' when within tol
    assert new_beam_obj.freq_interp_kind == "nearest"
    new_beam_obj.freq_interp_kind = "linear"
    assert power_beam.history != new_beam_obj.history
    new_beam_obj.history = power_beam.history
    assert power_beam == new_beam_obj

    # test frequency interpolation returns valid new UVBeam for different
    # number of freqs from input
    power_beam.saved_interp_functions = {}
    new_beam_obj = power_beam.interp(
        freq_array=np.linspace(123e6, 150e6, num=5),
        freq_interp_tol=0.0,
        new_object=True,
    )

    assert isinstance(new_beam_obj, UVBeam)
    np.testing.assert_array_almost_equal(
        new_beam_obj.freq_array[0], np.linspace(123e6, 150e6, num=5)
    )
    assert new_beam_obj.freq_interp_kind == "linear"
    # test that saved functions are erased in new obj
    assert not hasattr(new_beam_obj, "saved_interp_functions")
    assert power_beam.history != new_beam_obj.history
    new_beam_obj.history = power_beam.history

    # down select to orig freqs and test equality
    new_beam_obj.select(frequencies=freq_orig_vals)
    assert power_beam.history != new_beam_obj.history
    new_beam_obj.history = power_beam.history
    assert power_beam == new_beam_obj

    # using only one freq chan should trigger a ValueError if interp_bool is True
    # unless requesting the original frequency channel such that interp_bool is False.
    # Therefore, to test that interp_bool is False returns array slice as desired,
    # test that ValueError is not raised in this case.
    # Other ways of testing this (e.g. interp_data_array.flags['OWNDATA']) does not work
    _pb = power_beam.select(frequencies=power_beam.freq_array[0, :1], inplace=False)
    try:
        interp_data, interp_basis_vector = _pb.interp(freq_array=_pb.freq_array[0])
    except ValueError:
        raise AssertionError("UVBeam.interp didn't return an array slice as expected")

    # test errors if one frequency
    power_beam_singlef = power_beam.select(freq_chans=[0], inplace=False)
    with pytest.raises(
        ValueError, match="Only one frequency in UVBeam so cannot interpolate."
    ):
        power_beam_singlef.interp(freq_array=np.array([150e6]))

    # assert freq_interp_kind ValueError
    power_beam.interpolation_function = "az_za_simple"
    power_beam.freq_interp_kind = None
    with pytest.raises(
        ValueError, match="freq_interp_kind must be set on object first"
    ):
        power_beam.interp(
            az_array=power_beam.axis1_array,
            za_array=power_beam.axis2_array,
            freq_array=freq_orig_vals,
            polarizations=["xx"],
        )


def test_freq_interp_real_and_complex(cst_power_2freq):
    # test interpolation of real and complex data are the same
    power_beam = cst_power_2freq

    power_beam.interpolation_function = "az_za_simple"

    # make a new object with more frequencies
    freqs = np.linspace(123e6, 150e6, 4)
    power_beam.freq_interp_kind = "linear"
    pbeam = power_beam.interp(freq_array=freqs, new_object=True)

    # modulate the data
    pbeam.data_array[:, :, :, 1] *= 2
    pbeam.data_array[:, :, :, 2] *= 0.5

    # interpolate cubic on real data
    freqs = np.linspace(123e6, 150e6, 10)
    pbeam.freq_interp_kind = "cubic"
    pb_int = pbeam.interp(freq_array=freqs)[0]

    # interpolate cubic on complex data and compare to ensure they are the same
    pbeam.data_array = pbeam.data_array.astype(np.complex128)
    pb_int2 = pbeam.interp(freq_array=freqs)[0]
    assert np.all(np.isclose(np.abs(pb_int - pb_int2), 0))


@pytest.mark.parametrize("beam_type", ["efield", "power"])
def test_spatial_interpolation_samepoints(
    beam_type, cst_power_2freq_cut, cst_efield_2freq_cut
):
    """
    check that interpolating to existing points gives the same answer
    """
    if beam_type == "power":
        uvbeam = cst_power_2freq_cut
    else:
        uvbeam = cst_efield_2freq_cut

    za_orig_vals, az_orig_vals = np.meshgrid(uvbeam.axis2_array, uvbeam.axis1_array)
    az_orig_vals = az_orig_vals.ravel(order="C")
    za_orig_vals = za_orig_vals.ravel(order="C")
    freq_orig_vals = np.array([123e6, 150e6])

    # test error if no interpolation function is set
    with pytest.raises(
        ValueError, match="interpolation_function must be set on object first"
    ):
        uvbeam.interp(
            az_array=az_orig_vals, za_array=za_orig_vals, freq_array=freq_orig_vals,
        )

    uvbeam.interpolation_function = "az_za_simple"
    interp_data_array, interp_basis_vector = uvbeam.interp(
        az_array=az_orig_vals, za_array=za_orig_vals, freq_array=freq_orig_vals
    )

    interp_data_array = interp_data_array.reshape(uvbeam.data_array.shape, order="F")
    assert np.allclose(uvbeam.data_array, interp_data_array)
    if beam_type == "efield":
        interp_basis_vector = interp_basis_vector.reshape(
            uvbeam.basis_vector_array.shape, order="F"
        )
        assert np.allclose(uvbeam.basis_vector_array, interp_basis_vector)

    # test that new object from interpolation is identical
    new_beam = uvbeam.interp(
        az_array=uvbeam.axis1_array,
        za_array=uvbeam.axis2_array,
        az_za_grid=True,
        freq_array=freq_orig_vals,
        new_object=True,
    )
    assert new_beam.freq_interp_kind == "nearest"
    assert new_beam.history == (
        uvbeam.history + " Interpolated in "
        "frequency and to a new azimuth/zenith "
        "angle grid using pyuvdata with "
        "interpolation_function = az_za_simple "
        "and freq_interp_kind = nearest."
    )
    # make histories & freq_interp_kind equal
    new_beam.history = uvbeam.history
    new_beam.freq_interp_kind = "linear"
    assert new_beam == uvbeam

    # test error if new_object set without az_za_grid
    with pytest.raises(ValueError, match="A new object can only be returned"):
        uvbeam.interp(
            az_array=az_orig_vals,
            za_array=za_orig_vals,
            freq_array=freq_orig_vals,
            new_object=True,
        )

    if beam_type == "power":
        # test only a single polarization
        interp_data_array, interp_basis_vector = uvbeam.interp(
            az_array=az_orig_vals,
            za_array=za_orig_vals,
            freq_array=freq_orig_vals,
            polarizations=["xx"],
        )

        data_array_compare = uvbeam.data_array[:, :, :1]
        interp_data_array = interp_data_array.reshape(
            data_array_compare.shape, order="F"
        )
        assert np.allclose(data_array_compare, interp_data_array)


@pytest.mark.parametrize("beam_type", ["efield", "power"])
def test_spatial_interpolation_everyother(
    beam_type, cst_power_2freq_cut, cst_efield_2freq_cut
):
    """
    test that interp to every other point returns an object that matches a select
    """
    if beam_type == "power":
        uvbeam = cst_power_2freq_cut
    else:
        uvbeam = cst_efield_2freq_cut
    uvbeam.interpolation_function = "az_za_simple"

    axis1_inds = np.arange(0, uvbeam.Naxes1, 2)
    axis2_inds = np.arange(0, uvbeam.Naxes2, 2)

    select_beam = uvbeam.select(
        axis1_inds=axis1_inds, axis2_inds=axis2_inds, inplace=False
    )
    interp_beam = uvbeam.interp(
        az_array=uvbeam.axis1_array[axis1_inds],
        za_array=uvbeam.axis2_array[axis2_inds],
        az_za_grid=True,
        new_object=True,
    )
    assert select_beam.history != interp_beam.history
    interp_beam.history = select_beam.history
    assert select_beam == interp_beam

    # test no errors using different points
    az_interp_vals = np.array(
        np.arange(0, 2 * np.pi, np.pi / 9.0).tolist()
        + np.arange(0, 2 * np.pi, np.pi / 9.0).tolist()
    )
    za_interp_vals = np.array(
        (np.zeros((18)) + np.pi / 18).tolist() + (np.zeros((18)) + np.pi / 36).tolist()
    )
    freq_interp_vals = np.arange(125e6, 145e6, 5e6)

    interp_data_array, interp_basis_vector = uvbeam.interp(
        az_array=az_interp_vals, za_array=za_interp_vals, freq_array=freq_interp_vals
    )

    if beam_type == "power":
        # Test requesting separate polarizations on different calls
        # while reusing splines.
        interp_data_array, interp_basis_vector = uvbeam.interp(
            az_array=az_interp_vals[:2],
            za_array=za_interp_vals[:2],
            freq_array=freq_interp_vals,
            polarizations=["xx"],
            reuse_spline=True,
        )

        interp_data_array, interp_basis_vector = uvbeam.interp(
            az_array=az_interp_vals[:2],
            za_array=za_interp_vals[:2],
            freq_array=freq_interp_vals,
            polarizations=["yy"],
            reuse_spline=True,
        )

    # test reusing the spline fit.
    orig_data_array, interp_basis_vector = uvbeam.interp(
        az_array=az_interp_vals,
        za_array=za_interp_vals,
        freq_array=freq_interp_vals,
        reuse_spline=True,
    )

    reused_data_array, interp_basis_vector = uvbeam.interp(
        az_array=az_interp_vals,
        za_array=za_interp_vals,
        freq_array=freq_interp_vals,
        reuse_spline=True,
    )
    assert np.all(reused_data_array == orig_data_array)

    # test passing spline options
    spline_opts = {"kx": 4, "ky": 4}
    quartic_data_array, interp_basis_vector = uvbeam.interp(
        az_array=az_interp_vals,
        za_array=za_interp_vals,
        freq_array=freq_interp_vals,
        spline_opts=spline_opts,
    )

    # slightly different interpolation, so not identical.
    assert np.allclose(quartic_data_array, orig_data_array, atol=1e-10)
    assert not np.all(quartic_data_array == orig_data_array)

    select_data_array_orig, interp_basis_vector = uvbeam.interp(
        az_array=az_interp_vals[0:1],
        za_array=za_interp_vals[0:1],
        freq_array=np.array([127e6]),
    )

    select_data_array_reused, interp_basis_vector = uvbeam.interp(
        az_array=az_interp_vals[0:1],
        za_array=za_interp_vals[0:1],
        freq_array=np.array([127e6]),
        reuse_spline=True,
    )
    assert np.allclose(select_data_array_orig, select_data_array_reused)
    del uvbeam.saved_interp_functions


@pytest.mark.parametrize("beam_type", ["efield", "power"])
def test_spatial_interp_cutsky(beam_type, cst_power_2freq_cut, cst_efield_2freq_cut):
    """
    Test that when the beam doesn't cover the full sky it still works.
    """
    if beam_type == "power":
        uvbeam = cst_power_2freq_cut
    else:
        uvbeam = cst_efield_2freq_cut
    uvbeam.interpolation_function = "az_za_simple"

    # limit phi range
    axis1_inds = np.arange(0, np.ceil(uvbeam.Naxes1 / 2), dtype=int)
    axis2_inds = np.arange(0, uvbeam.Naxes2)

    uvbeam.select(axis1_inds=axis1_inds, axis2_inds=axis2_inds)

    # now do every other point test.
    axis1_inds = np.arange(0, uvbeam.Naxes1, 2)
    axis2_inds = np.arange(0, uvbeam.Naxes2, 2)

    select_beam = uvbeam.select(
        axis1_inds=axis1_inds, axis2_inds=axis2_inds, inplace=False
    )
    interp_beam = uvbeam.interp(
        az_array=uvbeam.axis1_array[axis1_inds],
        za_array=uvbeam.axis2_array[axis2_inds],
        az_za_grid=True,
        new_object=True,
    )
    assert select_beam.history != interp_beam.history
    interp_beam.history = select_beam.history
    assert select_beam == interp_beam


def test_spatial_interpolation_errors(cst_power_2freq_cut):
    """
    test that interp to every other point returns an object that matches a select
    """
    uvbeam = cst_power_2freq_cut
    uvbeam.interpolation_function = "az_za_simple"

    az_interp_vals = np.array(
        np.arange(0, 2 * np.pi, np.pi / 9.0).tolist()
        + np.arange(0, 2 * np.pi, np.pi / 9.0).tolist()
    )
    za_interp_vals = np.array(
        (np.zeros((18)) + np.pi / 18).tolist() + (np.zeros((18)) + np.pi / 36).tolist()
    )
    freq_interp_vals = np.arange(125e6, 145e6, 5e6)

    # test errors if frequency interp values outside range
    with pytest.raises(
        ValueError,
        match="at least one interpolation frequency is outside of "
        "the UVBeam freq_array range.",
    ):
        uvbeam.interp(
            az_array=az_interp_vals,
            za_array=za_interp_vals,
            freq_array=np.array([100]),
        )

    # test errors if positions outside range
    with pytest.raises(
        ValueError,
        match="at least one interpolation location "
        "is outside of the UVBeam pixel coverage.",
    ):
        uvbeam.interp(
            az_array=az_interp_vals, za_array=za_interp_vals + np.pi / 2,
        )

    # test no errors only frequency interpolation
    interp_data_array, interp_basis_vector = uvbeam.interp(freq_array=freq_interp_vals)

    # assert polarization value error
    with pytest.raises(
        ValueError,
        match="Requested polarization 1 not found in self.polarization_array",
    ):
        uvbeam.interp(
            az_array=az_interp_vals, za_array=za_interp_vals, polarizations=["pI"],
        )


@pytest.mark.parametrize("beam_type", ["efield", "power"])
def test_interp_longitude_branch_cut(beam_type, cst_efield_2freq, cst_power_2freq):
    if beam_type == "power":
        beam = cst_power_2freq
    else:
        beam = cst_efield_2freq

    beam.interpolation_function = "az_za_simple"
    interp_data_array, interp_basis_vector = beam.interp(
        az_array=np.deg2rad(
            np.repeat(np.array([[-1], [359], [0], [360]]), 181, axis=1).flatten()
        ),
        za_array=np.repeat(beam.axis2_array[np.newaxis, :], 4, axis=0).flatten(),
    )

    if beam_type == "power":
        npol_feed = beam.Npols
    else:
        npol_feed = beam.Nfeeds

    interp_data_array = interp_data_array.reshape(
        beam.Naxes_vec, beam.Nspws, npol_feed, beam.Nfreqs, 4, beam.Naxes2
    )

    assert np.allclose(
        interp_data_array[:, :, :, :, 0, :],
        interp_data_array[:, :, :, :, 1, :],
        rtol=beam._data_array.tols[0],
        atol=beam._data_array.tols[1],
    )

    assert np.allclose(
        interp_data_array[:, :, :, :, 2, :],
        interp_data_array[:, :, :, :, 3, :],
        rtol=beam._data_array.tols[0],
        atol=beam._data_array.tols[1],
    )


def test_interp_healpix_nside(cst_efield_2freq_cut, cst_efield_2freq_cut_healpix):
    efield_beam = cst_efield_2freq_cut

    efield_beam.interpolation_function = "az_za_simple"

    # test calling interp with healpix parameters directly gives same result
    min_res = np.min(
        np.array(
            [np.diff(efield_beam.axis1_array)[0], np.diff(efield_beam.axis2_array)[0]]
        )
    )
    nside_min_res = np.sqrt(3 / np.pi) * np.radians(60.0) / min_res
    nside = int(2 ** np.ceil(np.log2(nside_min_res)))

    new_efield_beam = cst_efield_2freq_cut_healpix
    assert new_efield_beam.nside == nside

    new_efield_beam.interpolation_function = "healpix_simple"

    # check error with cut sky
    with pytest.raises(
        ValueError, match="simple healpix interpolation requires full sky healpix maps."
    ):
        new_efield_beam.interp(
            az_array=efield_beam.axis1_array,
            za_array=efield_beam.axis2_array,
            az_za_grid=True,
            new_object=True,
        )


def test_healpix_interpolation(cst_efield_2freq):
    pytest.importorskip("astropy_healpix")
    efield_beam = cst_efield_2freq

    efield_beam.interpolation_function = "az_za_simple"

    # select every fourth point to make it smaller
    axis1_inds = np.arange(0, efield_beam.Naxes1, 4)
    axis2_inds = np.arange(0, efield_beam.Naxes2, 4)
    efield_beam.select(axis1_inds=axis1_inds, axis2_inds=axis2_inds)

    orig_efield_beam = efield_beam.copy()

    efield_beam.to_healpix()

    # check that interpolating to existing points gives the same answer
    efield_beam.interpolation_function = "healpix_simple"
    hp_obj = HEALPix(nside=efield_beam.nside)
    hpx_lon, hpx_lat = hp_obj.healpix_to_lonlat(efield_beam.pixel_array)
    za_orig_vals = (Angle(np.pi / 2, units.radian) - hpx_lat).radian
    az_orig_vals = hpx_lon.radian

    az_orig_vals = az_orig_vals.ravel(order="C")
    za_orig_vals = za_orig_vals.ravel(order="C")
    freq_orig_vals = np.array([123e6, 150e6])

    interp_data_array, interp_basis_vector = efield_beam.interp(
        az_array=az_orig_vals, za_array=za_orig_vals, freq_array=freq_orig_vals
    )
    data_array_compare = efield_beam.data_array
    interp_data_array = interp_data_array.reshape(data_array_compare.shape, order="F")
    assert np.allclose(data_array_compare, interp_data_array)

    # test that interp to every other point returns an object that matches a select
    pixel_inds = np.arange(0, efield_beam.Npixels, 2)
    select_beam = efield_beam.select(pixels=pixel_inds, inplace=False)
    interp_beam = efield_beam.interp(
        healpix_inds=efield_beam.pixel_array[pixel_inds],
        healpix_nside=efield_beam.nside,
        new_object=True,
    )
    assert select_beam.history != interp_beam.history
    interp_beam.history = select_beam.history
    assert select_beam == interp_beam

    # test interp from healpix to regular az/za grid
    new_reg_beam = efield_beam.interp(
        az_array=orig_efield_beam.axis1_array,
        za_array=orig_efield_beam.axis2_array,
        az_za_grid=True,
        new_object=True,
    )

    # this diff is pretty large. 2 rounds of interpolation is not a good thing.
    # but we can check that the rest of the object makes sense
    diff = new_reg_beam.data_array - orig_efield_beam.data_array
    diff_ratio = diff / orig_efield_beam.data_array
    assert np.all(np.abs(diff_ratio) < 4)
    # set data_array tolerances higher to test the rest of the object
    # tols are (relative, absolute)
    tols = [4, 0]
    new_reg_beam._data_array.tols = tols
    assert new_reg_beam.history != orig_efield_beam.history
    new_reg_beam.history = orig_efield_beam.history
    new_reg_beam.interpolation_function = "az_za_simple"
    assert new_reg_beam == orig_efield_beam

    # test errors with specifying healpix_inds without healpix_nside
    hp_obj = HEALPix(nside=efield_beam.nside)
    with pytest.raises(
        ValueError, match="healpix_nside must be set if healpix_inds is set"
    ):
        efield_beam.interp(
            healpix_inds=np.arange(hp_obj.npix), freq_array=freq_orig_vals
        )

    # test error setting both healpix_nside and az_array
    with pytest.raises(
        ValueError,
        match="healpix_nside and healpix_inds can not be set if az_array or "
        "za_array is set.",
    ):
        efield_beam.interp(
            healpix_nside=efield_beam.nside,
            az_array=az_orig_vals,
            za_array=za_orig_vals,
            freq_array=freq_orig_vals,
        )

    # basis_vector exception
    efield_beam.basis_vector_array[0, 1, :] = 10.0
    with pytest.raises(
        NotImplementedError,
        match="interpolation for input basis vectors that are not aligned to the "
        "native theta/phi coordinate system is not yet supported",
    ):
        efield_beam.interp(
            az_array=az_orig_vals, za_array=za_orig_vals,
        )

    # now convert to power beam
    power_beam = efield_beam.efield_to_power(inplace=False)
    del efield_beam
    interp_data_array, interp_basis_vector = power_beam.interp(
        az_array=az_orig_vals, za_array=za_orig_vals, freq_array=freq_orig_vals
    )
    data_array_compare = power_beam.data_array
    interp_data_array = interp_data_array.reshape(data_array_compare.shape, order="F")
    assert np.allclose(data_array_compare, interp_data_array)

    # test that interp to every other point returns an object that matches a select
    pixel_inds = np.arange(0, power_beam.Npixels, 2)
    select_beam = power_beam.select(pixels=pixel_inds, inplace=False)
    interp_beam = power_beam.interp(
        healpix_inds=power_beam.pixel_array[pixel_inds],
        healpix_nside=power_beam.nside,
        new_object=True,
    )
    assert select_beam.history != interp_beam.history
    interp_beam.history = select_beam.history
    assert select_beam == interp_beam

    # assert not feeding frequencies gives same answer
    interp_data_array2, interp_basis_vector2 = power_beam.interp(
        az_array=az_orig_vals, za_array=za_orig_vals
    )
    assert np.allclose(interp_data_array, interp_data_array2)

    # assert not feeding az_array gives same answer
    interp_data_array2, interp_basis_vector2 = power_beam.interp(
        az_array=az_orig_vals, za_array=za_orig_vals
    )
    assert np.allclose(interp_data_array, interp_data_array2)

    # test requesting polarization gives the same answer
    interp_data_array2, interp_basis_vector2 = power_beam.interp(
        az_array=az_orig_vals, za_array=za_orig_vals, polarizations=["yy"]
    )
    assert np.allclose(interp_data_array[:, :, 1:2], interp_data_array2[:, :, :1])

    # change complex data_array to real data_array and test again
    assert power_beam.data_array.dtype == np.complex128
    power_beam.data_array = np.abs(power_beam.data_array)
    interp_data_array, interp_basis_vector = power_beam.interp(
        az_array=az_orig_vals, za_array=za_orig_vals, freq_array=freq_orig_vals
    )
    data_array_compare = power_beam.data_array
    interp_data_array = interp_data_array.reshape(data_array_compare.shape, order="F")
    assert np.allclose(data_array_compare, interp_data_array)

    # test no inputs equals same answer
    interp_data_array2, interp_basis_vector2 = power_beam.interp()
    assert np.allclose(interp_data_array, interp_data_array2)

    # assert polarization value error
    with pytest.raises(
        ValueError,
        match="Requested polarization 1 not found in self.polarization_array",
    ):
        power_beam.interp(
            az_array=az_orig_vals, za_array=za_orig_vals, polarizations=["pI"]
        )

    # check error when pixels out of order
    power_beam.pixel_array = power_beam.pixel_array[
        np.argsort(power_beam.data_array[0, 0, 0, 0, :])
    ]
    with pytest.raises(
        ValueError,
        match="simple healpix interpolation requires healpix pixels to be in order.",
    ):
        power_beam.interp(az_array=az_orig_vals, za_array=za_orig_vals)

    # healpix coord exception
    power_beam.pixel_coordinate_system = "foo"
    with pytest.raises(ValueError, match='pixel_coordinate_system must be "healpix"'):
        power_beam.interp(az_array=az_orig_vals, za_array=za_orig_vals)


def test_to_healpix(
    cst_power_2freq_cut,
    cst_power_2freq_cut_healpix,
    cst_efield_2freq_cut,
    cst_efield_2freq_cut_healpix,
):
    power_beam = cst_power_2freq_cut
    power_beam_healpix = cst_power_2freq_cut_healpix

    sky_area_reduction_factor = (1.0 - np.cos(np.deg2rad(10))) / 2.0

    # check that history is updated appropriately
    assert power_beam_healpix.history == (
        power_beam.history
        + " Interpolated from "
        + power_beam.coordinate_system_dict["az_za"]["description"]
        + " to "
        + power_beam.coordinate_system_dict["healpix"]["description"]
        + " using pyuvdata with interpolation_function = az_za_simple."
    )

    hp_obj = HEALPix(nside=power_beam_healpix.nside)
    assert power_beam_healpix.Npixels <= hp_obj.npix * (sky_area_reduction_factor * 1.5)

    # test that Npixels make sense
    n_max_pix = power_beam.Naxes1 * power_beam.Naxes2
    assert power_beam_healpix.Npixels <= n_max_pix

    # Test error if not az_za
    power_beam.interpolation_function = "az_za_simple"
    power_beam.pixel_coordinate_system = "sin_zenith"
    with pytest.raises(ValueError, match='pixel_coordinate_system must be "az_za"'):
        power_beam.to_healpix()

    # Now check Efield interpolation
    efield_beam = cst_efield_2freq_cut
    interp_then_sq = cst_efield_2freq_cut_healpix
    interp_then_sq.efield_to_power(calc_cross_pols=False)

    # convert to power and then interpolate to compare.
    # Don't use power read from file because it has rounding errors that will
    # dominate this comparison
    efield_beam.interpolation_function = "az_za_simple"
    sq_then_interp = efield_beam.efield_to_power(calc_cross_pols=False, inplace=False)
    sq_then_interp.to_healpix()

    # square then interpolate is different from interpolate then square at a
    # higher level than normally allowed in the equality.
    # We can live with it for now, may need to improve it later
    diff = np.abs(interp_then_sq.data_array - sq_then_interp.data_array)
    assert np.max(diff) < 0.6
    reldiff = diff * 2 / np.abs(interp_then_sq.data_array + sq_then_interp.data_array)
    assert np.max(reldiff) < 0.005

    # set data_array tolerances higher to test the rest of the object
    # tols are (relative, absolute)
    tols = [0.05, 0]
    sq_then_interp._data_array.tols = tols

    # check history changes
    interp_history_add = (
        " Interpolated from "
        + power_beam.coordinate_system_dict["az_za"]["description"]
        + " to "
        + power_beam.coordinate_system_dict["healpix"]["description"]
        + " using pyuvdata with interpolation_function = az_za_simple."
    )
    sq_history_add = " Converted from efield to power using pyuvdata."
    assert (
        sq_then_interp.history
        == efield_beam.history + sq_history_add + interp_history_add
    )
    assert (
        interp_then_sq.history
        == efield_beam.history + interp_history_add + sq_history_add
    )

    # now change history on one so we can compare the rest of the object
    sq_then_interp.history = efield_beam.history + interp_history_add + sq_history_add

    assert sq_then_interp == interp_then_sq


def test_select_axis(cst_power_1freq, tmp_path):
    power_beam = cst_power_1freq

    # add optional parameters for testing purposes
    power_beam.extra_keywords = {"KEY1": "test_keyword"}
    power_beam.reference_impedance = 340.0
    power_beam.receiver_temperature_array = np.random.normal(
        50.0, 5, size=(power_beam.Nspws, power_beam.Nfreqs)
    )
    power_beam.loss_array = np.random.normal(
        50.0, 5, size=(power_beam.Nspws, power_beam.Nfreqs)
    )
    power_beam.mismatch_array = np.random.normal(
        0.0, 1.0, size=(power_beam.Nspws, power_beam.Nfreqs)
    )
    power_beam.s_parameters = np.random.normal(
        0.0, 0.3, size=(4, power_beam.Nspws, power_beam.Nfreqs)
    )

    old_history = power_beam.history

    # Test selecting on axis1
    inds1_to_keep = np.arange(14, 63)

    power_beam2 = power_beam.select(axis1_inds=inds1_to_keep, inplace=False)

    assert len(inds1_to_keep) == power_beam2.Naxes1
    for i in inds1_to_keep:
        assert power_beam.axis1_array[i] in power_beam2.axis1_array
    for i in np.unique(power_beam2.axis1_array):
        assert i in power_beam.axis1_array

    assert uvutils._check_histories(
        old_history + "  Downselected to "
        "specific parts of first image axis "
        "using pyuvdata.",
        power_beam2.history,
    )

    write_file_beamfits = str(tmp_path / "select_beam.fits")

    # test writing beamfits with only one element in axis1
    inds_to_keep = [len(inds1_to_keep) + 1]
    power_beam2 = power_beam.select(axis1_inds=inds_to_keep, inplace=False)
    power_beam2.write_beamfits(write_file_beamfits, clobber=True)

    # check for errors associated with indices not included in data
    with pytest.raises(ValueError, match="axis1_inds must be > 0 and < Naxes1"):
        power_beam2.select(axis1_inds=[power_beam.Naxes1 - 1])

    # check for warnings and errors associated with unevenly spaced image pixels
    power_beam2 = power_beam.copy()
    with uvtest.check_warnings(
        UserWarning, "Selected values along first image axis are not evenly spaced"
    ):
        power_beam2.select(axis1_inds=[0, 5, 6])
    with pytest.raises(
        ValueError,
        match="The pixels are not evenly spaced along first axis. "
        "The beam fits format does not support unevenly spaced pixels.",
    ):
        power_beam2.write_beamfits(write_file_beamfits)

    # Test selecting on axis2
    inds2_to_keep = np.arange(5, 14)

    power_beam2 = power_beam.select(axis2_inds=inds2_to_keep, inplace=False)

    assert len(inds2_to_keep) == power_beam2.Naxes2
    for i in inds2_to_keep:
        assert power_beam.axis2_array[i] in power_beam2.axis2_array
    for i in np.unique(power_beam2.axis2_array):
        assert i in power_beam.axis2_array

    assert uvutils._check_histories(
        old_history + "  Downselected to "
        "specific parts of second image axis "
        "using pyuvdata.",
        power_beam2.history,
    )

    write_file_beamfits = str(tmp_path / "select_beam.fits")

    # test writing beamfits with only one element in axis2
    inds_to_keep = [len(inds2_to_keep) + 1]
    power_beam2 = power_beam.select(axis2_inds=inds_to_keep, inplace=False)
    power_beam2.write_beamfits(write_file_beamfits, clobber=True)

    # check for errors associated with indices not included in data
    with pytest.raises(ValueError, match="axis2_inds must be > 0 and < Naxes2"):
        power_beam2.select(axis2_inds=[power_beam.Naxes2 - 1])

    # check for warnings and errors associated with unevenly spaced image pixels
    power_beam2 = power_beam.copy()
    with uvtest.check_warnings(
        UserWarning, "Selected values along second image axis are not evenly spaced"
    ):
        power_beam2.select(axis2_inds=[0, 5, 6])
    with pytest.raises(
        ValueError,
        match="The pixels are not evenly spaced along second axis. "
        "The beam fits format does not support unevenly spaced pixels.",
    ):
        power_beam2.write_beamfits(write_file_beamfits)


def test_select_frequencies(cst_power_1freq, tmp_path):
    power_beam = cst_power_1freq

    # generate more frequencies for testing by copying and adding several times
    while power_beam.Nfreqs < 8:
        new_beam = power_beam.copy()
        new_beam.freq_array = power_beam.freq_array + power_beam.Nfreqs * 1e6
        power_beam += new_beam

    # add optional parameters for testing purposes
    power_beam.extra_keywords = {"KEY1": "test_keyword"}
    power_beam.reference_impedance = 340.0
    power_beam.receiver_temperature_array = np.random.normal(
        50.0, 5, size=(power_beam.Nspws, power_beam.Nfreqs)
    )
    power_beam.loss_array = np.random.normal(
        50.0, 5, size=(power_beam.Nspws, power_beam.Nfreqs)
    )
    power_beam.mismatch_array = np.random.normal(
        0.0, 1.0, size=(power_beam.Nspws, power_beam.Nfreqs)
    )
    power_beam.s_parameters = np.random.normal(
        0.0, 0.3, size=(4, power_beam.Nspws, power_beam.Nfreqs)
    )

    old_history = power_beam.history
    freqs_to_keep = power_beam.freq_array[0, np.arange(2, 7)]

    power_beam2 = power_beam.select(frequencies=freqs_to_keep, inplace=False)

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

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

    write_file_beamfits = str(tmp_path / "select_beam.fits")
    # test writing beamfits with only one frequency

    freqs_to_keep = power_beam.freq_array[0, 5]
    power_beam2 = power_beam.select(frequencies=freqs_to_keep, inplace=False)
    power_beam2.write_beamfits(write_file_beamfits, clobber=True)

    freq_select = np.max(power_beam.freq_array) + 10
    # check for errors associated with frequencies not included in data
    with pytest.raises(
        ValueError,
        match="Frequency {f} is not present in the freq_array".format(f=freq_select),
    ):
        power_beam.select(frequencies=[freq_select])

    # check for warnings and errors associated with unevenly spaced frequencies
    power_beam2 = power_beam.copy()
    with uvtest.check_warnings(
        UserWarning, "Selected frequencies are not evenly spaced"
    ):
        power_beam2.select(frequencies=power_beam2.freq_array[0, [0, 5, 6]])
    with pytest.raises(ValueError, match="The frequencies are not evenly spaced "):
        power_beam2.write_beamfits(write_file_beamfits)

    # Test selecting on freq_chans
    chans_to_keep = np.arange(2, 7)

    power_beam2 = power_beam.select(freq_chans=chans_to_keep, inplace=False)

    assert len(chans_to_keep) == power_beam2.Nfreqs
    for chan in chans_to_keep:
        assert power_beam.freq_array[0, chan] in power_beam2.freq_array
    for f in np.unique(power_beam2.freq_array):
        assert f in power_beam.freq_array[0, chans_to_keep]

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

    # Test selecting both channels and frequencies
    freqs_to_keep = power_beam.freq_array[0, np.arange(6, 8)]  # Overlaps with chans
    all_chans_to_keep = np.arange(2, 8)

    power_beam2 = power_beam.select(
        frequencies=freqs_to_keep, freq_chans=chans_to_keep, inplace=False
    )

    assert len(all_chans_to_keep) == power_beam2.Nfreqs
    for chan in all_chans_to_keep:
        assert power_beam.freq_array[0, chan] in power_beam2.freq_array
    for f in np.unique(power_beam2.freq_array):
        assert f in power_beam.freq_array[0, all_chans_to_keep]


def test_select_feeds(cst_efield_1freq):
    efield_beam = cst_efield_1freq

    # add optional parameters for testing purposes
    efield_beam.extra_keywords = {"KEY1": "test_keyword"}
    efield_beam.reference_impedance = 340.0
    efield_beam.receiver_temperature_array = np.random.normal(
        50.0, 5, size=(efield_beam.Nspws, efield_beam.Nfreqs)
    )
    efield_beam.loss_array = np.random.normal(
        50.0, 5, size=(efield_beam.Nspws, efield_beam.Nfreqs)
    )
    efield_beam.mismatch_array = np.random.normal(
        0.0, 1.0, size=(efield_beam.Nspws, efield_beam.Nfreqs)
    )
    efield_beam.s_parameters = np.random.normal(
        0.0, 0.3, size=(4, efield_beam.Nspws, efield_beam.Nfreqs)
    )

    old_history = efield_beam.history
    feeds_to_keep = ["x"]

    efield_beam2 = efield_beam.select(feeds=feeds_to_keep, inplace=False)

    assert len(feeds_to_keep) == efield_beam2.Nfeeds
    for f in feeds_to_keep:
        assert f in efield_beam2.feed_array
    for f in np.unique(efield_beam2.feed_array):
        assert f in feeds_to_keep

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

    # check with physical orientation strings:
    efield_beam3 = efield_beam.select(feeds=["e"], inplace=False)

    assert efield_beam2 == efield_beam3

    # check for errors associated with feeds not included in data
    with pytest.raises(
        ValueError, match="Feed {f} is not present in the feed_array".format(f="p")
    ):
        efield_beam.select(feeds=["p"])

    # check for error with selecting polarizations on efield beams
    with pytest.raises(
        ValueError, match="polarizations cannot be used with efield beams"
    ):
        efield_beam.select(polarizations=[-5, -6])

    # Test check basis vectors
    efield_beam.basis_vector_array[0, 1, :, :] = 1.0
    with pytest.raises(
        ValueError, match="basis vectors must have lengths of 1 or less."
    ):
        efield_beam.check()

    efield_beam.basis_vector_array[0, 0, :, :] = np.sqrt(0.5)
    efield_beam.basis_vector_array[0, 1, :, :] = np.sqrt(0.5)
    assert efield_beam.check()

    efield_beam.basis_vector_array = None
    with pytest.raises(
        ValueError, match="Required UVParameter _basis_vector_array has not been set."
    ):
        efield_beam.check()


@pytest.mark.parametrize(
    "pols_to_keep", ([-5, -6], ["xx", "yy"], ["nn", "ee"], [[-5, -6]])
)
def test_select_polarizations(pols_to_keep, cst_power_1freq):
    power_beam = cst_power_1freq

    # generate more polarizations for testing by copying and adding several times
    while power_beam.Npols < 4:
        new_beam = power_beam.copy()
        new_beam.polarization_array = power_beam.polarization_array - power_beam.Npols
        power_beam += new_beam

    # add optional parameters for testing purposes
    power_beam.extra_keywords = {"KEY1": "test_keyword"}
    power_beam.reference_impedance = 340.0
    power_beam.receiver_temperature_array = np.random.normal(
        50.0, 5, size=(power_beam.Nspws, power_beam.Nfreqs)
    )
    power_beam.loss_array = np.random.normal(
        50.0, 5, size=(power_beam.Nspws, power_beam.Nfreqs)
    )
    power_beam.mismatch_array = np.random.normal(
        0.0, 1.0, size=(power_beam.Nspws, power_beam.Nfreqs)
    )
    power_beam.s_parameters = np.random.normal(
        0.0, 0.3, size=(4, power_beam.Nspws, power_beam.Nfreqs)
    )

    old_history = power_beam.history

    power_beam2 = power_beam.select(polarizations=pols_to_keep, inplace=False)

    if isinstance(pols_to_keep[0], list):
        pols_to_keep = pols_to_keep[0]

    assert len(pols_to_keep) == power_beam2.Npols
    for p in pols_to_keep:
        if isinstance(p, int):
            assert p in power_beam2.polarization_array
        else:
            assert (
                uvutils.polstr2num(p, x_orientation=power_beam2.x_orientation)
                in power_beam2.polarization_array
            )
    for p in np.unique(power_beam2.polarization_array):
        if isinstance(pols_to_keep[0], int):
            assert p in pols_to_keep
        else:
            assert p in uvutils.polstr2num(
                pols_to_keep, x_orientation=power_beam2.x_orientation
            )

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


def test_select_polarizations_errors(cst_power_1freq):
    power_beam = cst_power_1freq

    # generate more polarizations for testing by copying and adding several times
    while power_beam.Npols < 4:
        new_beam = power_beam.copy()
        new_beam.polarization_array = power_beam.polarization_array - power_beam.Npols
        power_beam += new_beam

    # check for errors associated with polarizations not included in data
    with pytest.raises(
        ValueError,
        match="polarization {p} is not present in the polarization_array".format(p=-3),
    ):
        power_beam.select(polarizations=[-3, -4])

    # check for warnings and errors associated with unevenly spaced polarizations
    with uvtest.check_warnings(
        UserWarning, "Selected polarizations are not evenly spaced"
    ):
        power_beam.select(polarizations=power_beam.polarization_array[[0, 1, 3]])
    write_file_beamfits = os.path.join(DATA_PATH, "test/select_beam.fits")
    with pytest.raises(
        ValueError, match="The polarization values are not evenly spaced "
    ):
        power_beam.write_beamfits(write_file_beamfits)

    # check for error with selecting on feeds on power beams
    with pytest.raises(ValueError, match="feeds cannot be used with power beams"):
        power_beam.select(feeds=["x"])


def test_select(cst_power_1freq, cst_efield_1freq):
    power_beam = cst_power_1freq

    # generate more frequencies for testing by copying and adding
    new_beam = power_beam.copy()
    new_beam.freq_array = power_beam.freq_array + power_beam.Nfreqs * 1e6
    power_beam += new_beam

    # add optional parameters for testing purposes
    power_beam.extra_keywords = {"KEY1": "test_keyword"}
    power_beam.reference_impedance = 340.0
    power_beam.receiver_temperature_array = np.random.normal(
        50.0, 5, size=(power_beam.Nspws, power_beam.Nfreqs)
    )
    power_beam.loss_array = np.random.normal(
        50.0, 5, size=(power_beam.Nspws, power_beam.Nfreqs)
    )
    power_beam.mismatch_array = np.random.normal(
        0.0, 1.0, size=(power_beam.Nspws, power_beam.Nfreqs)
    )
    power_beam.s_parameters = np.random.normal(
        0.0, 0.3, size=(4, power_beam.Nspws, power_beam.Nfreqs)
    )

    # now test selecting along all axes at once
    old_history = power_beam.history

    inds1_to_keep = np.arange(14, 63)
    inds2_to_keep = np.arange(5, 14)
    freqs_to_keep = [power_beam.freq_array[0, 0]]
    pols_to_keep = [-5]

    power_beam2 = power_beam.select(
        axis1_inds=inds1_to_keep,
        axis2_inds=inds2_to_keep,
        frequencies=freqs_to_keep,
        polarizations=pols_to_keep,
        inplace=False,
    )

    assert len(inds1_to_keep) == power_beam2.Naxes1
    for i in inds1_to_keep:
        assert power_beam.axis1_array[i] in power_beam2.axis1_array
    for i in np.unique(power_beam2.axis1_array):
        assert i in power_beam.axis1_array

    assert len(inds2_to_keep) == power_beam2.Naxes2
    for i in inds2_to_keep:
        assert power_beam.axis2_array[i] in power_beam2.axis2_array
    for i in np.unique(power_beam2.axis2_array):
        assert i in power_beam.axis2_array

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

    assert len(pols_to_keep) == power_beam2.Npols
    for p in pols_to_keep:
        assert p in power_beam2.polarization_array
    for p in np.unique(power_beam2.polarization_array):
        assert p in pols_to_keep

    assert uvutils._check_histories(
        old_history + "  Downselected to "
        "specific parts of first image axis, "
        "parts of second image axis, "
        "frequencies, polarizations using pyuvdata.",
        power_beam2.history,
    )

    # repeat for efield beam
    efield_beam = cst_efield_1freq

    # generate more frequencies for testing by copying and adding
    new_beam = efield_beam.copy()
    new_beam.freq_array = efield_beam.freq_array + efield_beam.Nfreqs * 1e6
    efield_beam += new_beam

    # add optional parameters for testing purposes
    efield_beam.extra_keywords = {"KEY1": "test_keyword"}
    efield_beam.reference_impedance = 340.0
    efield_beam.receiver_temperature_array = np.random.normal(
        50.0, 5, size=(efield_beam.Nspws, efield_beam.Nfreqs)
    )
    efield_beam.loss_array = np.random.normal(
        50.0, 5, size=(efield_beam.Nspws, efield_beam.Nfreqs)
    )
    efield_beam.mismatch_array = np.random.normal(
        0.0, 1.0, size=(efield_beam.Nspws, efield_beam.Nfreqs)
    )
    efield_beam.s_parameters = np.random.normal(
        0.0, 0.3, size=(4, efield_beam.Nspws, efield_beam.Nfreqs)
    )

    feeds_to_keep = ["x"]

    efield_beam2 = efield_beam.select(
        axis1_inds=inds1_to_keep,
        axis2_inds=inds2_to_keep,
        frequencies=freqs_to_keep,
        feeds=feeds_to_keep,
        inplace=False,
    )

    assert len(inds1_to_keep) == efield_beam2.Naxes1
    for i in inds1_to_keep:
        assert efield_beam.axis1_array[i] in efield_beam2.axis1_array
    for i in np.unique(efield_beam2.axis1_array):
        assert i in efield_beam.axis1_array

    assert len(inds2_to_keep) == efield_beam2.Naxes2
    for i in inds2_to_keep:
        assert efield_beam.axis2_array[i] in efield_beam2.axis2_array
    for i in np.unique(efield_beam2.axis2_array):
        assert i in efield_beam.axis2_array

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

    assert len(feeds_to_keep) == efield_beam2.Nfeeds
    for f in feeds_to_keep:
        assert f in efield_beam2.feed_array
    for f in np.unique(efield_beam2.feed_array):
        assert f in feeds_to_keep

    assert uvutils._check_histories(
        old_history + "  Downselected to "
        "specific parts of first image axis, "
        "parts of second image axis, "
        "frequencies, feeds using pyuvdata.",
        efield_beam2.history,
    )


def test_add(cst_power_1freq, cst_efield_1freq):
    power_beam = cst_power_1freq

    # generate more frequencies for testing by copying and adding
    new_beam = power_beam.copy()
    new_beam.freq_array = power_beam.freq_array + power_beam.Nfreqs * 1e6
    power_beam += new_beam

    # add optional parameters for testing purposes
    power_beam.extra_keywords = {"KEY1": "test_keyword"}
    power_beam.reference_impedance = 340.0
    power_beam.receiver_temperature_array = np.random.normal(
        50.0, 5, size=(power_beam.Nspws, power_beam.Nfreqs)
    )
    power_beam.loss_array = np.random.normal(
        50.0, 5, size=(power_beam.Nspws, power_beam.Nfreqs)
    )
    power_beam.mismatch_array = np.random.normal(
        0.0, 1.0, size=(power_beam.Nspws, power_beam.Nfreqs)
    )
    power_beam.s_parameters = np.random.normal(
        0.0, 0.3, size=(4, power_beam.Nspws, power_beam.Nfreqs)
    )

    # Add along first image axis
    beam1 = power_beam.select(axis1_inds=np.arange(0, 180), inplace=False)
    beam2 = power_beam.select(axis1_inds=np.arange(180, 360), inplace=False)
    beam1 += beam2
    # Check history is correct, before replacing and doing a full object check
    assert uvutils._check_histories(
        power_beam.history + "  Downselected to specific parts of "
        "first image axis using pyuvdata. "
        "Combined data along first image axis "
        "using pyuvdata.",
        beam1.history,
    )
    beam1.history = power_beam.history
    assert beam1 == power_beam

    # Out of order - axis1
    beam1 = power_beam.select(axis1_inds=np.arange(180, 360), inplace=False)
    beam2 = power_beam.select(axis1_inds=np.arange(0, 180), inplace=False)
    beam1 += beam2
    beam1.history = power_beam.history
    assert beam1 == power_beam

    # Add along second image axis
    beam1 = power_beam.select(axis2_inds=np.arange(0, 90), inplace=False)
    beam2 = power_beam.select(axis2_inds=np.arange(90, 181), inplace=False)
    beam1 += beam2
    # Check history is correct, before replacing and doing a full object check
    assert uvutils._check_histories(
        power_beam.history + "  Downselected to specific parts of "
        "second image axis using pyuvdata. "
        "Combined data along second image axis "
        "using pyuvdata.",
        beam1.history,
    )
    beam1.history = power_beam.history
    assert beam1 == power_beam

    # Out of order - axis2
    beam1 = power_beam.select(axis2_inds=np.arange(90, 181), inplace=False)
    beam2 = power_beam.select(axis2_inds=np.arange(0, 90), inplace=False)
    beam1 += beam2
    beam1.history = power_beam.history
    assert beam1 == power_beam

    # Add frequencies
    beam1 = power_beam.select(freq_chans=0, inplace=False)
    beam2 = power_beam.select(freq_chans=1, inplace=False)
    beam1 += beam2
    # Check history is correct, before replacing and doing a full object check
    assert uvutils._check_histories(
        power_beam.history + "  Downselected to specific frequencies "
        "using pyuvdata. Combined data along "
        "frequency axis using pyuvdata.",
        beam1.history,
    )
    beam1.history = power_beam.history
    assert beam1 == power_beam

    # Out of order - freqs
    beam1 = power_beam.select(freq_chans=1, inplace=False)
    beam2 = power_beam.select(freq_chans=0, inplace=False)
    beam1 += beam2
    beam1.history = power_beam.history
    assert beam1 == power_beam

    # Add polarizations
    beam1 = power_beam.select(polarizations=-5, inplace=False)
    beam2 = power_beam.select(polarizations=-6, inplace=False)
    beam1 += beam2
    assert uvutils._check_histories(
        power_beam.history + "  Downselected to specific polarizations "
        "using pyuvdata. Combined data along "
        "polarization axis using pyuvdata.",
        beam1.history,
    )
    beam1.history = power_beam.history
    assert beam1 == power_beam

    # Out of order - pols
    beam1 = power_beam.select(polarizations=-6, inplace=False)
    beam2 = power_beam.select(polarizations=-5, inplace=False)
    beam1 += beam2
    beam1.history = power_beam.history
    assert beam1 == power_beam

    # Add feeds
    efield_beam = cst_efield_1freq

    # generate more frequencies for testing by copying and adding
    new_beam = efield_beam.copy()
    new_beam.freq_array = efield_beam.freq_array + efield_beam.Nfreqs * 1e6
    efield_beam += new_beam

    # add optional parameters for testing purposes
    efield_beam.extra_keywords = {"KEY1": "test_keyword"}
    efield_beam.reference_impedance = 340.0
    efield_beam.receiver_temperature_array = np.random.normal(
        50.0, 5, size=(efield_beam.Nspws, efield_beam.Nfreqs)
    )
    efield_beam.loss_array = np.random.normal(
        50.0, 5, size=(efield_beam.Nspws, efield_beam.Nfreqs)
    )
    efield_beam.mismatch_array = np.random.normal(
        0.0, 1.0, size=(efield_beam.Nspws, efield_beam.Nfreqs)
    )
    efield_beam.s_parameters = np.random.normal(
        0.0, 0.3, size=(4, efield_beam.Nspws, efield_beam.Nfreqs)
    )

    beam1 = efield_beam.select(feeds=efield_beam.feed_array[0], inplace=False)
    beam2 = efield_beam.select(feeds=efield_beam.feed_array[1], inplace=False)
    beam1 += beam2
    assert uvutils._check_histories(
        efield_beam.history + "  Downselected to specific feeds "
        "using pyuvdata. Combined data along "
        "feed axis using pyuvdata.",
        beam1.history,
    )
    beam1.history = efield_beam.history
    assert beam1 == efield_beam

    # Out of order - feeds
    beam1 = efield_beam.select(feeds=efield_beam.feed_array[1], inplace=False)
    beam2 = efield_beam.select(feeds=efield_beam.feed_array[0], inplace=False)
    beam1 += beam2
    beam1.history = efield_beam.history
    assert beam1, efield_beam

    # Add multiple axes
    beam_ref = power_beam.copy()
    beam1 = power_beam.select(
        axis1_inds=np.arange(0, power_beam.Naxes1 // 2),
        polarizations=power_beam.polarization_array[0],
        inplace=False,
    )
    beam2 = power_beam.select(
        axis1_inds=np.arange(power_beam.Naxes1 // 2, power_beam.Naxes1),
        polarizations=power_beam.polarization_array[1],
        inplace=False,
    )
    beam1 += beam2
    assert uvutils._check_histories(
        power_beam.history + "  Downselected to specific parts of "
        "first image axis, polarizations using "
        "pyuvdata. Combined data along first "
        "image, polarization axis using pyuvdata.",
        beam1.history,
    )
    # Zero out missing data in reference object
    beam_ref.data_array[:, :, 0, :, :, power_beam.Naxes1 // 2 :] = 0.0
    beam_ref.data_array[:, :, 1, :, :, : power_beam.Naxes1 // 2] = 0.0
    beam1.history = power_beam.history
    assert beam1 == beam_ref

    # Another combo with efield
    beam_ref = efield_beam.copy()
    beam1 = efield_beam.select(
        axis1_inds=np.arange(0, efield_beam.Naxes1 // 2),
        axis2_inds=np.arange(0, efield_beam.Naxes2 // 2),
        inplace=False,
    )
    beam2 = efield_beam.select(
        axis1_inds=np.arange(efield_beam.Naxes1 // 2, efield_beam.Naxes1),
        axis2_inds=np.arange(efield_beam.Naxes2 // 2, efield_beam.Naxes2),
        inplace=False,
    )
    beam1 += beam2
    assert uvutils._check_histories(
        efield_beam.history + "  Downselected to specific parts of "
        "first image axis, parts of second "
        "image axis using pyuvdata. Combined "
        "data along first image, second image "
        "axis using pyuvdata.",
        beam1.history,
    )

    # Zero out missing data in reference object
    beam_ref.data_array[
        :, :, :, :, : efield_beam.Naxes2 // 2, efield_beam.Naxes1 // 2 :
    ] = 0.0
    beam_ref.data_array[
        :, :, :, :, efield_beam.Naxes2 // 2 :, : efield_beam.Naxes1 // 2
    ] = 0.0

    beam_ref.basis_vector_array[
        :, :, : efield_beam.Naxes2 // 2, efield_beam.Naxes1 // 2 :
    ] = 0.0
    beam_ref.basis_vector_array[
        :, :, efield_beam.Naxes2 // 2 :, : efield_beam.Naxes1 // 2
    ] = 0.0
    beam1.history = efield_beam.history
    assert beam1, beam_ref

    # Check warnings
    # generate more frequencies for testing by copying and adding several times
    while power_beam.Nfreqs < 8:
        new_beam = power_beam.copy()
        new_beam.freq_array = power_beam.freq_array + power_beam.Nfreqs * 1e6
        power_beam += new_beam

    beam1 = power_beam.select(freq_chans=np.arange(0, 4), inplace=False)
    beam2 = power_beam.select(freq_chans=np.arange(5, 8), inplace=False)
    with uvtest.check_warnings(
        UserWarning, "Combined frequencies are not evenly spaced"
    ):
        beam1.__add__(beam2)

    # generate more polarizations for testing by copying and adding several times
    while power_beam.Npols < 4:
        new_beam = power_beam.copy()
        new_beam.polarization_array = power_beam.polarization_array - power_beam.Npols
        power_beam += new_beam

    power_beam.receiver_temperature_array = np.ones((1, 8))
    beam1 = power_beam.select(
        polarizations=power_beam.polarization_array[0:2], inplace=False
    )
    beam2 = power_beam.select(
        polarizations=power_beam.polarization_array[3], inplace=False
    )
    with uvtest.check_warnings(
        UserWarning, "Combined polarizations are not evenly spaced"
    ):
        beam1.__iadd__(beam2)

    beam1 = power_beam.select(
        polarizations=power_beam.polarization_array[0:2], inplace=False
    )
    beam2 = power_beam.select(
        polarizations=power_beam.polarization_array[2:3], inplace=False
    )
    beam2.receiver_temperature_array = None
    assert beam1.receiver_temperature_array is not None
    with uvtest.check_warnings(
        UserWarning,
        "Only one of the UVBeam objects being combined has optional parameter",
    ):
        beam1 += beam2

    assert beam1.receiver_temperature_array is None

    # Combining histories
    beam1 = power_beam.select(
        polarizations=power_beam.polarization_array[0:2], inplace=False
    )
    beam2 = power_beam.select(
        polarizations=power_beam.polarization_array[2:4], inplace=False
    )
    beam2.history += " testing the history. Read/written with pyuvdata"
    new_beam = beam1 + beam2
    assert uvutils._check_histories(
        power_beam.history + "  Downselected to specific polarizations using pyuvdata. "
        "Combined data along polarization axis using pyuvdata. Unique part of next "
        "object history follows.  testing the history.",
        new_beam.history,
    )
    new_beam.history = power_beam.history
    assert new_beam == power_beam

    new_beam = beam1.__add__(beam2, verbose_history=True)
    assert uvutils._check_histories(
        power_beam.history + "  Downselected to specific polarizations using pyuvdata. "
        "Combined data along polarization axis using pyuvdata. Next object history "
        "follows. " + beam2.history,
        new_beam.history,
    )

    # ------------------------
    # Test failure modes of add function

    # Wrong class
    beam1 = power_beam.copy()
    with pytest.raises(ValueError, match="Only UVBeam "):
        beam1.__iadd__(np.zeros(5))

    params_to_change = {
        "beam_type": "efield",
        "data_normalization": "solid_angle",
        "telescope_name": "foo",
        "feed_name": "foo",
        "feed_version": "v12",
        "model_name": "foo",
        "model_version": "v12",
        "pixel_coordinate_system": "sin_zenith",
        "Naxes_vec": 3,
        "nside": 16,
        "ordering": "nested",
    }

    beam1 = power_beam.select(freq_chans=0, inplace=False)
    beam2 = power_beam.select(freq_chans=1, inplace=False)
    for param, value in params_to_change.items():
        beam1_copy = beam1.copy()
        if param == "beam_type":
            beam2_copy = efield_beam.select(freq_chans=1, inplace=False)
        elif param == "Naxes_vec":
            beam2_copy = beam2.copy()
            beam2_copy.Naxes_vec = value
            beam2_copy.data_array = np.concatenate(
                (beam2_copy.data_array, beam2_copy.data_array, beam2_copy.data_array)
            )
        else:
            beam2_copy = beam2.copy()
            setattr(beam2_copy, param, value)
        with pytest.raises(
            ValueError,
            match=f"UVParameter {param} does not match. Cannot combine objects.",
        ):
            beam1_copy.__iadd__(beam2_copy)
    del beam1_copy
    del beam2_copy

    # Overlapping data
    beam2 = power_beam.copy()
    with pytest.raises(
        ValueError, match="These objects have overlapping data and cannot be combined."
    ):
        beam1.__iadd__(beam2)


@pytest.mark.parametrize("beam_type", ["efield", "power"])
def test_select_healpix_pixels(
    beam_type, cst_power_1freq_cut_healpix, cst_efield_1freq_cut_healpix, tmp_path
):
    if beam_type == "power":
        beam_healpix = cst_power_1freq_cut_healpix
    else:
        beam_healpix = cst_efield_1freq_cut_healpix

    # add optional parameters for testing purposes
    beam_healpix.extra_keywords = {"KEY1": "test_keyword"}
    beam_healpix.reference_impedance = 340.0
    beam_healpix.receiver_temperature_array = np.random.normal(
        50.0, 5, size=(beam_healpix.Nspws, beam_healpix.Nfreqs)
    )
    beam_healpix.loss_array = np.random.normal(
        50.0, 5, size=(beam_healpix.Nspws, beam_healpix.Nfreqs)
    )
    beam_healpix.mismatch_array = np.random.normal(
        0.0, 1.0, size=(beam_healpix.Nspws, beam_healpix.Nfreqs)
    )
    beam_healpix.s_parameters = np.random.normal(
        0.0, 0.3, size=(4, beam_healpix.Nspws, beam_healpix.Nfreqs)
    )

    old_history = beam_healpix.history
    pixels_to_keep = np.arange(31, 184)

    beam_healpix2 = beam_healpix.select(pixels=pixels_to_keep, inplace=False)

    assert len(pixels_to_keep) == beam_healpix2.Npixels
    for pi in pixels_to_keep:
        assert pi in beam_healpix2.pixel_array
    for pi in np.unique(beam_healpix2.pixel_array):
        assert pi in pixels_to_keep

    assert uvutils._check_histories(
        old_history + "  Downselected to specific healpix pixels using pyuvdata.",
        beam_healpix2.history,
    )

    write_file_beamfits = str(tmp_path / "select_beam.fits")

    # test writing beamfits with only one pixel
    pixels_to_keep = [43]
    beam_healpix2 = beam_healpix.select(pixels=pixels_to_keep, inplace=False)
    beam_healpix2.write_beamfits(write_file_beamfits, clobber=True)

    # check for errors associated with pixels not included in data
    pixel_select = 12 * beam_healpix.nside ** 2 + 10
    with pytest.raises(
        ValueError,
        match="Pixel {p} is not present in the pixel_array".format(p=pixel_select),
    ):
        beam_healpix.select(pixels=[pixel_select])

    # test writing beamfits with non-contiguous pixels
    pixels_to_keep = np.arange(2, 150, 4)

    beam_healpix2 = beam_healpix.select(pixels=pixels_to_keep, inplace=False)
    beam_healpix2.write_beamfits(write_file_beamfits, clobber=True)

    # -----------------
    # check for errors selecting axis1_inds on healpix beams
    inds1_to_keep = np.arange(14, 63)
    with pytest.raises(
        ValueError, match="axis1_inds cannot be used with healpix coordinate system"
    ):
        beam_healpix.select(axis1_inds=inds1_to_keep)

    # check for errors selecting axis2_inds on healpix beams
    inds2_to_keep = np.arange(5, 14)
    with pytest.raises(
        ValueError, match="axis2_inds cannot be used with healpix coordinate system"
    ):
        beam_healpix.select(axis2_inds=inds2_to_keep)

    # ------------------------
    # test selecting along all axes at once for healpix beams
    freqs_to_keep = [beam_healpix.freq_array[0, 0]]

    if beam_type == "efield":
        feeds_to_keep = ["x"]
        pols_to_keep = None
    else:
        pols_to_keep = [-5]
        feeds_to_keep = None

    beam_healpix2 = beam_healpix.select(
        pixels=pixels_to_keep,
        frequencies=freqs_to_keep,
        polarizations=pols_to_keep,
        feeds=feeds_to_keep,
        inplace=False,
    )

    assert len(pixels_to_keep) == beam_healpix2.Npixels
    for pi in pixels_to_keep:
        assert pi in beam_healpix2.pixel_array
    for pi in np.unique(beam_healpix2.pixel_array):
        assert pi in pixels_to_keep

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

    if beam_type == "efield":
        assert len(feeds_to_keep) == beam_healpix2.Nfeeds
        for f in feeds_to_keep:
            assert f in beam_healpix2.feed_array
        for f in np.unique(beam_healpix2.feed_array):
            assert f in feeds_to_keep
    else:
        assert len(pols_to_keep) == beam_healpix2.Npols
        for p in pols_to_keep:
            assert p in beam_healpix2.polarization_array
        for p in np.unique(beam_healpix2.polarization_array):
            assert p in pols_to_keep

    if beam_type == "efield":
        history_add = "feeds"
    else:
        history_add = "polarizations"

    assert uvutils._check_histories(
        old_history + "  Downselected to "
        "specific healpix pixels, frequencies, "
        f"{history_add} using pyuvdata.",
        beam_healpix2.history,
    )


@pytest.mark.parametrize("beam_type", ["efield", "power"])
def test_select_healpix_pixels_error(
    beam_type, cst_power_2freq_cut, cst_efield_2freq_cut
):
    if beam_type == "power":
        beam = cst_power_2freq_cut
    else:
        beam = cst_efield_2freq_cut
    # check for errors selecting pixels on non-healpix beams
    with pytest.raises(
        ValueError, match="pixels can only be used with healpix coordinate system"
    ):
        beam.select(pixels=np.arange(31, 184))


@pytest.mark.parametrize("beam_type", ["efield", "power"])
def test_add_healpix(
    beam_type, cst_power_2freq_cut_healpix, cst_efield_2freq_cut_healpix
):
    if beam_type == "power":
        beam_healpix = cst_power_2freq_cut_healpix
    else:
        beam_healpix = cst_efield_2freq_cut_healpix

    # add optional parameters for testing purposes
    beam_healpix.extra_keywords = {"KEY1": "test_keyword"}
    beam_healpix.reference_impedance = 340.0
    beam_healpix.receiver_temperature_array = np.random.normal(
        50.0, 5, size=(beam_healpix.Nspws, beam_healpix.Nfreqs)
    )
    beam_healpix.loss_array = np.random.normal(
        50.0, 5, size=(beam_healpix.Nspws, beam_healpix.Nfreqs)
    )
    beam_healpix.mismatch_array = np.random.normal(
        0.0, 1.0, size=(beam_healpix.Nspws, beam_healpix.Nfreqs)
    )
    beam_healpix.s_parameters = np.random.normal(
        0.0, 0.3, size=(4, beam_healpix.Nspws, beam_healpix.Nfreqs)
    )

    # Test adding a different combo with healpix
    beam_ref = beam_healpix.copy()
    beam1 = beam_healpix.select(
        pixels=beam_healpix.pixel_array[0 : beam_healpix.Npixels // 2],
        freq_chans=0,
        inplace=False,
    )
    beam2 = beam_healpix.select(
        pixels=beam_healpix.pixel_array[beam_healpix.Npixels // 2 :],
        freq_chans=1,
        inplace=False,
    )
    beam1 += beam2
    assert uvutils._check_histories(
        beam_healpix.history + "  Downselected to specific healpix "
        "pixels, frequencies using pyuvdata. "
        "Combined data along healpix pixel, "
        "frequency axis using pyuvdata.",
        beam1.history,
    )
    # Zero out missing data in reference object
    beam_ref.data_array[:, :, :, 0, beam_healpix.Npixels // 2 :] = 0.0
    beam_ref.data_array[:, :, :, 1, : beam_healpix.Npixels // 2] = 0.0
    beam1.history = beam_healpix.history
    assert beam1 == beam_ref

    if beam_type == "efield":
        # Test adding another combo with efield
        beam_ref = beam_healpix.copy()
        beam1 = beam_healpix.select(
            freq_chans=0, feeds=beam_healpix.feed_array[0], inplace=False
        )
        beam2 = beam_healpix.select(
            freq_chans=1, feeds=beam_healpix.feed_array[1], inplace=False
        )
        beam1 += beam2
        assert uvutils._check_histories(
            beam_healpix.history + "  Downselected to specific frequencies, "
            "feeds using pyuvdata. Combined data "
            "along frequency, feed axis using pyuvdata.",
            beam1.history,
        )
        # Zero out missing data in reference object
        beam_ref.data_array[:, :, 1, 0, :] = 0.0
        beam_ref.data_array[:, :, 0, 1, :] = 0.0
        beam1.history = beam_healpix.history
        assert beam1 == beam_ref

    # Add without inplace
    beam1 = beam_healpix.select(
        pixels=beam_healpix.pixel_array[0 : beam_healpix.Npixels // 2], inplace=False
    )
    beam2 = beam_healpix.select(
        pixels=beam_healpix.pixel_array[beam_healpix.Npixels // 2 :], inplace=False
    )
    beam1 = beam1 + beam2
    assert uvutils._check_histories(
        beam_healpix.history + "  Downselected to specific healpix pixels "
        "using pyuvdata. Combined data "
        "along healpix pixel axis using pyuvdata.",
        beam1.history,
    )
    beam1.history = beam_healpix.history
    assert beam1 == beam_healpix

    # ---------------
    # Test error: adding overlapping data with healpix
    beam1 = beam_healpix.copy()
    beam2 = beam_healpix.copy()
    with pytest.raises(
        ValueError, match="These objects have overlapping data and cannot be combined."
    ):
        beam1.__iadd__(beam2)


def test_beam_area_healpix(cst_power_1freq_cut_healpix, cst_efield_1freq_cut_healpix):
    power_beam_healpix = cst_power_1freq_cut_healpix

    # Test beam area methods
    # Check that non-peak normalizations error
    with pytest.raises(ValueError, match="beam must be peak normalized"):
        power_beam_healpix.get_beam_area()
    with pytest.raises(ValueError, match="beam must be peak normalized"):
        power_beam_healpix.get_beam_sq_area()

    healpix_norm = power_beam_healpix.copy()
    healpix_norm.data_normalization = "solid_angle"
    with pytest.raises(ValueError, match="beam must be peak normalized"):
        healpix_norm.get_beam_area()
    with pytest.raises(ValueError, match="beam must be peak normalized"):
        healpix_norm.get_beam_sq_area()

    # change it back to 'physical'
    healpix_norm.data_normalization = "physical"
    # change it to peak for rest of checks
    healpix_norm.peak_normalize()

    # Check sizes of output
    numfreqs = healpix_norm.freq_array.shape[-1]
    beam_int = healpix_norm.get_beam_area(pol="xx")
    beam_sq_int = healpix_norm.get_beam_sq_area(pol="xx")
    assert beam_int.shape[0] == numfreqs
    assert beam_sq_int.shape[0] == numfreqs

    # Check for the case of a uniform beam over the whole sky
    hp_obj = HEALPix(nside=healpix_norm.nside)
    d_omega = hp_obj.pixel_area.to("steradian").value
    npix = healpix_norm.Npixels
    healpix_norm.data_array = np.ones_like(healpix_norm.data_array)
    assert np.allclose(
        np.sum(healpix_norm.get_beam_area(pol="xx")), numfreqs * npix * d_omega
    )
    healpix_norm.data_array = 2.0 * np.ones_like(healpix_norm.data_array)
    assert np.allclose(
        np.sum(healpix_norm.get_beam_sq_area(pol="xx")), numfreqs * 4.0 * npix * d_omega
    )

    # check XX and YY beam areas work and match to within 5 sigfigs
    xx_area = healpix_norm.get_beam_area("XX")
    xx_area = healpix_norm.get_beam_area("xx")
    assert np.allclose(xx_area, xx_area)
    yy_area = healpix_norm.get_beam_area("YY")
    assert np.allclose(yy_area / xx_area, np.ones(numfreqs))
    # nt.assert_almost_equal(yy_area / xx_area, 1.0, places=5)
    xx_area = healpix_norm.get_beam_sq_area("XX")
    yy_area = healpix_norm.get_beam_sq_area("YY")
    assert np.allclose(yy_area / xx_area, np.ones(numfreqs))
    # nt.assert_almost_equal(yy_area / xx_area, 1.0, places=5)

    # Check that if pseudo-Stokes I (pI) is in the beam polarization_array it
    # just uses it
    healpix_norm.polarization_array = [1, 2]

    # Check error if desired pol is allowed but isn't in the polarization_array
    with pytest.raises(
        ValueError, match="Do not have the right polarization information"
    ):
        healpix_norm.get_beam_area(pol="xx")
    with pytest.raises(
        ValueError, match="Do not have the right polarization information"
    ):
        healpix_norm.get_beam_sq_area(pol="xx")

    # Check polarization error
    healpix_norm.polarization_array = [9, 18, 27, -4]
    with pytest.raises(
        ValueError, match="Do not have the right polarization information"
    ):
        healpix_norm.get_beam_area(pol="xx")
    with pytest.raises(
        ValueError, match="Do not have the right polarization information"
    ):
        healpix_norm.get_beam_sq_area(pol="xx")

    efield_beam = cst_efield_1freq_cut_healpix
    healpix_norm_fullpol = efield_beam.efield_to_power(inplace=False)
    healpix_norm_fullpol.peak_normalize()
    xx_area = healpix_norm_fullpol.get_beam_sq_area("XX")
    yy_area = healpix_norm_fullpol.get_beam_sq_area("YY")
    XY_area = healpix_norm_fullpol.get_beam_sq_area("XY")
    YX_area = healpix_norm_fullpol.get_beam_sq_area("YX")
    # check if XY beam area is equal to beam YX beam area
    assert np.allclose(XY_area, YX_area)
    # check if XY/YX beam area is less than XX/YY beam area
    assert np.all(np.less(XY_area, xx_area))
    assert np.all(np.less(XY_area, yy_area))
    assert np.all(np.less(YX_area, xx_area))
    assert np.all(np.less(YX_area, yy_area))

    # Check if power is scalar
    healpix_vec_norm = efield_beam.efield_to_power(
        keep_basis_vector=True, calc_cross_pols=False, inplace=False
    )
    healpix_vec_norm.peak_normalize()
    with pytest.raises(ValueError, match="Expect scalar for power beam, found vector"):
        healpix_vec_norm.get_beam_area()
    with pytest.raises(ValueError, match="Expect scalar for power beam, found vector"):
        healpix_vec_norm.get_beam_sq_area()

    # Check only power beams accepted
    with pytest.raises(ValueError, match="beam_type must be power"):
        efield_beam.get_beam_area()
    with pytest.raises(ValueError, match="beam_type must be power"):
        efield_beam.get_beam_sq_area()

    # check pseudo-Stokes parameters
    efield_beam = cst_efield_1freq_cut_healpix

    efield_beam.efield_to_pstokes()
    efield_beam.peak_normalize()
    pI_area = efield_beam.get_beam_sq_area("pI")
    pQ_area = efield_beam.get_beam_sq_area("pQ")
    pU_area = efield_beam.get_beam_sq_area("pU")
    pV_area = efield_beam.get_beam_sq_area("pV")
    assert np.all(np.less(pQ_area, pI_area))
    assert np.all(np.less(pU_area, pI_area))
    assert np.all(np.less(pV_area, pI_area))

    # check backwards compatability with pstokes nomenclature and int polnum
    I_area = efield_beam.get_beam_area("I")
    pI_area = efield_beam.get_beam_area("pI")
    area1 = efield_beam.get_beam_area(1)
    assert np.allclose(I_area, pI_area)
    assert np.allclose(I_area, area1)

    # check efield beam type is accepted for pseudo-stokes and power for
    # linear polarizations
    with pytest.raises(ValueError, match="Expect scalar for power beam, found vector"):
        healpix_vec_norm.get_beam_sq_area("pI")
    with pytest.raises(
        ValueError, match="Do not have the right polarization information"
    ):
        efield_beam.get_beam_sq_area("xx")


def test_get_beam_function_errors(cst_power_1freq_cut):
    power_beam = cst_power_1freq_cut.copy()

    with pytest.raises(AssertionError, match="pixel_coordinate_system must be healpix"):
        power_beam._get_beam("xx")

    # Check only healpix accepted (HEALPix checks are in test_healpix)
    # change data_normalization to peak for rest of checks
    power_beam.peak_normalize()
    with pytest.raises(ValueError, match="Currently only healpix format supported"):
        power_beam.get_beam_area()
    with pytest.raises(ValueError, match="Currently only healpix format supported"):
        power_beam.get_beam_sq_area()


def test_get_beam_functions(cst_power_1freq_cut_healpix):
    healpix_power_beam = cst_power_1freq_cut_healpix
    healpix_power_beam.peak_normalize()
    healpix_power_beam._get_beam("xx")
    with pytest.raises(
        ValueError, match="Do not have the right polarization information"
    ):
        healpix_power_beam._get_beam(4)


def test_generic_read_cst():
    uvb = UVBeam()
    uvb.read(
        cst_files,
        beam_type="power",
        frequency=np.array([150e6, 123e6]),
        feed_pol="y",
        telescope_name="TEST",
        feed_name="bob",
        feed_version="0.1",
        model_name="E-field pattern - Rigging height 4.9m",
        model_version="1.0",
        run_check=False,
    )
    assert uvb.check()


@pytest.mark.parametrize(
    "filename", [cst_yaml_file, mwa_beam_file, casa_beamfits],
)
def test_generic_read(filename):
    """Test generic read can infer the file types correctly."""
    uvb = UVBeam()
    # going to check in a second anyway, no need to double check.
    uvb.read(filename, run_check=False)
    # hera casa beam is missing some parameters but we just want to check
    # that reading is going okay
    if filename == casa_beamfits:
        # fill in missing parameters
        uvb.data_normalization = "peak"
        uvb.feed_name = "casa_ideal"
        uvb.feed_version = "v0"
        uvb.model_name = "casa_airy"
        uvb.model_version = "v0"

        # this file is actually in an orthoslant projection RA/DEC at zenith at a
        # particular time.
        # For now pretend it's in a zenith orthoslant projection
        uvb.pixel_coordinate_system = "orthoslant_zenith"
    assert uvb.check()


def test_generic_read_bad_filetype():
    uvb = UVBeam()
    with pytest.raises(ValueError, match="File type could not be determined"):
        uvb.read("foo")


def test_generic_read_multi(tmp_path):
    uvb = UVBeam()
    uvb.read(mwa_beam_file, pixels_per_deg=1, freq_range=[100e6, 200e6])

    uvb1 = uvb.select(frequencies=uvb.freq_array[0, ::2], inplace=False)
    uvb2 = uvb.select(frequencies=uvb.freq_array[0, 1::2], inplace=False)
    fname1 = str(tmp_path / "test_beam1.beamfits")
    fname2 = str(tmp_path / "test_beam2.beamfits")
    uvb1.write_beamfits(fname1)
    uvb2.write_beamfits(fname2)

    uvb3 = UVBeam()
    uvb3.read([fname1, fname2])
    assert uvb3.filename == ["test_beam1.beamfits", "test_beam2.beamfits"]
    # the histories will be different
    uvb3.history = uvb.history

    assert uvb3 == uvb


@pytest.mark.parametrize("skip", [True, False])
@pytest.mark.parametrize("flip_order", [True, False])
def test_generic_read_multi_bad_files(tmp_path, skip, flip_order):
    uvb = UVBeam()
    uvb = UVBeam()
    uvb.read(mwa_beam_file, pixels_per_deg=1, freq_range=[100e6, 200e6])

    uvb1 = uvb.select(frequencies=uvb.freq_array[0, ::2], inplace=False)
    uvb2 = uvb.select(frequencies=uvb.freq_array[0, 1::2], inplace=False)
    fname1 = str(tmp_path / "test_beam1.beamfits")
    fname2 = str(tmp_path / "test_beam2.beamfits")
    uvb1.write_beamfits(fname1)
    uvb2.write_beamfits(fname2)

    # Give file a bad beam type
    fits.setval(fname1, "BTYPE", value="foobar")
    uvb3 = UVBeam()
    filenames = [fname1, fname2]
    if flip_order:
        # reverse the order to trigger other try/catch block
        filenames = list(reversed(filenames))
    if skip:
        with uvtest.check_warnings(
            UserWarning, f"Failed to read {filenames[0]} due to ValueError"
        ):
            uvb3.read(filenames, skip_bad_files=skip)
        uvb3.history == uvb2.history
        assert uvb3 == uvb2

    else:

        with pytest.raises(ValueError, match="Unknown beam_type: foobar, beam_type"):
            uvb3.read(filenames, skip_bad_files=skip)


def test_generic_read_all_bad_files(tmp_path):
    uvb = UVBeam()
    uvb = UVBeam()
    uvb.read(mwa_beam_file, pixels_per_deg=1, freq_range=[100e6, 200e6])

    uvb1 = uvb.select(frequencies=uvb.freq_array[0, ::2], inplace=False)
    uvb2 = uvb.select(frequencies=uvb.freq_array[0, 1::2], inplace=False)
    fname1 = str(tmp_path / "test_beam1.beamfits")
    fname2 = str(tmp_path / "test_beam2.beamfits")
    uvb1.write_beamfits(fname1)
    uvb2.write_beamfits(fname2)
    # Give files a bad beam type
    fits.setval(fname1, "BTYPE", value="foobar")
    fits.setval(fname2, "BTYPE", value="foobar")
    uvb3 = UVBeam()
    filenames = [fname1, fname2]
    with uvtest.check_warnings(
        UserWarning, "ALL FILES FAILED ON READ",
    ):
        uvb3.read(filenames, skip_bad_files=True)


@pytest.mark.parametrize(
    "filename", [cst_yaml_file, mwa_beam_file, casa_beamfits],
)
def test_from_file(filename):
    """Test from file produces same the results as reading explicitly."""
    uvb = UVBeam()
    # don't run checks because of casa_beamfits, we'll do that later
    uvb.read(filename, run_check=False)
    uvb2 = UVBeam.from_file(filename, run_check=False)
    uvb.read(filename, run_check=False)
    # hera casa beam is missing some parameters but we just want to check
    # that reading is going okay
    if filename == casa_beamfits:
        # fill in missing parameters
        for _uvb in [uvb, uvb2]:
            _uvb.data_normalization = "peak"
            _uvb.feed_name = "casa_ideal"
            _uvb.feed_version = "v0"
            _uvb.model_name = "casa_airy"
            _uvb.model_version = "v0"

            # this file is actually in an orthoslant projection RA/DEC at zenith at a
            # particular time.
            # For now pretend it's in a zenith orthoslant projection
            _uvb.pixel_coordinate_system = "orthoslant_zenith"
    # double check the files are valid
    assert uvb.check()
    assert uvb2.check()
    assert uvb == uvb2


# pyuvdata/uvbeam/tests/conftest.py
# -*- mode: python; coding: utf-8 -*-
# Copyright (c) 2021 Radio Astronomy Software Group
# Licensed under the 2-clause BSD License

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

import pytest
import numpy as np

from pyuvdata.data import DATA_PATH
from pyuvdata import UVBeam

filenames = ["HERA_NicCST_150MHz.txt", "HERA_NicCST_123MHz.txt"]
cst_folder = "NicCSTbeams"
cst_files = [os.path.join(DATA_PATH, cst_folder, f) for f in filenames]


def make_cst_beam(beam_type):
    """Make the default CST testing beam."""
    extra_keywords = {
        "software": "CST 2016",
        "sim_type": "E-farfield",
        "layout": "1 antenna",
        "port_num": 1,
    }

    beam = UVBeam()
    beam.read_cst_beam(
        cst_files,
        beam_type=beam_type,
        frequency=[150e6, 123e6],
        telescope_name="HERA",
        feed_name="Dipole",
        feed_version="1.0",
        feed_pol=["x"],
        model_name="Dipole - Rigging height 4.9 m",
        model_version="1.0",
        x_orientation="east",
        reference_impedance=100,
        history=(
            "Derived from https://github.com/Nicolas-Fagnoni/Simulations."
            "\nOnly 2 files included to keep test data volume low."
        ),
        extra_keywords=extra_keywords,
    )
    return beam


def cut_beam(beam):
    """Downselect a beam to a small sky area to speed tests up."""
    za_max = np.deg2rad(10.0)
    za_inds_use = np.nonzero(beam.axis2_array <= za_max)[0]
    beam.select(axis2_inds=za_inds_use)
    return beam


def single_freq_version(beam):
    """Make a single freq version with expected history."""
    history_use = beam.history[: beam.history.find(" Combined data")]
    beam.select(freq_chans=1)
    beam.filename = [beam.filename[1]]
    beam._filename.form = (1,)
    beam.history = history_use
    return beam


@pytest.fixture(scope="session")
def cst_efield_2freq_main():
    """Make session level 2-freq efield beam."""
    return make_cst_beam("efield")


@pytest.fixture(scope="function")
def cst_efield_2freq(cst_efield_2freq_main):
    """Make function level 2-freq efield beam."""
    return cst_efield_2freq_main.copy()


@pytest.fixture(scope="session")
def cst_efield_2freq_cut_main(cst_efield_2freq_main):
    """Make session level cut down 2-freq efield beam."""
    return cut_beam(cst_efield_2freq_main.copy())


@pytest.fixture(scope="function")
def cst_efield_2freq_cut(cst_efield_2freq_cut_main):
    """Make function level cut down 2-freq efield beam."""
    return cst_efield_2freq_cut_main.copy()


@pytest.fixture(scope="session")
def cst_efield_2freq_cut_healpix_main(cst_efield_2freq_cut_main):
    """Make session level cut down HEALPix 2-freq efield beam."""
    pytest.importorskip("astropy_healpix")
    beam = cst_efield_2freq_cut_main.copy()
    beam.interpolation_function = "az_za_simple"
    beam.to_healpix()
    return beam


@pytest.fixture(scope="function")
def cst_efield_2freq_cut_healpix(cst_efield_2freq_cut_healpix_main):
    """Make function level cut down HEALPix 2-freq efield beam."""
    return cst_efield_2freq_cut_healpix_main.copy()


@pytest.fixture(scope="session")
def cst_efield_1freq_main(cst_efield_2freq_main):
    """Make session level single freq efield beam."""
    return single_freq_version(cst_efield_2freq_main.copy())


@pytest.fixture(scope="function")
def cst_efield_1freq(cst_efield_1freq_main):
    """Make function level single freq efield beam."""
    return cst_efield_1freq_main.copy()


@pytest.fixture(scope="session")
def cst_efield_1freq_cut_main(cst_efield_2freq_cut_main):
    """Make session level cut down single freq efield beam."""
    return single_freq_version(cst_efield_2freq_cut_main.copy())


@pytest.fixture(scope="function")
def cst_efield_1freq_cut(cst_efield_1freq_cut_main):
    """Make function level cut down single freq efield beam."""
    return cst_efield_1freq_cut_main.copy()


@pytest.fixture(scope="session")
def cst_efield_1freq_cut_healpix_main(cst_efield_2freq_cut_healpix_main):
    """Make session level HEALPix cut down single freq efield beam."""
    return single_freq_version(cst_efield_2freq_cut_healpix_main.copy())


@pytest.fixture(scope="function")
def cst_efield_1freq_cut_healpix(cst_efield_1freq_cut_healpix_main):
    """Make function level HEALPix cut down single freq efield beam."""
    return cst_efield_1freq_cut_healpix_main.copy()


@pytest.fixture(scope="session")
def cst_power_2freq_main():
    """Make session level 2-freq power beam."""
    return make_cst_beam("power")


@pytest.fixture(scope="function")
def cst_power_2freq(cst_power_2freq_main):
    """Make function level 2-freq efield beam."""
    return cst_power_2freq_main.copy()


@pytest.fixture(scope="session")
def cst_power_2freq_cut_main(cst_power_2freq_main):
    """Make session level cut down 2-freq power beam."""
    return cut_beam(cst_power_2freq_main.copy())


@pytest.fixture(scope="function")
def cst_power_2freq_cut(cst_power_2freq_cut_main):
    """Make function level cut down 2-freq power beam."""
    return cst_power_2freq_cut_main.copy()


@pytest.fixture(scope="session")
def cst_power_2freq_cut_healpix_main(cst_power_2freq_cut_main):
    """Make session level HEALPix cut down 2-freq power beam."""
    pytest.importorskip("astropy_healpix")
    beam = cst_power_2freq_cut_main.copy()
    beam.interpolation_function = "az_za_simple"
    beam.to_healpix()
    return beam


@pytest.fixture(scope="function")
def cst_power_2freq_cut_healpix(cst_power_2freq_cut_healpix_main):
    """Make function level HEALPix cut down 2-freq power beam."""
    return cst_power_2freq_cut_healpix_main.copy()


@pytest.fixture(scope="session")
def cst_power_1freq_main(cst_power_2freq_main):
    """Make session level single freq power beam."""
    return single_freq_version(cst_power_2freq_main.copy())


@pytest.fixture(scope="function")
def cst_power_1freq(cst_power_1freq_main):
    """Make function level single freq power beam."""
    return cst_power_1freq_main.copy()


@pytest.fixture(scope="session")
def cst_power_1freq_cut_main(cst_power_2freq_cut_main):
    """Make session level cut down single freq power beam."""
    return single_freq_version(cst_power_2freq_cut_main.copy())


@pytest.fixture(scope="function")
def cst_power_1freq_cut(cst_power_1freq_cut_main):
    """Make function level cut down single freq power beam."""
    return cst_power_1freq_cut_main.copy()


@pytest.fixture(scope="session")
def cst_power_1freq_cut_healpix_main(cst_power_2freq_cut_healpix_main):
    """Make session level HEALPix cut down single freq power beam."""
    return single_freq_version(cst_power_2freq_cut_healpix_main.copy())


@pytest.fixture(scope="function")
def cst_power_1freq_cut_healpix(cst_power_1freq_cut_healpix_main):
    """Make function level HEALPix cut down single freq power beam."""
    return cst_power_1freq_cut_healpix_main.copy()
