Skip to content

Commit

Permalink
Removing and deprecating x-orientation attribute in Telescope
Browse files Browse the repository at this point in the history
  • Loading branch information
kartographer committed Feb 7, 2025
1 parent b47fbfe commit ca78d58
Show file tree
Hide file tree
Showing 34 changed files with 982 additions and 240 deletions.
263 changes: 234 additions & 29 deletions src/pyuvdata/telescopes.py

Large diffs are not rendered by default.

9 changes: 7 additions & 2 deletions src/pyuvdata/utils/antenna.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
"alt-az+nasmyth-r": 4,
"alt-az+nasmyth-l": 5,
"phased": 6,
"fixed": 7,
"other": 8,
"bizarre": 8, # Semi-common code in UVFITS/CASA for "unknown" types
}

MOUNT_NUM2STR_DICT = {
Expand All @@ -22,8 +25,10 @@
2: "orbiting",
3: "x-y",
4: "alt-az+nasmyth-r",
5: "alt-az+nasmyth-l",
6: "phased",
5: "alt-az+nasmyth-l", # here and above, UVFITS-defined
6: "phased", # <- pyuvdata defined, but not uncommon in UVFITS
7: "fixed", # <- pyuvdata defined
8: "other", # <- pyuvdata defined
}


Expand Down
7 changes: 2 additions & 5 deletions src/pyuvdata/utils/bls.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,11 +182,8 @@ def parse_ants(uv, ant_str, *, print_toggle=False, x_orientation=None):
f"to call 'parse_ants': {required_attrs}."
)

if x_orientation is None and (
hasattr(uv.telescope, "x_orientation")
and uv.telescope.x_orientation is not None
):
x_orientation = uv.telescope.x_orientation
if x_orientation is None:
x_orientation = uv.telescope.get_x_orientation_from_feeds()

ant_re = r"(\(((-?\d+[lrxy]?,?)+)\)|-?\d+[lrxy]?)"
bl_re = f"(^({ant_re}_{ant_re}|{ant_re}),?)"
Expand Down
35 changes: 32 additions & 3 deletions src/pyuvdata/utils/io/hdf5.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import contextlib

from ..coordinates import ENU_from_ECEF, LatLonAlt_from_XYZ
from ..pol import get_x_orientation_from_feeds

hdf5plugin_present = True
try:
Expand Down Expand Up @@ -689,9 +690,37 @@ def mount_type(self) -> list[str]:
@cached_property
def feed_array(self) -> np.ndarray[str]:
"""The antenna names in the file."""
return np.char.decode(
self.header["feed_array"][:].astype("|S"), encoding="utf8"
).astype(np.object_)
if "feed_array" in self.header:
return np.char.decode(
self.header["feed_array"][:].astype("|S"), encoding="utf8"
)

return None

@cached_property
def feed_angle(self) -> np.ndarray[float]:
"""The antenna names in the file."""
if "feed_angle" in self.header:
return self.header["feed_angle"][()]

return None

@cached_property
def x_orientation(self) -> str:
"""The x_orientation in the file."""
if "feed_array" in self.header and "feed_angle" in self.header:
# If these are present, then x-orientation can be "calculated" based
# on the feed orientations. Note that this will return None if what is
# listed doesn't match either "east" or "north" configuration
return get_x_orientation_from_feeds(
feed_array=self.feed_array, feed_angle=self.feed_angle, tols=(1e-6, 0)
)
if "x_orientation" in self.header:
# If the header is present, then just load that up.
return bytes(self.header["x_orientation"][()]).decode("utf8")

# Otherwise, no x_orientation is present, so just return None
return None

@cached_property
def extra_keywords(self) -> dict:
Expand Down
8 changes: 3 additions & 5 deletions src/pyuvdata/utils/io/ms.py
Original file line number Diff line number Diff line change
Expand Up @@ -1475,13 +1475,11 @@ def write_ms_feed(
has_feed = True
else:
if uvobj.flex_spw_polarization_array is None:
pols = utils.polnum2str(uvobj.polarization_array)
pols = uvobj.polarization_array
else:
pols = utils.polnum2str(uvobj.flex_spw_polarization_array)
pols = uvobj.flex_spw_polarization_array

feed_pols = {
feed for pol in pols for feed in utils.pol.POL_TO_FEED_DICT[pol]
}
feed_pols = utils.pol.get_feeds_from_pols(pols)
nfeeds = len(feed_pols)
feed_array = np.tile(sorted(feed_pols), (uvobj.telescope.Nants, 1))
feed_angle = np.zeros((uvobj.telescope.Nants, nfeeds))
Expand Down
180 changes: 180 additions & 0 deletions src/pyuvdata/utils/pol.py
Original file line number Diff line number Diff line change
Expand Up @@ -812,3 +812,183 @@ def _check_jones_spacing(*, jones_array, strict=True, allow_resort=False):
"make it impossible to write this data out to calfits."
)
tools._strict_raise(err_msg=err_msg, strict=strict)


def get_feeds_from_pols(polarization_array):
"""
Return a list of expected feeds based on polarization values.
Translates values in polarization_array or jones_array into
Parameters
----------
polarization_array : array_like of int
Array listing the polarization codes present, based on the UVFITS numbering
schedule. See utils.POL_NUM2STR_DICT for a mapping between codes and
polarization types.
Returns
-------
feed_array : list of str
List of expected feed types given the polarizations present in the data. Will
be one of "x", "y", "l", "r", and generally of length <= 2.
"""
# Preserve order of feeds based on pols using dict.fromkeys
feed_pols = list(
dict.fromkeys(
feed
for pol in polarization_array
for feed in POL_TO_FEED_DICT[POL_NUM2STR_DICT[pol]]
)
)
return sorted(feed_pols)


def get_x_orientation_from_feeds(feed_array, feed_angle, tols=None):
"""
Determine x-orientation equivalent value based on feed information.
This is a helper function meant to provide a way of translating newer parameters
(feed_array and feed_angle) describing feed orientation with the older
"x-orientation" parameter.
Parameters
----------
feed_array : array-like of str or None
List of feeds for a given telescope, should be one of "x", "y", "l", "r".
Shape (Nants, Nfeeds) or (Nfeeds,), must match that of feed_angle, dtype str.
feed_angle : array-like of float
Orientation of the feed with respect to zenith (or with respect to north if
pointed at zenith). Units is in rads, vertical polarization is nominally 0,
and horizontal polarization is nominally pi / 2. Shape (Nants, Nfeeds) or
(Nfeeds,), must match that of feed_array, dtype float.
tols : tuple of float
Tolerances for feed_angle, used with `isclose`.
Returns
-------
x_orientation : str
One of "east", "north", or None, based on values present in feed_array and
feed_angle. None denotes that either one (or both) of feed_array and feed_angle
were None, or that the values were inconsistent with either "north" or "east"
orientation.
"""
if feed_array is None or feed_angle is None:
# If feed info is unset, then return None
return None

if tols is None:
tols = (0, 0)
rtol, atol = tols

x_mask = np.isin(feed_array, ["x", "X"])

# Anything that's not 'x' should be oriented straight up (0 deg) for "east"
# orientation, otherwise at -90 deg for "north".
if np.allclose(feed_angle, np.where(x_mask, np.pi / 2, 0), rtol=rtol, atol=atol):
# x is horizontal
return "east"
if np.allclose(feed_angle, np.where(x_mask, 0, -np.pi / 2), rtol=rtol, atol=atol):
# x is vertical for observer ("east is up"!)
return "north"

# No match? Then time to declare defeat.
return None


def get_feeds_from_x_orientation(
*,
x_orientation,
nants,
feeds=None,
polarization_array=None,
flex_polarization_array=None,
):
"""
Determine feed angles based on equivalent x-orientation.
This is a helper function meant to provide a way of translating the older
"x-orientation" parameter into the newer parameters describing feed orientation
(feed_array and feed_angle).
Parameters
----------
x_orientation : str
String describing the orientation of the x-polarization. Must be one of "east"
or "north" (or the associated aliases "n", "e", "ns", "ew")
nants : int
Number of antennas, used to determine the shape of the output.
feeds : str or array-like of str
List of feeds expected for the telescope. Must be one of "x", "y", "l", or "r".
A single feed type can be provided as a string, otherwise the list should
contain no more than two elements. If not provided (and polarization_array is
also not provided), default is ["x", "y"].
polarization_array : array-like of int or None
Array listing the polarization codes present, based on the UVFITS numbering
scheme. See utils.POL_NUM2STR_DICT for a mapping between codes and
polarization types. Used with `utils.pol.get_feeds_from_pols` to determine
feeds present if not supplied, ignored if flex_polarization_array is set
to anything but None.
flex_polarization_array : array-like of int or None
Array listing the polarization codes present per spectral window (used with
certain "flexible-polarization" objects), based on the UVFITS numbering
scheme. See utils.POL_NUM2STR_DICT for a mapping between codes and
polarization types. Used with `utils.pol.get_feeds_from_pols` to determine
feeds present if not supplied.
Returns
-------
Nfeeds : int
Length of feeds (or None of x_orientation is None).
feed_array : array-like of str or None
List of feeds, given on a per-antenna basis. Each feed will be listed as one of
"x", "y", "l", "r". Shape (Nants, Nfeeds), dtype str (or None of x_orientation
is None).
feed_angle : array-like of float
Orientation of the feed with respect to zenith (or with respect to north if
pointed at zenith). Units is in rads, vertical polarization is nominally 0,
and horizontal polarization is nominally pi / 2. Shape (Nants, Nfeeds) (or None
of x_orientation is None).
"""
if x_orientation is None:
# If x_orientation is None, then there isn't anything to determine
return None, None, None

if feeds is None:
if flex_polarization_array is not None:
feeds = get_feeds_from_pols(polarization_array=flex_polarization_array)
elif polarization_array is not None:
feeds = get_feeds_from_pols(polarization_array=polarization_array)
else:
warnings.warn(
"Unknown polarization basis -- assuming linearly polarized (x/y) "
"feeds for Telescope.feed_array."
)
feeds = ["x", "y"]
elif isinstance(feeds, str):
feeds = [feeds]

if x_orientation.lower() not in XORIENTMAP:
raise ValueError(
f"x_orientation not recognized, must be one of {list(XORIENTMAP)}."
)

x_orientation = XORIENTMAP[x_orientation.lower()]

# Check to make sure inputs here are valid
if not isinstance(feeds, list | tuple | np.ndarray) or len(feeds) not in [1, 2]:
raise ValueError("feeds must be a list or tuple of length 1 or 2.")
if not all(item in ["l", "r", "x", "y"] for item in feeds):
raise ValueError('feeds must contain only "x", "y", "l", and/or "r".')

Nfeeds = len(feeds)
feed_array = np.tile(feeds, (nants, 1))
feed_angle = np.zeros((nants, Nfeeds), dtype=float)

x_mask = feed_array == "x"
if x_orientation == "east":
feed_angle[x_mask] = np.pi / 2
if x_orientation == "north":
feed_angle[~x_mask] = -np.pi / 2

return Nfeeds, feed_array, feed_angle
12 changes: 7 additions & 5 deletions src/pyuvdata/utils/uvcalibrate.py
Original file line number Diff line number Diff line change
Expand Up @@ -440,26 +440,28 @@ def uvcalibrate(
if len(uvcal_freqs_to_keep) < uvcal.Nfreqs:
downselect_cal_freq = True

# check if uvdata.telescope.x_orientation isn't set (it's required for uvcal)
uvd_x = uvdata.telescope.x_orientation
# check if x_orientation-equivalent in uvdata isn't set (it's required for uvcal)
uvd_x = uvdata.telescope.get_x_orientation_from_feeds()
if uvd_x is None:
# use the uvcal x_orientation throughout
uvd_x = uvcal.telescope.x_orientation
uvd_x = uvcal.telescope.get_x_orientation_from_feeds()
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.telescope.x_orientation
uvcal.jones_array, x_orientation=uvcal.telescope.get_x_orientation_from_feeds()
)
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.telescope.x_orientation)
jones_str = parse_jpolstr(
feed, x_orientation=uvcal.telescope.get_x_orientation_from_feeds()
)
if jones_str not in uvcal_pol_strs:
raise ValueError(
f"Feed polarization {feed} exists on UVData but not on UVCal. "
Expand Down
Loading

0 comments on commit ca78d58

Please sign in to comment.