From a453036adb4dc503e8a9ded416e7fe6478e9eee4 Mon Sep 17 00:00:00 2001 From: Bryna Hazelton Date: Sun, 21 Jan 2024 16:48:22 -0800 Subject: [PATCH] Add a UVBeam.new method similar to the ones on UVData and UVCal Deprecate the unused `spw_array` and `Nspws` attributes on UVBeam. Deprecate the `UVBeam.freq_interp_kind` attribute in favor of a parameter on the interp method. Deprecate upper case strings in UVBeam.feed_array --- CHANGELOG.md | 14 + pyuvdata/uvbeam/beamfits.py | 17 +- pyuvdata/uvbeam/cst_beam.py | 3 - pyuvdata/uvbeam/initializers.py | 380 +++++++++++++++++++++ pyuvdata/uvbeam/mwa_beam.py | 4 - pyuvdata/uvbeam/tests/test_beamfits.py | 2 - pyuvdata/uvbeam/tests/test_initializers.py | 298 ++++++++++++++++ pyuvdata/uvbeam/tests/test_uvbeam.py | 162 ++++++--- pyuvdata/uvbeam/uvbeam.py | 199 +++++++---- pyuvdata/uvcal/initializers.py | 4 + pyuvdata/uvcal/tests/test_initializers.py | 4 + pyuvdata/uvdata/initializers.py | 4 + pyuvdata/uvdata/tests/test_initializers.py | 4 + 13 files changed, 959 insertions(+), 136 deletions(-) create mode 100644 pyuvdata/uvbeam/initializers.py create mode 100644 pyuvdata/uvbeam/tests/test_initializers.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 2dd0ab3c7..9eb305a3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file. ## [Unreleased] ### Added +- A new `freq_interp_kind` parameter to `UVBeam.interp`, `UVBeam._interp_az_za_rect_spline` +and `UVBeam._interp_healpix_bilinear` to allow the frequency interpolation +specification to be passed into the methods. Note this defaults to "cubic" rather +than "linear" (the old default for the attribute of the same name on UVBeam objects) +because several groups have found that a linear interpolation leads to nasty +artifacts in visibility simulations for EoR applications. +- A new `UVBeam.new()` method (based on new function `new_uvbeam`) that creates a new, +self-consistent `UVBeam` object from scratch from a set of flexible input parameters. - Added a the `UVData.update_antenna_positions` method to enable making antenna position updates with corresponding updates the uvw-coordinates and visibility phases. - Added a switch to `UVData.write_ms` called `flip_conj`, which allows a user to write @@ -32,6 +40,12 @@ tolerance value to be user-specified. Additionally, failing this check results in a warning (was an error). ### Deprecated +- The `freq_interp_kind` attribute on UVBeams. +- The `spw_array` and `Nspws` attributes on UVBeam objects. Also the +`unset_spw_params` and `set_spw_params` parameters to the `use_future_array_shapes` +and `use_current_array_shapes` methods on UVBeam objects. +- Upper case feed names (e.g. "N" or "E") in UVBeam.feed_array. This was never +fully tested and didn't work properly. - Having `freq_range` defined on non-wide-band gain style UVCal objects. - Having `freq_array` and `channel_width` defined on wide-band UVCal objects. diff --git a/pyuvdata/uvbeam/beamfits.py b/pyuvdata/uvbeam/beamfits.py index e9ac142eb..557e151bb 100644 --- a/pyuvdata/uvbeam/beamfits.py +++ b/pyuvdata/uvbeam/beamfits.py @@ -253,9 +253,6 @@ def read_beamfits( "UVBeam does not support having a spectral window axis " "larger than one." ) - if not use_future_array_shapes: - self.Nspws = 1 - self.spw_array = np.array([0]) if n_dimensions > ax_nums["basisvec"] - 1: if ( @@ -268,12 +265,7 @@ def read_beamfits( "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 and not use_future_array_shapes: - self.Nspws = 1 - self.spw_array = np.array([0]) + if self.Naxes_vec is None and self.beam_type == "power": if self.Naxes_vec is None: self.Naxes_vec = 1 @@ -365,7 +357,6 @@ def read_beamfits( 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.freq_interp_kind = primary_header.pop("FINTERP", None) self.history = str(primary_header.get("HISTORY", "")) if not uvutils._check_history_version( @@ -663,12 +654,6 @@ def write_beamfits( if self.x_orientation is not None: primary_header["XORIENT"] = self.x_orientation - 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) + "]" diff --git a/pyuvdata/uvbeam/cst_beam.py b/pyuvdata/uvbeam/cst_beam.py index 59b27e065..3afe695c4 100644 --- a/pyuvdata/uvbeam/cst_beam.py +++ b/pyuvdata/uvbeam/cst_beam.py @@ -200,9 +200,6 @@ def read_cst_beam( self.freq_array = np.zeros((1, self.Nfreqs)) self.bandpass_array = np.zeros((1, self.Nfreqs)) - if not use_future_array_shapes: - self.Nspws = 1 - self.spw_array = np.array([0]) self.pixel_coordinate_system = "az_za" self._set_cs_params() diff --git a/pyuvdata/uvbeam/initializers.py b/pyuvdata/uvbeam/initializers.py new file mode 100644 index 000000000..7a9f0b4f0 --- /dev/null +++ b/pyuvdata/uvbeam/initializers.py @@ -0,0 +1,380 @@ +"""From-memory initializers for UVCal objects.""" +from __future__ import annotations + +from typing import Literal + +import numpy as np +import numpy.typing as npt +from astropy.time import Time + +from .. import __version__, utils +from ..uvdata.initializers import XORIENTMAP + +# todo: spw_array?? + + +def new_uvbeam( + *, + telescope_name: str, + feed_name: str, + feed_version: str, + model_name: str, + model_version: str, + data_normalization: Literal["physical", "peak", "solid_angle"], + freq_array: npt.NDArray[np.float], + feed_array: npt.NDArray[np.str] | None = None, + polarization_array: npt.NDArray[np.str | np.int] + | list[str | int] + | tuple[str | int] + | None = None, + x_orientation: Literal["east", "north", "e", "n", "ew", "ns"] | None = None, + pixel_coordinate_system: Literal["az_za", "orthoslant_zenith", "healpix"] + | None = None, + axis1_array: npt.NDArray[np.float] | None = None, + axis2_array: npt.NDArray[np.float] | None = None, + nside: int | None = None, + ordering: Literal["ring", "nested"] | None = None, + healpix_pixel_array: npt.NDArray[np.int] | None = None, + basis_vector_array: npt.NDArray[np.float] | None = None, + bandpass_array: npt.NDArray[np.float] | None = None, + element_location_array: npt.NDArray[np.float] | None = None, + element_coordinate_system: Literal["n-e", "x-y"] | None = None, + delay_array: npt.NDArray[np.float] | None = None, + gain_array: npt.NDArray[np.float] | None = None, + coupling_matrix: npt.NDArray[np.float] | None = None, + data_array: npt.NDArray[np.float] | None = None, + history: str = "", +): + r"""Create a new UVBeam object with default parameters. + + Parameters + ---------- + telescope_name : str + Telescope name. + feed_name : str + Name of physical feed + feed_version : str + Version of physical feed. + model_name : str + Name of beam model. + model_version: str + Version of beam model. + uvc.telescope_name = telescope_name + data_normalization : str + "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." + freq_array : ndarray of float + Array of frequencies in Hz. + feed_array : ndarray of str + Array of feed orientations. Options are: n/e or x/y or r/l. Must be + provided for an E-field beam. + polarization_array : ndarray of str or int + Array of polarization integers or strings (eg. 'xx' or 'ee'). Must be + provided for a power beam. + x_orientation : str, optional + Orientation of the x-axis. Options are 'east', 'north', 'e', 'n', 'ew', 'ns'. + pixel_coordinate_system : str + Pixel coordinate system, options are "az_za", "orthoslant_zenith" and "healpix". + Forced to be "healpix" if ``nside`` is given and by *default* set to + "az_za" if not. + axis1_array : ndarray of float + Coordinates along first pixel axis (e.g. azimuth for an azimuth/zenith + angle coordinate system). Should not provided for healpix coordinates. + axis2_array : ndarray of float + Coordinates along second pixel axis (e.g. zenith angle for an azimuth/zenith + angle coordinate system). Should not provided for healpix coordinates. + nside : int + Healpix nside parameter, should only be provided for healpix coordinates. + healpix_pixel_array : ndarray of int + Healpix pixels to include. If nside is provided, defaults to all the pixels + in the Healpix map. + ordering : str + Healpix ordering parameter, defaults to "ring" if nside is provided. + basis_vector_array : ndarray of float + Beam basis vector components, essentially the mapping between the + directions that the electrical field values are recorded in to the + directions aligned with the pixel coordinate system (or azimuth/zenith + angle for HEALPix beams). Defaults to unit vectors aligned with the + pixel coordinate systems for E-field beams. + bandpass_array : ndarray of float + 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). Must be the same length as the ``freq_array``. Defaults + to an array of all ones the length of the ``freq_array``. + element_location_array : ndarray of float + Array of phased array element locations in the element_coordinate_system. + Must be a 2 dimensional array where the first dimension indexes the + coordinate system (so should be length 2) and the second dimension indexes + the phased array elements. Only used for phase array antennas. + element_coordinate_system : str + Coordinate system for describing the layout of a phased array feed. + Options are: n-e or x-y. Defaults to "x-y" if ``element_location_array`` + is provided. Only used for phase array antennas. + delay_array : ndarray of float + Array of element delays in seconds. Defaults to an array of all zeros if + ``element_location_array`` is provided. Only used for phase array antennas. + gain_array : ndarray of float + Array of element gains in dB. Defaults to an array of all ones if + ``element_location_array`` is provided. Only used for phase array antennas. + coupling_matrix : ndarray of float + Matrix of complex element couplings in dB. Must be an ndarray of shape + (Nelements, Nelements, Nfeeds, Nfeeds, Nfreqs). Defaults to an array + with couplings of one for the same element and zero for other elements if + ``element_location_array`` is provided. Only used for phase array antennas. + data_array : ndarray of float, optional + Either complex E-field values (if `feed_array` is given) or power values + (if `polarization_array` is given) for the beam model. Units are + normalized to either peak or solid angle as given by data_normalization. + If None (the default), the data_array is initialized to an array of the + appropriate shape and type containing all zeros. + history : str, optional + History string to be added to the object. Default is a simple string + containing the date and time and pyuvdata version. + \*\*kwargs + All other keyword arguments are added to the object as attributes. + + Returns + ------- + UVCal + A new UVCal object with default parameters. + + """ + from .uvbeam import UVBeam + + uvb = UVBeam() + + if (feed_array is not None and polarization_array is not None) or ( + feed_array is None and polarization_array is None + ): + raise ValueError("Provide *either* feed_array *or* polarization_array") + + if feed_array is not None: + uvb.beam_type = "efield" + uvb.feed_array = np.asarray(feed_array) + + uvb.Nfeeds = uvb.feed_array.size + uvb._set_efield() + else: + uvb.beam_type = "power" + polarization_array = np.asarray(polarization_array) + if polarization_array.dtype.kind != "i": + polarization_array = np.asarray(utils.polstr2num(polarization_array)) + uvb.polarization_array = polarization_array + + uvb.Npols = uvb.polarization_array.size + uvb._set_power() + + uvb._set_future_array_shapes() + + if (nside is not None) and (axis1_array is not None or axis2_array is not None): + raise ValueError( + "Provide *either* nside (and optionally healpix_pixel_array and " + "ordering) *or* axis1_array and axis2_array." + ) + + if nside is not None or healpix_pixel_array is not None: + if nside is None: + raise ValueError("nside must be provided if healpix_pixel_array is given.") + if healpix_pixel_array is None: + healpix_pixel_array = np.arange(12 * nside**2, dtype=int) + if ordering is None: + ordering = "ring" + + uvb.nside = nside + uvb.pixel_array = healpix_pixel_array + uvb.ordering = ordering + + uvb.Npixels = healpix_pixel_array.size + + uvb.pixel_coordinate_system = "healpix" + uvb.Naxes_vec = 2 + uvb.Ncomponents_vec = 2 + elif axis1_array is not None and axis2_array is not None: + uvb.axis1_array = axis1_array + uvb.axis2_array = axis2_array + + uvb.Naxes1 = axis1_array.size + uvb.Naxes2 = axis2_array.size + + if pixel_coordinate_system is not None: + allowed_pcs = list(uvb.coordinate_system_dict.keys()) + if uvb.pixel_coordinate_system not in allowed_pcs: + raise ValueError( + f"pixel_coordinate_system must be one of {allowed_pcs}" + ) + + uvb.pixel_coordinate_system = pixel_coordinate_system or "az_za" + uvb.Naxes_vec = 2 + uvb.Ncomponents_vec = 2 + else: + raise ValueError( + "Either nside or both axis1_array and axis2_array must be provided." + ) + + if uvb.beam_type == "power": + uvb.Naxes_vec = 1 + + uvb._set_cs_params() + + uvb.telescope_name = telescope_name + uvb.feed_name = feed_name + uvb.feed_version = feed_version + uvb.model_name = model_name + uvb.model_version = model_version + + uvb.data_normalization = data_normalization + + if not isinstance(freq_array, np.ndarray): + raise ValueError("freq_array must be a numpy ndarray.") + if freq_array.ndim != 1: + raise ValueError("freq_array must be one dimensional.") + uvb.freq_array = freq_array + uvb.Nfreqs = freq_array.size + + if x_orientation is not None: + uvb.x_orientation = XORIENTMAP[x_orientation.lower()] + + if basis_vector_array is not None: + if uvb.pixel_coordinate_system == "healpix": + bv_shape = (uvb.Naxes_vec, uvb.Ncomponents_vec, uvb.Npixels) + else: + bv_shape = (uvb.Naxes_vec, uvb.Ncomponents_vec, uvb.Naxes2, uvb.Naxes1) + if basis_vector_array.shape != bv_shape: + raise ValueError( + f"basis_vector_array shape {basis_vector_array.shape} does not match " + f"expected shape {bv_shape}." + ) + uvb.basis_vector_array = basis_vector_array + elif uvb.beam_type == "efield": + if uvb.pixel_coordinate_system == "healpix": + basis_vector_array = np.zeros( + (uvb.Naxes_vec, uvb.Ncomponents_vec, uvb.Npixels), dtype=float + ) + basis_vector_array[0, 0] = np.ones(uvb.Npixels, dtype=float) + basis_vector_array[1, 1] = np.ones(uvb.Npixels, dtype=float) + else: + basis_vector_array = np.zeros( + (uvb.Naxes_vec, uvb.Ncomponents_vec, uvb.Naxes2, uvb.Naxes1), + dtype=float, + ) + basis_vector_array[0, 0] = np.ones((uvb.Naxes2, uvb.Naxes1), dtype=float) + basis_vector_array[1, 1] = np.ones((uvb.Naxes2, uvb.Naxes1), dtype=float) + uvb.basis_vector_array = basis_vector_array + + if bandpass_array is not None: + if bandpass_array.shape != freq_array.shape: + raise ValueError( + "The bandpass array must have the same shape as the freq_array." + ) + uvb.bandpass_array = bandpass_array + else: + uvb.bandpass_array = np.ones_like(uvb.freq_array) + + if element_location_array is not None: + if feed_array is None: + raise ValueError( + "feed_array must be provided if element_location_array is given." + ) + + if element_location_array.ndim != 2: + raise ValueError("element_location_array must be 2 dimensional") + shape = element_location_array.shape + if shape[0] != 2: + raise ValueError( + "The first dimension of element_location_array must be length 2" + ) + if shape[1] <= 1: + raise ValueError( + "The second dimension of element_location_array must be >= 2." + ) + + if element_coordinate_system is None: + element_coordinate_system = "x-y" + if delay_array is None: + delay_array = np.zeros(shape[1]) + else: + if delay_array.shape != (shape[1],): + raise ValueError( + "delay_array must be one dimensional with length " + "equal to the second dimension of element_location_array" + ) + if gain_array is None: + gain_array = np.ones(shape[1]) + else: + if gain_array.shape != (shape[1],): + raise ValueError( + "gain_array must be one dimensional with length " + "equal to the second dimension of element_location_array" + ) + coupling_shape = (shape[1], shape[1], uvb.Nfeeds, uvb.Nfeeds, uvb.Nfreqs) + if coupling_matrix is None: + coupling_matrix = np.zeros(coupling_shape, dtype=complex) + for element in range(shape[1]): + coupling_matrix[element, element] = np.ones( + (uvb.Nfeeds, uvb.Nfeeds, uvb.Nfreqs), dtype=complex + ) + else: + if coupling_matrix.shape != coupling_shape: + raise ValueError( + f"coupling_matrix shape {coupling_matrix.shape} does not " + f"match expected shape {coupling_shape}." + ) + + uvb.antenna_type = "phased_array" + uvb._set_phased_array() + uvb.element_coordinate_system = element_coordinate_system + uvb.element_location_array = element_location_array + uvb.Nelements = shape[1] + uvb.delay_array = delay_array + uvb.gain_array = gain_array + uvb.coupling_matrix = coupling_matrix + else: + uvb.antenna_type = "simple" + uvb._set_simple() + + # Set data parameters + if uvb.beam_type == "efield": + if uvb.pixel_coordinate_system == "healpix": + data_shape = (uvb.Naxes_vec, uvb.Nfeeds, uvb.Nfreqs, uvb.Npixels) + else: + data_shape = (uvb.Naxes_vec, uvb.Nfeeds, uvb.Nfreqs, uvb.Naxes2, uvb.Naxes1) + data_type = complex + else: + if uvb.pixel_coordinate_system == "healpix": + data_shape = (uvb.Naxes_vec, uvb.Npols, uvb.Nfreqs, uvb.Npixels) + else: + data_shape = (uvb.Naxes_vec, uvb.Npols, uvb.Nfreqs, uvb.Naxes2, uvb.Naxes1) + data_type = float + + if data_array is not None: + if not isinstance(data_array, np.ndarray): + raise ValueError("data_array must be a numpy ndarray") + if data_array.shape != data_shape: + raise ValueError( + f"Data array shape {data_array.shape} does not match " + f"expected shape {data_shape}." + ) + uvb.data_array = data_array + else: + uvb.data_array = np.zeros(data_shape, dtype=data_type) + + history += ( + f"Object created by new_uvbeam() at {Time.now().iso} using " + f"pyuvdata version {__version__}." + ) + uvb.history = history + + uvb.check() + return uvb diff --git a/pyuvdata/uvbeam/mwa_beam.py b/pyuvdata/uvbeam/mwa_beam.py index 675311340..bdb35a605 100644 --- a/pyuvdata/uvbeam/mwa_beam.py +++ b/pyuvdata/uvbeam/mwa_beam.py @@ -669,10 +669,6 @@ def read_mwa_beam( self.freq_array = self.freq_array[np.newaxis, :] self.bandpass_array = np.ones((1, self.Nfreqs)) - if not use_future_array_shapes: - self.Nspws = 1 - self.spw_array = np.array([0]) - self.pixel_coordinate_system = "az_za" self._set_cs_params() diff --git a/pyuvdata/uvbeam/tests/test_beamfits.py b/pyuvdata/uvbeam/tests/test_beamfits.py index 6e693656d..5589f3e67 100644 --- a/pyuvdata/uvbeam/tests/test_beamfits.py +++ b/pyuvdata/uvbeam/tests/test_beamfits.py @@ -102,8 +102,6 @@ def test_read_cst_write_read_fits_efield(cst_efield_1freq, future_shapes, tmp_pa beam_out = UVBeam() - beam_in.freq_interp_kind = "linear" - write_file = str(tmp_path / "outtest_beam.fits") beam_in.write_beamfits(write_file, clobber=True) diff --git a/pyuvdata/uvbeam/tests/test_initializers.py b/pyuvdata/uvbeam/tests/test_initializers.py new file mode 100644 index 000000000..eacacde3c --- /dev/null +++ b/pyuvdata/uvbeam/tests/test_initializers.py @@ -0,0 +1,298 @@ +# -*- mode: python; coding: utf-8 -*- +# Copyright (c) 2024 Radio Astronomy Software Group +# Licensed under the 2-clause BSD License +import re + +import numpy as np +import pytest + +from pyuvdata import UVBeam + +ph_params = [ + "element_location_array", + "element_coordinate_system", + "delay_array", + "gain_array", + "coupling_matrix", +] + + +@pytest.fixture() +def uvb_common_kw(): + return { + "telescope_name": "mock", + "feed_name": "short dipole", + "feed_version": "1.0", + "model_name": "hertzian", + "model_version": "1.0", + "data_normalization": "physical", + "freq_array": np.linspace(100e6, 200e6, 10), + } + + +@pytest.fixture() +def uvb_azza_kw(): + return { + "axis1_array": np.deg2rad(np.linspace(-180, 179, 360)), + "axis2_array": np.deg2rad(np.linspace(0, 90, 181)), + } + + +@pytest.fixture() +def uvb_healpix_kw(): + return {"nside": 64} + + +@pytest.fixture() +def uvb_efield_kw(): + return {"feed_array": ["x", "y"]} + + +@pytest.fixture() +def uvb_power_kw(): + return {"polarization_array": ["xx", "yy"]} + + +@pytest.fixture() +def uvb_azza_efield_kw(uvb_common_kw, uvb_azza_kw, uvb_efield_kw): + return {**uvb_common_kw, **uvb_azza_kw, **uvb_efield_kw} + + +@pytest.fixture() +def phased_array_efield(uvb_azza_efield_kw, phased_array_beam_2freq): + uvb_azza_efield_kw["freq_array"] = (uvb_azza_efield_kw["freq_array"])[0:2] + for param in ph_params: + uvb_azza_efield_kw[param] = getattr(phased_array_beam_2freq, param) + + return uvb_azza_efield_kw + + +@pytest.mark.parametrize("coord_sys", ["az_za", "healpix"]) +@pytest.mark.parametrize("beam_type", ["efield", "power"]) +def test_new_uvcal_simplest( + uvb_common_kw, + uvb_azza_kw, + uvb_healpix_kw, + uvb_efield_kw, + uvb_power_kw, + coord_sys, + beam_type, +): + if coord_sys == "az_za": + kw_use = {**uvb_common_kw, **uvb_azza_kw} + else: + kw_use = {**uvb_common_kw, **uvb_healpix_kw} + + if beam_type == "efield": + kw_use = {**kw_use, **uvb_efield_kw} + else: + kw_use = {**kw_use, **uvb_power_kw} + + uvb = UVBeam.new(**kw_use) + assert uvb.Nfreqs == 10 + if beam_type == "efield": + assert uvb.Nfeeds == 2 + else: + assert uvb.Npols == 2 + + if uvb.pixel_coordinate_system == "healpix": + assert uvb.Npixels == 12 * uvb.nside**2 + else: + assert uvb.Naxes1 == 360 + assert uvb.Naxes2 == 181 + + +def test_x_orientation(uvb_azza_efield_kw): + uvb_azza_efield_kw["x_orientation"] = "e" + uvb = UVBeam.new(**uvb_azza_efield_kw) + + assert uvb.x_orientation == "east" + + +def test_basis_vec(uvb_azza_efield_kw): + uvb1 = UVBeam.new(**uvb_azza_efield_kw) + + basis_vector_array = np.zeros((2, 2, 181, 360), dtype=float) + basis_vector_array[0, 0] = np.ones((181, 360), dtype=float) + basis_vector_array[1, 1] = np.ones((181, 360), dtype=float) + + uvb_azza_efield_kw["basis_vector_array"] = basis_vector_array + uvb2 = UVBeam.new(**uvb_azza_efield_kw) + + # make histories match (time stamp makes them different) + uvb2.history = uvb1.history + + assert uvb2 == uvb1 + + uvb_azza_efield_kw["basis_vector_array"] = np.zeros((2, 2, 360, 181), dtype=float) + with pytest.raises( + ValueError, + match=re.escape( + "basis_vector_array shape (2, 2, 360, 181) does not match expected " + "shape (2, 2, 181, 360)." + ), + ): + UVBeam.new(**uvb_azza_efield_kw) + + +def test_bandpass(uvb_azza_efield_kw): + uvb1 = UVBeam.new(**uvb_azza_efield_kw) + + uvb_azza_efield_kw["bandpass_array"] = np.ones(10) + uvb2 = UVBeam.new(**uvb_azza_efield_kw) + + # make histories match (time stamp makes them different) + uvb2.history = uvb1.history + + assert uvb2 == uvb1 + + uvb_azza_efield_kw["bandpass_array"] = np.ones((1, 10)) + with pytest.raises( + ValueError, + match="The bandpass array must have the same shape as the freq_array.", + ): + UVBeam.new(**uvb_azza_efield_kw) + + +@pytest.mark.parametrize( + "rm_param", + [None, "element_coordinate_system", "delay_array", "gain_array", "coupling_matrix"], +) +def test_phased_array(phased_array_efield, phased_array_beam_2freq, rm_param): + if rm_param is not None: + del phased_array_efield[rm_param] + + uvb = UVBeam.new(**phased_array_efield) + + for param in ph_params: + assert getattr(uvb, "_" + param) == getattr( + phased_array_beam_2freq, "_" + param + ) + + +def test_no_feed_pol_error(uvb_common_kw): + with pytest.raises( + ValueError, + match=re.escape("Provide *either* feed_array *or* polarization_array"), + ): + UVBeam.new(**uvb_common_kw) + + +def test_pcs_params_error(uvb_common_kw, uvb_efield_kw, uvb_azza_kw, uvb_healpix_kw): + with pytest.raises( + ValueError, + match="Either nside or both axis1_array and axis2_array must be provided.", + ): + UVBeam.new(**uvb_common_kw, **uvb_efield_kw) + + with pytest.raises( + ValueError, + match=re.escape( + "Provide *either* nside (and optionally healpix_pixel_array and " + "ordering) *or* axis1_array and axis2_array." + ), + ): + UVBeam.new(**uvb_common_kw, **uvb_efield_kw, **uvb_azza_kw, **uvb_healpix_kw) + + hpx_kws = {"healpix_pixel_array": np.arange(12 * 64**2)} + with pytest.raises( + ValueError, match="nside must be provided if healpix_pixel_array is given." + ): + UVBeam.new(**uvb_common_kw, **uvb_efield_kw, **hpx_kws) + + pcs_kws = {"pixel_coordinate_system": "foo"} + with pytest.raises( + ValueError, + match=re.escape( + "pixel_coordinate_system must be one of ['az_za', " + "'orthoslant_zenith', 'healpix']" + ), + ): + UVBeam.new(**uvb_common_kw, **uvb_efield_kw, **uvb_azza_kw, **pcs_kws) + + +def test_freq_array_errors(uvb_azza_efield_kw): + uvb_azza_efield_kw["freq_array"] = (uvb_azza_efield_kw["freq_array"])[np.newaxis] + with pytest.raises(ValueError, match="freq_array must be one dimensional."): + UVBeam.new(**uvb_azza_efield_kw) + + uvb_azza_efield_kw["freq_array"] = uvb_azza_efield_kw["freq_array"].tolist() + with pytest.raises(ValueError, match="freq_array must be a numpy ndarray"): + UVBeam.new(**uvb_azza_efield_kw) + + +def test_data_array_errors(uvb_azza_efield_kw): + uvb_azza_efield_kw["data_array"] = (2, 2, 10, 360, 181) + with pytest.raises(ValueError, match="data_array must be a numpy ndarray"): + UVBeam.new(**uvb_azza_efield_kw) + + uvb_azza_efield_kw["data_array"] = np.zeros((2, 2, 10, 360, 181)) + with pytest.raises( + ValueError, + match=re.escape( + "Data array shape (2, 2, 10, 360, 181) does not match expected " + "shape (2, 2, 10, 181, 360)." + ), + ): + UVBeam.new(**uvb_azza_efield_kw) + + +@pytest.mark.parametrize( + ["key_rm", "new_key", "value", "msg"], + [ + [ + "feed_array", + "polarization_array", + [-1, -2], + "feed_array must be provided if element_location_array is given.", + ], + [ + None, + "element_location_array", + np.arange(4), + "element_location_array must be 2 dimensional", + ], + [ + None, + "element_location_array", + np.ones((4, 4)), + "The first dimension of element_location_array must be length 2", + ], + [ + None, + "element_location_array", + np.ones((2, 1)), + "The second dimension of element_location_array must be >= 2.", + ], + [ + None, + "delay_array", + np.arange(6), + "delay_array must be one dimensional with length " + "equal to the second dimension of element_location_array", + ], + [ + None, + "gain_array", + np.arange(6), + "gain_array must be one dimensional with length " + "equal to the second dimension of element_location_array", + ], + [ + None, + "coupling_matrix", + np.zeros((4, 4, 2, 2, 20)), + re.escape( + "coupling_matrix shape (4, 4, 2, 2, 20) does not " + "match expected shape (4, 4, 2, 2, 2)." + ), + ], + ], +) +def test_phased_array_errors(phased_array_efield, key_rm, new_key, value, msg): + if key_rm is not None: + del phased_array_efield[key_rm] + phased_array_efield[new_key] = value + + with pytest.raises(ValueError, match=msg): + UVBeam.new(**phased_array_efield) diff --git a/pyuvdata/uvbeam/tests/test_uvbeam.py b/pyuvdata/uvbeam/tests/test_uvbeam.py index 0db76bd50..3ad2e97a1 100644 --- a/pyuvdata/uvbeam/tests/test_uvbeam.py +++ b/pyuvdata/uvbeam/tests/test_uvbeam.py @@ -73,15 +73,12 @@ def uvbeam_data(): "feed_array", "polarization_array", "basis_vector_array", - "Nspws", - "spw_array", "extra_keywords", "Nelements", "element_coordinate_system", "element_location_array", "delay_array", "x_orientation", - "freq_interp_kind", "gain_array", "coupling_matrix", "reference_impedance", @@ -275,14 +272,22 @@ def test_future_array_shapes( beam2 = beam.copy() # test the no-op - beam.use_future_array_shapes() + with uvtest.check_warnings( + DeprecationWarning, + match="The unset_spw_params parameter is deprecated and has no effect. " + "This will become an error in version 2.6.", + ): + beam.use_future_array_shapes(unset_spw_params=True) with uvtest.check_warnings( - DeprecationWarning, match="This method will be removed in version 3.0" + [DeprecationWarning] * 2, + match=[ + "This method will be removed in version 3.0", + "The set_spw_params parameter is deprecated and has no effect. " + "This will become an error in version 2.6.", + ], ): - beam.use_current_array_shapes() - assert beam.Nspws == 1 - assert beam.spw_array is not None + beam.use_current_array_shapes(set_spw_params=False) beam.check() # test the no-op @@ -292,13 +297,41 @@ def test_future_array_shapes( beam.use_current_array_shapes() beam.use_future_array_shapes() - assert beam.Nspws is None - assert beam.spw_array is None beam.check() assert beam == beam2 +@pytest.mark.parametrize("param", ["freq_interp_kind", "spw_array", "Nspws"]) +def test_deprecated_params(cst_efield_2freq, param): + with uvtest.check_warnings( + DeprecationWarning, + match=f"The {param} attribute on UVBeam objects is " + "deprecated and support for it will be removed in version 2.6.", + ): + getattr(cst_efield_2freq, param) + + with uvtest.check_warnings( + DeprecationWarning, + match=f"The {param} attribute on UVBeam objects is " + "deprecated and support for it will be removed in version 2.6.", + ): + setattr(cst_efield_2freq, param, "foo") + + assert getattr(cst_efield_2freq, param) == "foo" + + +def test_deprecated_feed_names(cst_efield_2freq): + cst_efield_2freq.feed_array = np.array(["N", "E"]) + + with uvtest.check_warnings( + DeprecationWarning, + match="Feed array has values ['N', 'E'] that are deprecated. Values in " + "feed_array should be lower case. This will become an error in version 2.6", + ): + cst_efield_2freq.check() + + def test_set_cs_params(cst_efield_2freq): """ Test _set_cs_params. @@ -483,8 +516,12 @@ def test_efield_to_pstokes_error(cst_power_2freq_cut): @pytest.mark.filterwarnings("ignore:This method will be removed in version 3.0") -@pytest.mark.parametrize("future_shapes", [True, False]) -def test_efield_to_power(future_shapes, cst_efield_2freq_cut, cst_power_2freq_cut): +@pytest.mark.parametrize( + ["future_shapes", "physical_orientation"], [[True, False], [False, True]] +) +def test_efield_to_power( + future_shapes, physical_orientation, cst_efield_2freq_cut, cst_power_2freq_cut +): efield_beam = cst_efield_2freq_cut power_beam = cst_power_2freq_cut @@ -492,6 +529,12 @@ def test_efield_to_power(future_shapes, cst_efield_2freq_cut, cst_power_2freq_cu efield_beam.use_current_array_shapes() power_beam.use_current_array_shapes() + if physical_orientation: + efield_beam.feed_array = np.array(["e", "n"]) + power_beam.polarization_array = np.array( + uvutils.polstr2num(["ee", "nn"], x_orientation=power_beam.x_orientation) + ) + 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 @@ -690,6 +733,7 @@ def test_freq_interpolation( interp_arrays = beam.interp( freq_array=freq_orig_vals, freq_interp_tol=0.0, + freq_interp_kind="linear", return_bandpass=True, return_coupling=need_coupling, ) @@ -719,6 +763,7 @@ def test_freq_interpolation( interp_arrays = beam.interp( freq_array=freq_orig_vals, freq_interp_tol=1.0, + freq_interp_kind="cubic", return_bandpass=True, return_coupling=need_coupling, ) @@ -756,8 +801,15 @@ def test_freq_interpolation( exp_warnings.append( f"Input object has {param_name} defined but we do not " "currently support interpolating it in frequency. Returned " - "object will not have it set to None." + "object will have it set to None." ) + # check setting freq_interp_kind on object also works + with uvtest.check_warnings( + DeprecationWarning, + match="The freq_interp_kind attribute on UVBeam objects is " + "deprecated and support for it will be removed in version 2.6. ", + ): + beam.freq_interp_kind = "linear" with uvtest.check_warnings(UserWarning, match=exp_warnings): new_beam_obj = beam.interp( freq_array=freq_orig_vals, freq_interp_tol=0.0, new_object=True @@ -767,9 +819,9 @@ def test_freq_interpolation( np.testing.assert_array_almost_equal(new_beam_obj.freq_array, freq_orig_vals) else: 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 "freq_interp_kind = linear" in new_beam_obj.history assert beam.history != new_beam_obj.history new_beam_obj.history = beam.history # add back optional params to get equality: @@ -777,9 +829,20 @@ def test_freq_interpolation( setattr(new_beam_obj, param_name, getattr(beam, param_name)) assert beam == new_beam_obj - with uvtest.check_warnings(UserWarning, match=exp_warnings): + with uvtest.check_warnings( + [UserWarning] * (len(exp_warnings) + 1), + match=exp_warnings + + [ + "The freq_interp_kind parameter was set but it does not " + "match the freq_interp_kind attribute on the object. " + "Using the one passed to this method." + ], + ): new_beam_obj = beam.interp( - freq_array=freq_orig_vals, freq_interp_tol=1.0, new_object=True + freq_array=freq_orig_vals, + freq_interp_tol=1.0, + freq_interp_kind="cubic", + new_object=True, ) assert isinstance(new_beam_obj, UVBeam) if future_shapes: @@ -787,8 +850,7 @@ def test_freq_interpolation( else: 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 "freq_interp_kind = nearest" in new_beam_obj.history assert beam.history != new_beam_obj.history new_beam_obj.history = beam.history # add back optional params to get equality: @@ -803,6 +865,7 @@ def test_freq_interpolation( new_beam_obj = beam.interp( freq_array=np.linspace(123e6, 150e6, num=5), freq_interp_tol=0.0, + freq_interp_kind="linear", new_object=True, ) @@ -815,7 +878,6 @@ def test_freq_interpolation( 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 beam.history != new_beam_obj.history @@ -855,18 +917,6 @@ def test_freq_interpolation( ): beam_singlef.interp(freq_array=np.array([150e6])) - # assert freq_interp_kind ValueError - beam.freq_interp_kind = None - with pytest.raises( - ValueError, match="freq_interp_kind must be set on object first" - ): - beam.interp( - az_array=beam.axis1_array, - za_array=beam.axis2_array, - freq_array=freq_orig_vals, - polarizations=["xx"], - ) - @pytest.mark.filterwarnings("ignore:This method will be removed in version 3.0") @pytest.mark.parametrize("future_shapes", [True, False]) @@ -879,7 +929,6 @@ def test_freq_interp_real_and_complex(future_shapes, cst_power_2freq): # make a new object with more frequencies freqs = np.linspace(123e6, 150e6, 4) - power_beam.freq_interp_kind = "linear" optional_freq_params = [ "receiver_temperature_array", @@ -892,10 +941,12 @@ def test_freq_interp_real_and_complex(future_shapes, cst_power_2freq): exp_warnings.append( f"Input object has {param_name} defined but we do not " "currently support interpolating it in frequency. Returned " - "object will not have it set to None." + "object will have it set to None." ) with uvtest.check_warnings(UserWarning, match=exp_warnings): - pbeam = power_beam.interp(freq_array=freqs, new_object=True) + pbeam = power_beam.interp( + freq_array=freqs, freq_interp_kind="linear", new_object=True + ) # modulate the data pbeam.data_array[..., 1] *= 2 @@ -903,7 +954,6 @@ def test_freq_interp_real_and_complex(future_shapes, cst_power_2freq): # 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 @@ -993,7 +1043,7 @@ def test_spatial_interpolation_samepoints( exp_warnings.append( f"Input object has {param_name} defined but we do not " "currently support interpolating it in frequency. Returned " - "object will not have it set to None." + "object will have it set to None." ) with uvtest.check_warnings(UserWarning, match=exp_warnings): new_beam = uvbeam.interp( @@ -1003,7 +1053,6 @@ def test_spatial_interpolation_samepoints( 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 " @@ -1013,7 +1062,6 @@ def test_spatial_interpolation_samepoints( ) # make histories & freq_interp_kind equal new_beam.history = uvbeam.history - new_beam.freq_interp_kind = "linear" # add back optional params to get equality: for param_name in optional_freq_params: setattr(new_beam, param_name, getattr(uvbeam, param_name)) @@ -1087,51 +1135,59 @@ def test_spatial_interpolation_everyother( ) 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 + _, _ = uvbeam.interp( + az_array=az_interp_vals, + za_array=za_interp_vals, + freq_array=freq_interp_vals, + freq_interp_kind="linear", ) if beam_type == "power": # Test requesting separate polarizations on different calls # while reusing splines. - interp_data_array, interp_basis_vector = uvbeam.interp( + _, _ = uvbeam.interp( az_array=az_interp_vals[:2], za_array=za_interp_vals[:2], freq_array=freq_interp_vals, + freq_interp_kind="linear", polarizations=["xx"], reuse_spline=True, ) - interp_data_array, interp_basis_vector = uvbeam.interp( + _, _ = uvbeam.interp( az_array=az_interp_vals[:2], za_array=za_interp_vals[:2], freq_array=freq_interp_vals, + freq_interp_kind="linear", polarizations=["yy"], reuse_spline=True, ) # test reusing the spline fit. - orig_data_array, interp_basis_vector = uvbeam.interp( + orig_data_array, _ = uvbeam.interp( az_array=az_interp_vals, za_array=za_interp_vals, freq_array=freq_interp_vals, + freq_interp_kind="linear", reuse_spline=True, ) - reused_data_array, interp_basis_vector = uvbeam.interp( + reused_data_array, _ = uvbeam.interp( az_array=az_interp_vals, za_array=za_interp_vals, freq_array=freq_interp_vals, + freq_interp_kind="linear", 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( + quartic_data_array, _ = uvbeam.interp( az_array=az_interp_vals, za_array=za_interp_vals, freq_array=freq_interp_vals, + freq_interp_kind="linear", spline_opts=spline_opts, ) @@ -1139,16 +1195,18 @@ def test_spatial_interpolation_everyother( 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( + select_data_array_orig, _ = uvbeam.interp( az_array=az_interp_vals[0:1], za_array=za_interp_vals[0:1], freq_array=np.array([127e6]), + freq_interp_kind="linear", ) - select_data_array_reused, interp_basis_vector = uvbeam.interp( + select_data_array_reused, _ = uvbeam.interp( az_array=az_interp_vals[0:1], za_array=za_interp_vals[0:1], freq_array=np.array([127e6]), + freq_interp_kind="linear", reuse_spline=True, ) assert np.allclose(select_data_array_orig, select_data_array_reused) @@ -1226,7 +1284,7 @@ def test_spatial_interpolation_errors(cst_power_2freq_cut): 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) + _, _ = uvbeam.interp(freq_array=freq_interp_vals, freq_interp_kind="linear") # assert polarization value error with pytest.raises( @@ -1394,7 +1452,7 @@ def test_healpix_interpolation( message = [ f"Input object has {param_name} defined but we do not " "currently support interpolating it in frequency. Returned " - "object will not have it set to None." + "object will have it set to None." for param_name in [ "receiver_temperature_array", "loss_array", @@ -1407,6 +1465,7 @@ def test_healpix_interpolation( healpix_inds=hpx_efield_beam.pixel_array[pixel_inds], healpix_nside=hpx_efield_beam.nside, freq_array=np.array([np.mean(freq_orig_vals)]), + freq_interp_kind="linear", new_object=True, ) assert "Interpolated in frequency and to a new healpix grid" in interp_beam.history @@ -1930,14 +1989,16 @@ def test_select_feeds( ): if antenna_type == "simple": efield_beam = cst_efield_1freq + efield_beam.feed_array = np.array(["n", "e"]) + feeds_to_keep = ["e"] else: efield_beam = phased_array_beam_2freq + feeds_to_keep = ["x"] if not future_shapes: efield_beam.use_current_array_shapes() old_history = efield_beam.history - feeds_to_keep = ["x"] if antenna_type == "phased_array": expected_warning = UserWarning @@ -2831,7 +2892,6 @@ def test_beam_area_healpix( 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") - print(beam_int.shape) assert beam_int.shape[0] == numfreqs assert beam_sq_int.shape[0] == numfreqs diff --git a/pyuvdata/uvbeam/uvbeam.py b/pyuvdata/uvbeam/uvbeam.py index f7ba98835..4d8ba094d 100644 --- a/pyuvdata/uvbeam/uvbeam.py +++ b/pyuvdata/uvbeam/uvbeam.py @@ -10,12 +10,15 @@ import numpy as np from astropy import units from astropy.coordinates import Angle +from docstring_parser import DocstringStyle from scipy import interpolate from .. import _uvbeam from .. import parameter as uvp from .. import utils as uvutils +from ..docstrings import combine_docstrings from ..uvbase import UVBase +from . import initializers __all__ = ["UVBeam"] @@ -80,16 +83,6 @@ def __init__(self): "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, - required=False, - ) - desc = ( "Number of basis vectors used to represent the antenna response in each " "pixel. These need not align with the pixel coordinate system or even be " @@ -258,7 +251,7 @@ def __init__(self): desc = ( "Array of feed orientations. shape (Nfeeds). " - 'options are: N/E or x/y or R/L. Not required if beam_type is "power".' + 'options are: n/e or x/y or r/l. Not required if beam_type is "power".' ) self._feed_array = uvp.UVParameter( "feed_array", @@ -266,7 +259,7 @@ def __init__(self): required=False, expected_type=str, form=("Nfeeds",), - acceptable_vals=["N", "E", "x", "y", "R", "L"], + acceptable_vals=["N", "E", "x", "y", "R", "L", "n", "e", "r", "l"], ) self._Npols = uvp.UVParameter( @@ -304,14 +297,6 @@ def __init__(self): tols=1e-3, ) # mHz - self._spw_array = uvp.UVParameter( - "spw_array", - description="Array of spectral window Numbers, shape (Nspws)", - form=("Nspws",), - expected_type=int, - required=False, - ) - desc = ( "Normalization standard of data_array, options are: " '"physical", "peak" or "solid_angle". Physical normalization ' @@ -443,14 +428,14 @@ def __init__(self): desc = ( 'Required if antenna_type = "phased_array". Element coordinate ' - "system, options are: N-E or x-y" + "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"], + acceptable_vals=["n-e", "x-y"], ) desc = ( @@ -519,19 +504,6 @@ def __init__(self): acceptable_vals=["east", "north"], ) - 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. " @@ -623,6 +595,74 @@ def __init__(self): super(UVBeam, self).__init__() + @staticmethod + @combine_docstrings(initializers.new_uvbeam, style=DocstringStyle.NUMPYDOC) + def new(**kwargs): + """ + Create a new UVBeam object. + + All parameters are passed through to + the :func:`~pyuvdata.uvbeam.initializers.new_uvbeam` function. + + Returns + ------- + UVBeam + A new UVBeam object. + """ + return initializers.new_uvbeam(**kwargs) + + def __setattr__(self, __name: str, __value) -> None: + """Raise DeprecationWarnings for setting freq_interp_kind.""" + if __name == "freq_interp_kind": + warnings.warn( + "The freq_interp_kind attribute on UVBeam objects is " + "deprecated and support for it will be removed in version 2.6. " + "Instead, pass the desired function to the `interp` or " + "`to_healpix` methods. ", + DeprecationWarning, + ) + elif __name == "spw_array": + warnings.warn( + "The spw_array attribute on UVBeam objects is " + "deprecated and support for it will be removed in version 2.6.", + DeprecationWarning, + ) + elif __name == "Nspws": + warnings.warn( + "The Nspws attribute on UVBeam objects is " + "deprecated and support for it will be removed in version 2.6.", + DeprecationWarning, + ) + return super().__setattr__(__name, __value) + + def __getattr__(self, __name: str): + """Raise DeprecationWarnings for interpolation_function.""" + if __name == "freq_interp_kind": + warnings.warn( + "The freq_interp_kind attribute on UVBeam objects is " + "deprecated and support for it will be removed in version 2.6." + "Instead, pass the desired function to the `interp` or " + "`to_healpix` methods. ", + DeprecationWarning, + ) + return None + elif __name == "spw_array": + warnings.warn( + "The spw_array attribute on UVBeam objects is " + "deprecated and support for it will be removed in version 2.6.", + DeprecationWarning, + ) + return None + elif __name == "Nspws": + warnings.warn( + "The Nspws attribute on UVBeam objects is " + "deprecated and support for it will be removed in version 2.6.", + DeprecationWarning, + ) + return None + # Error if attribute not found + return self.__getattribute__(__name) + def _freq_params(self): """List of strings giving the parameters shaped like the freq_array.""" form = self._freq_array.form @@ -656,7 +696,7 @@ def _set_future_array_shapes(self): ) self.future_array_shapes = True - def use_future_array_shapes(self, unset_spw_params=True): + def use_future_array_shapes(self, unset_spw_params=None): """ Change the array shapes of this object to match the planned future shapes. @@ -667,10 +707,16 @@ def use_future_array_shapes(self, unset_spw_params=True): Parameters ---------- unset_spw_params : bool - Option to unset the (now optional) spectral window related parameters - (spw_array and Nspws). + Deprecated and has no effect. """ + if unset_spw_params is not None: + warnings.warn( + "The unset_spw_params parameter is deprecated and has no effect. " + "This will become an error in version 2.6.", + DeprecationWarning, + ) + if self.future_array_shapes: return self._set_future_array_shapes() @@ -686,11 +732,7 @@ def use_future_array_shapes(self, unset_spw_params=True): if self.s_parameters is not None: self.s_parameters = self.s_parameters[:, 0, :] - if unset_spw_params: - self.spw_array = None - self.Nspws = None - - def use_current_array_shapes(self, set_spw_params=True): + def use_current_array_shapes(self, set_spw_params=None): """ Change the array shapes of this object to match the current future shapes. @@ -700,9 +742,7 @@ def use_current_array_shapes(self, set_spw_params=True): Parameters ---------- set_spw_params : bool - Option to set the spectral window related parameters (spw_array and Nspws) - to their default values if they are not set. These parameters are optional, - but were required in the past. + Deprecated and has no effect. """ warnings.warn( @@ -711,6 +751,13 @@ def use_current_array_shapes(self, set_spw_params=True): DeprecationWarning, ) + if set_spw_params is not None: + warnings.warn( + "The set_spw_params parameter is deprecated and has no effect. " + "This will become an error in version 2.6.", + DeprecationWarning, + ) + if not self.future_array_shapes: return @@ -739,9 +786,6 @@ def use_current_array_shapes(self, set_spw_params=True): if this_prop is not None: setattr(self, prop_name, this_prop[np.newaxis, :]) - if self.spw_array is None and self.Nspws is None and set_spw_params: - self.Nspws = 1 - self.spw_array = np.array([0]) self.future_array_shapes = False def _set_cs_params(self): @@ -1027,11 +1071,25 @@ def check( check_extra=check_extra, run_check_acceptability=run_check_acceptability ) - # check that basis_vector_array are basis vectors + # check that basis_vector_array are not longer than 1 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 for deprecated values in feed_array + if self.beam_type == "efield": + warn_feed = [] + for feed in self.feed_array: + if feed in ["N", "E", "R", "L"]: + warn_feed.append(feed) + if len(warn_feed) > 0: + warnings.warn( + f"Feed array has values {warn_feed} that are deprecated. " + "Values in feed_array should be lower case. This will become " + "an error in version 2.6", + DeprecationWarning, + ) + # issue warning if extra_keywords keys are longer than 8 characters for key in list(self.extra_keywords.keys()): if len(key) > 8: @@ -1592,7 +1650,7 @@ def _interp_az_za_rect_spline( az_array, za_array, freq_array, - freq_interp_kind="linear", + freq_interp_kind="cubic", freq_interp_tol=1.0, polarizations=None, reuse_spline=False, @@ -1884,7 +1942,7 @@ def _interp_healpix_bilinear( az_array, za_array, freq_array, - freq_interp_kind="linear", + freq_interp_kind="cubic", freq_interp_tol=1.0, polarizations=None, reuse_spline=False, @@ -2093,6 +2151,7 @@ def interp( az_array=None, za_array=None, interpolation_function=None, + freq_interp_kind=None, az_za_grid=False, healpix_nside=None, healpix_inds=None, @@ -2136,6 +2195,14 @@ def interp( Specify the interpolation function to use. Defaults to: "az_za_simple" for objects with the "az_za" pixel_coordinate_system and "healpix_simple" for objects with the "healpix" pixel_coordinate_system. + freq_interp_kind : str + Interpolation method to use frequency. See scipy.interpolate.interp1d + for details. Defaults to the freq_interp_kind attribute on the object + if it is set (setting it is deprecated), otherwise it defaults to: + "cubic" (Note that this is a change. It used to default to "linear" + when it was assigned to the object. However, multiple groups have + found that a linear interpolation leads to nasty artifacts in + visibility simulations for EoR applications.) az_za_grid : bool Option to treat the `az_array` and `za_array` as the input vectors for points on a mesh grid. @@ -2220,8 +2287,24 @@ def interp( f"pixel_coordinate_system: {self.pixel_coordinate_system}" ) - if self.freq_interp_kind is None: - raise ValueError("freq_interp_kind must be set on object first") + with warnings.catch_warnings(): + warnings.filterwarnings( + "ignore", "The freq_interp_kind attribute on UVBeam objects" + ) + if freq_interp_kind is None and self.freq_interp_kind is None: + freq_interp_kind = "cubic" + elif freq_interp_kind is not None and self.freq_interp_kind is not None: + if freq_interp_kind != self.freq_interp_kind: + warnings.warn( + "The freq_interp_kind parameter was set but it does not " + "match the freq_interp_kind attribute on the object. " + "Using the one passed to this method." + ) + + if freq_interp_kind is not None: + kind_use = freq_interp_kind + else: + kind_use = self.freq_interp_kind if return_coupling is True and self.antenna_type != "phased_array": raise ValueError( @@ -2245,7 +2328,6 @@ def interp( interp_func_name = interpolation_function interp_func = self.interpolation_function_dict[interpolation_function]["func"] - 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)) @@ -2342,7 +2424,6 @@ def interp( else: new_uvb.freq_array = freq_array.reshape(1, -1) new_uvb.bandpass_array = interp_bandpass - new_uvb.freq_interp_kind = kind_use if self.antenna_type == "phased_array": new_uvb.coupling_matrix = interp_coupling_matrix @@ -2358,7 +2439,7 @@ def interp( warnings.warn( f"Input object has {param_name} defined but we do not " "currently support interpolating it in frequency. Returned " - "object will not have it set to None." + "object will have it set to None." ) setattr(new_uvb, param_name, None) @@ -2422,9 +2503,7 @@ def interp( f" using pyuvdata with interpolation_function = {interp_func_name}" ) if freq_array is not None: - history_update_string += ( - f" and freq_interp_kind = {new_uvb.freq_interp_kind}" - ) + history_update_string += f" and freq_interp_kind = {kind_use}" history_update_string += "." new_uvb.history = new_uvb.history + history_update_string new_uvb.data_array = interp_data diff --git a/pyuvdata/uvcal/initializers.py b/pyuvdata/uvcal/initializers.py index d70624ada..dac89e12e 100644 --- a/pyuvdata/uvcal/initializers.py +++ b/pyuvdata/uvcal/initializers.py @@ -1,3 +1,7 @@ +# -*- mode: python; coding: utf-8 -*- +# Copyright (c) 2024 Radio Astronomy Software Group +# Licensed under the 2-clause BSD License + """From-memory initializers for UVCal objects.""" from __future__ import annotations diff --git a/pyuvdata/uvcal/tests/test_initializers.py b/pyuvdata/uvcal/tests/test_initializers.py index 63dca789e..0d3335d57 100644 --- a/pyuvdata/uvcal/tests/test_initializers.py +++ b/pyuvdata/uvcal/tests/test_initializers.py @@ -1,3 +1,7 @@ +# -*- mode: python; coding: utf-8 -*- +# Copyright (c) 2024 Radio Astronomy Software Group +# Licensed under the 2-clause BSD License + import re import numpy as np diff --git a/pyuvdata/uvdata/initializers.py b/pyuvdata/uvdata/initializers.py index 68a3ce900..cfac343fe 100644 --- a/pyuvdata/uvdata/initializers.py +++ b/pyuvdata/uvdata/initializers.py @@ -1,3 +1,7 @@ +# -*- mode: python; coding: utf-8 -*- +# Copyright (c) 2024 Radio Astronomy Software Group +# Licensed under the 2-clause BSD License + """A module defining functions for initializing UVData objects from scratch.""" from __future__ import annotations diff --git a/pyuvdata/uvdata/tests/test_initializers.py b/pyuvdata/uvdata/tests/test_initializers.py index a2302e190..f671ab888 100644 --- a/pyuvdata/uvdata/tests/test_initializers.py +++ b/pyuvdata/uvdata/tests/test_initializers.py @@ -1,3 +1,7 @@ +# -*- mode: python; coding: utf-8 -*- +# Copyright (c) 2024 Radio Astronomy Software Group +# Licensed under the 2-clause BSD License + """Tests of in-memory initialization of UVData objects.""" from __future__ import annotations