Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pol convention #1455

Merged
merged 24 commits into from
Jul 23, 2024
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
d51a637
feat: add pol_convention parameter
steven-murray Jul 5, 2024
17dae1f
docs: add to changelog
steven-murray Jul 5, 2024
7d43ed6
fix: circular import
steven-murray Jul 5, 2024
0feb93a
fix: change behaviour of missin pol_convention
steven-murray Jul 8, 2024
99f56a5
style: add type-hints to uvcalibrate
steven-murray Jul 8, 2024
70dc32b
fix: add read/write of pol_convention to uvh5 and calh5
steven-murray Jul 8, 2024
b244201
test: add basic test of polarization conventions for uvd
steven-murray Jul 8, 2024
13b0063
fix bug that causes error on reading files with polarization conventi…
jsdillon Jul 9, 2024
67c6d53
remove warning for pol_convention in uvdata and uvcal
steven-murray Jul 11, 2024
8592e49
fix: add warning for missing gain_scale in uvcalibrate
steven-murray Jul 12, 2024
f2f8f1d
Fix tests
bhazelton Jul 12, 2024
ef627df
Add read/write handling for pol_convention across file types
bhazelton Jul 15, 2024
4450b4a
Fix min_deps tests
bhazelton Jul 15, 2024
161ca81
Fix tutorial
bhazelton Jul 15, 2024
c405532
Add warning for pol_convention set and gain_scale is not
bhazelton Jul 16, 2024
738cc16
Update the docstrings for pol_convention
bhazelton Jul 16, 2024
e8ade38
docs: update uvcal_tutorial
steven-murray Jul 17, 2024
631641f
docs: update docs to be more clear around pol_convention
steven-murray Jul 18, 2024
37ad4bd
test: add tests for uvcalibrate with pol_conventions
steven-murray Jul 19, 2024
e8610a6
test: ignore warnings in pol conventions tests
steven-murray Jul 19, 2024
14f0cca
test: add tests for circular pols
steven-murray Jul 21, 2024
760e327
fix: remove print statements
steven-murray Jul 22, 2024
9ca2714
ui: raise more informative error when uvd has stokes pols in uvcalibrate
steven-murray Jul 23, 2024
f66efe3
fix: typo in any check
steven-murray Jul 23, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ All notable changes to this project will be documented in this file.
## [Unreleased]

### Added
- New UVParameter `pol_convention` on `UVData` and `UVCal`. This specifies the convention
assumed for converting linear to stokes polarizations -- either "sum" or "avg". Also
added to `uvcalibrate` to apply from the `UVCal` to the `UVData`.
- New `ignore_telescope_param_update_warnings_for` function that globally ignores
warnings for specific telescopes.
- New `.telescope` cached property on the `FastUVH5Meta` object that auto-creates a
Expand Down
83 changes: 83 additions & 0 deletions docs/uvcal_tutorial.rst
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,23 @@ gives the beginning and end of the time range. The local sidereal times follow a
pattern, UVCal objects should have either an ``lst_array`` or an ``lst_range`` attribute
set.

Generating calibration solutions typically requires choosing a convention concerning how
polarized sky emission is mapped to the linear polarizations of the instrument. For
bhazelton marked this conversation as resolved.
Show resolved Hide resolved
linear polarizations ``XX`` and ``YY``, the stokes ``I`` sky emission can be mapped to
``I = (XX + YY)/2`` (the ``avg`` convention) or ``I = XX + YY`` (the ``sum``
convention). This choice is generally encoded in the sky model to which the visibilities
are calibrated. Different tools and simulators make different choices, generally following
a standard choice for the field. In ``pyuvdata`` either of these choices are OK, but the
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Different tools and simulators make different choices, generally following a standard choice for the field.

I think we need to be more explicit here about which tools do what, especially if we are going to recommend that users always specify this parameter. In particular, I believe the tasks in CASA (e.g., tclean) and MIRIAD, along with WSClean, all assume the avg convention. I actually am not sure of what uses the sum convention, though I expect we can gather that information.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not very familiar with such tools, but I'll add that particular information.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FHD uses the sum convention and the various HERA packages are moving to it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool. I added that as well.

choice should be recorded as the ``pol_convention`` parameter in both ``UVCal`` and
``UVData`` objects. Since the ``pol_convention`` has always (at least implicitly) been
chosen for calibration solutions, we suggest *always* specifying this parameter on the
``UVCal`` object (though we do not enforce this, for backwards compatibility reasons).
Only *calibrated* ``UVData`` objects make sense to have the ``pol_convention`` specified.
To learn more about this parameter and how ``pyuvdata`` deals with it, please see the
section below `UVCal: Calibrating UVData`_.



For most users, the convenience methods for quick data access (see
`UVCal: Quick data access`_) are the easiest way to get data for particular antennas.
Those methods take the antenna numbers (i.e. numbers listed in ``telescope.antenna_numbers``)
Expand Down Expand Up @@ -260,13 +277,72 @@ UVCal: Calibrating UVData
Calibration solutions in a :class:`pyuvdata.UVCal` object can be applied to a
:class:`pyuvdata.UVData` object using the :func:`pyuvdata.utils.uvcalibrate` function.

Generating calibration solutions typically requires choosing a convention concerning how
polarized sky emission is mapped to the linear polarizations of the instrument. For
bhazelton marked this conversation as resolved.
Show resolved Hide resolved
linear polarizations ``XX`` and ``YY``, the stokes ``I`` sky emission can be mapped to
``I = (XX + YY)/2`` (the ``avg`` convention) or ``I = XX + YY`` (the ``sum``
convention). This choice is generally encoded in the sky model to which the visibilities
are calibrated. Different tools and simulators make different choices, generally following
a standard choice for the field. When calibrating a ``UVData`` object with a ``UVCal``
object using :func:`pyuvdata.utils.uvcalibrate`, it is *required* to specify this
convention. At this time, the convention can be specified either on the ``UVCal`` object
itself, or as a parameter to :func:`pyuvdata.utils.uvcalibrate`. The chosen ``pol_convention``
will then be applied to and stored on the resulting ``UVData`` object.

There are a few non-trivial combinations of parameters concerning the ``pol_convention``
that should be noted:
bhazelton marked this conversation as resolved.
Show resolved Hide resolved

* There are two parameters to :func:`pyuvdata.utils.uvcalibrate` that specify how
the convention should be handled: ``uvd_pol_convention`` and ``uvc_pol_convention``,
and these act differently depending on whether ``undo`` is True or False. The
``uvc_pol_convention`` is only ever meant to specify what convention the ``UVCal``
object actually uses, and is therefore unnecessary if ``UVCal.pol_convention`` is
specified (regardless of whether calibrating or uncalibrating). On the other hand,
the ``uvd_pol_convention`` specifies the *desired* convention on the resulting
``UVData`` object if calibrating, and otherwise specifies the actual convention on
the ``UVData`` object (if uncalibrating, and this convention is not already specified
on the object itself).
* Regardless of the value of ``undo``, the convention that is inferred for the
calibration solutions is determined as follows:
* If neither ``uvc_pol_convention`` nor ``uvcal.pol_convention`` are specified, a
a warning is raised (since the resulting calibrated data is not well-determined),
and it is *assumed* that the solutions have the same convention as the ``UVData``
(i.e. the desired convention in the case of calibration, or the actual convention
in the case of uncalibration). If these are also not specified, no convention
corrections are applied, and the result is ambiguous.
* If both ``uvc_pol_convention`` and ``uvcal.pol_convention`` are specified and are
different, an error is raised.
* When **calibrating** in :func:`pyuvdata.utils.uvcalibrate` (i.e. ``undo=False``):
* If ``uvdata.pol_convention`` is specified, an error is raised, because you are
trying to calibrate already-calibrated data.
* The convention applied to the resulting ``UVData`` object is inferred in the
following precedence: (i) the value of ``uvd_pol_convention``, (ii) whatever is
specified as the convention of the ``UVCal`` object (either via ``uvc_pol_convention``
or ``uvcal.pol_convention``, see above), (iii) if still unspecified, no convention
will be used and a warning will be raised. This was always the behaviour in earlier
versions of ``pyuvdata`` (pre-v3).
* When **un-calibrating** with :func:`pyuvdata.utils.uvcalibrate` (i.e. ``undo=True``):
* If both ``uvd_pol_convention`` and ``uvdata.pol_convention`` are defined and
are different, an error is raised.
* If neither are set, a warning is raised, since the resulting un-calibrated values
may not be the same as the original values before calibration (since a different
convention could have been used to calibrate originally than is being used to
de-calibrate). However, calibration will continue, assuming that the ``UVData``
object has the same convention as the ``UVCal`` object used to de-calibrate.
* It is not supported to have ``pol_convention`` set on ``UVCal``, but *not*
``gain_scale``. A ``pol_convention`` only makes sense in the context of having a
scale for the gains.
* Mis-matching ``uvd_pol_convention`` and ``uvc_pol_convention`` is perfectly fine: any
necessary corrections in the calibration will be made to obtain the correct desired
convention.

a) Calibration of UVData by UVCal
*********************************
.. code-block:: python

>>> # We can calibrate directly using a UVCal object
>>> import os
>>> import numpy as np
>>> from pyuvdata import UVData, UVCal, utils
>>> from pyuvdata.data import DATA_PATH
>>> uvd = UVData.from_file(
Expand All @@ -281,7 +357,14 @@ a) Calibration of UVData by UVCal
>>> uvc.telescope.antenna_names = np.array(
... [name.replace("ant", "HH") for name in uvc.telescope.antenna_names]
... )
>>> # We should also set the gain_scale and pol_convention, which was not set
>>> # in this old file. In old HERA files, like this one, the pol_convention
>>> # was implicitly "avg" but in new files it is explicitly "sum"
>>> uvc.gain_scale = "Jy"
>>> uvc.pol_convention = "avg"
>>> uvd_calibrated = utils.uvcalibrate(uvd, uvc, inplace=False)
>>> print(uvd_calibrated.pol_convention)
avg

>>> # We can also un-calibrate using the same UVCal
>>> uvd_uncalibrated = utils.uvcalibrate(uvd_calibrated, uvc, inplace=False, undo=True)
Expand Down
152 changes: 144 additions & 8 deletions src/pyuvdata/utils/uvcalibrate.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,107 @@
# Licensed under the 2-clause BSD License
"""Code to apply calibration solutions to visibility data."""
import warnings
from typing import Literal

import numpy as np

from .pol import POL_TO_FEED_DICT, jnum2str, parse_jpolstr, polnum2str, polstr2num


def _get_pol_conventions(
uvdata,
uvcal,
undo: bool,
uvc_pol_convention: Literal["sum", "avg"] | None,
uvd_pol_convention: Literal["sum", "avg"] | None,
):
if uvc_pol_convention is None and uvcal.pol_convention is None:
warnings.warn(
message=(
"pol_convention is not specified on the UVCal object, and "
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know that I'd want this to error when the pol convention is not specified on either UVCal and UVData, at least not if we're treating pol_convention as an optional parameter. Why can't this just stay a warning?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @kartographer, I'm not following -- this is a warning?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@steven-murray -- sorry, I mean a general UserWarning, not a DeprecationWarning. I.e., this should alert the user that there's a potential for mismatch, but I don't think this should throw an error.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I updated all the DeprecationWarning's to UserWarnings. Originally I had intended these to be DeprecationWarnings because we want to push people to use the keywords. However, it would be really annoying to have that warning if you really just don't care about the final units (like, you're just using calibration solutions to multiply your data by 2...)

"uvc_pol_convention was not specified. This is deprecated, and will"
" be an error in pyuvdata 3.2, since it is impossible to properly "
"infer. For now, we are tentatively assuming "
"that the UVCal and UVData objects (implicitly) have the same "
"convention."
),
category=DeprecationWarning,
stacklevel=2,
)
uvc_pol_convention = uvd_pol_convention or uvdata.pol_convention
elif uvc_pol_convention is None:
uvc_pol_convention = uvcal.pol_convention
elif uvc_pol_convention != uvcal.pol_convention:
raise ValueError(
"uvc_pol_convention is set, and different than uvcal.pol_convention."
)

if undo:
if uvd_pol_convention is None and uvdata.pol_convention is None:
warnings.warn(
message=(
"pol_convention is not specified on the UVData object, and "
"uvd_pol_convention was not specified. This is deprecated, and will"
" be an error in pyuvdata 3.2, since it is impossible to know how "
"to properly un-calibrate. For now, we are tentatively assuming "
"that the UVCal and UVData objects (implicitly) have the same "
"convention."
),
category=DeprecationWarning,
stacklevel=2,
)
uvd_pol_convention = uvc_pol_convention
elif uvd_pol_convention is None:
uvd_pol_convention = uvdata.pol_convention
elif uvd_pol_convention != uvdata.pol_convention:
raise ValueError(
"Both uvd_pol_convention and uvdata.pol_convention were specified with"
f"different values: {uvd_pol_convention} and {uvdata.pol_convention}."
)
else:
if uvdata.pol_convention is not None:
raise ValueError("You are trying to calibrate already-calibrated data.")
if uvd_pol_convention is None:
uvd_pol_convention = uvc_pol_convention

if uvd_pol_convention is None:
# Both uvc and uvd have no pol_convention specified
warnings.warn(
message=(
"Neither uvd_pol_convention nor uvc_pol_convention are specified, "
"so the resulting UVData object will have ambiguous convention. "
"This is deprecated and will be an error in pyuvdata v3.2."
),
category=DeprecationWarning,
stacklevel=2,
)

if uvd_pol_convention not in ["sum", "avg", None]:
raise ValueError(
f"uvd_pol_convention must be 'sum' or 'avg'. Got {uvd_pol_convention}"
)
if uvc_pol_convention not in ["sum", "avg", None]:
raise ValueError(
f"uvc_pol_convention must be 'sum' or 'avg'. Got {uvc_pol_convention}"
)

return uvc_pol_convention, uvd_pol_convention


def uvcalibrate(
uvdata,
uvcal,
*,
inplace=True,
prop_flags=True,
d_term_cal=False,
flip_gain_conj=False,
delay_convention="minus",
undo=False,
time_check=True,
ant_check=True,
inplace: bool = True,
prop_flags: bool = True,
d_term_cal: bool = False,
flip_gain_conj: bool = False,
delay_convention: Literal["minus", "plus"] = "minus",
undo: bool = False,
time_check: bool = True,
ant_check: bool = True,
uvc_pol_convention: Literal["sum", "avg"] | None = None,
uvd_pol_convention: Literal["sum", "avg"] | None = None,
bhazelton marked this conversation as resolved.
Show resolved Hide resolved
):
"""
Calibrate a UVData object with a UVCal object.
Expand Down Expand Up @@ -61,6 +144,16 @@ def uvcalibrate(
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.
pol_convention : str, {"sum", "avg"}, optional
The convention for how instrumental polarizations (e.g. XX and YY) are
converted to Stokes parameters. Options are 'sum' and 'avg', corresponding
to I=XX+YY and I=(XX+YY)/2 (for linear instrumental polarizations)
respectively. If None, the convention is determined from the UVCal object.
If `pol_convention` is not specified and is not set on the UVCal object,
a deprecation warning is raised (will be an error in the future). If the
convention specified here is different from that in the UVCal object,
the one specified here will take precedence, and the necessary conversions
will be made.

Returns
-------
Expand All @@ -79,6 +172,25 @@ def uvcalibrate(
"calibrations"
)

if uvcal.gain_scale is None:
warnings.warn(
"gain_scale is not set, so there is no way to know what the resulting units"
" are. For now, we assume that `gain_scale` matches whatever is on the "
"UVData object (i.e. we do not change its units). Furthermore, all "
"corrections concerning the pol_convention will be ignored.",
category=UserWarning,
stacklevel=2,
)
elif undo and uvcal.gain_scale != uvdata.vis_units:
raise ValueError(
"Cannot undo calibration if gain_scale is not the same as the units on "
"the UVData object."
)

uvc_pol_convention, uvd_pol_convention = _get_pol_conventions(
uvdata, uvcal, undo, uvc_pol_convention, uvd_pol_convention
)

if not inplace:
uvdata = uvdata.copy()

Expand Down Expand Up @@ -417,5 +529,29 @@ def uvcalibrate(
if uvcal_use.gain_scale is not None:
uvdata.vis_units = uvcal_use.gain_scale

# Set pol convention properly
if uvcal.gain_scale is not None:
_apply_pol_convention_corrections(
uvdata, undo, uvd_pol_convention, uvc_pol_convention
)

if not inplace:
return uvdata


def _apply_pol_convention_corrections(
uvdata, undo, uvd_pol_convention, uvc_pol_convention
):
if uvd_pol_convention != uvc_pol_convention:
correction = np.ones(uvdata.Npols)
correction[uvdata.polarization_array > 0] *= 2
correction[uvdata.polarization_array < 0] /= 2

if (undo and uvd_pol_convention == "sum") or (
not undo and uvd_pol_convention == "avg"
):
correction = 1 / correction

uvdata.data_array *= correction

uvdata.pol_convention = None if undo else uvd_pol_convention
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is pretty hard to parse and be confident in, but I think your test shows that it's working properly, at least for linear pols. I don't see a test on Stokes visibilities (i.e. when uvdata.polarization_array > 0). Is there a test for that case?

Also, please move this to above uvcalibrate so it's defined before it's called.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK cool. I moved the function, wrote a docstring for it to hopefully make the logic more clear, and also added tests for circular polarizations. However, in doing this I found that it seems that it is not possible to calibrate/decalibrate uvdata objects with stokes polarizations (it complains that the polarizations can't be found on the UVCal object).

Is this to be expected?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't really know about a case where stokes calibration solutions make sense, but I feel like @kartographer ran into it when he was implementing MS cal read/write?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this is definitely a corner-case (that could be wrapped with a "pyuvdata does not support... if you file an issue..."), but the most common case I ran into was visibilities recorded as (pseudo-)Stokes I, which can have gains tables corresponding also to (pseudo-)Stokes I. Looking at the UVCal class though, we don't currently allow for Jones > 0, which is maybe what's causing the issue here.

This got raised in the MS read/write mostly on the visibility side, since the MS format has a FEED table that I had to match to the data, hence why those pseudo Stokes values are supported there.

5 changes: 5 additions & 0 deletions src/pyuvdata/uvcal/calfits.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,8 @@ def write_calfits(
prihdr["DIFFUSE"] = self.diffuse_model
if self.gain_scale is not None:
prihdr["GNSCALE"] = self.gain_scale
if self.pol_convention is not None:
prihdr["POLCONV"] = self.pol_convention
if self.Ntimes > 1:
prihdr["INTTIME"] = median_int_time
else:
Expand Down Expand Up @@ -599,13 +601,16 @@ def read_calfits(

self.gain_convention = hdr.pop("GNCONVEN")
self.gain_scale = hdr.pop("GNSCALE", None)
self.pol_convention = hdr.pop("POLCONV", None)
self.cal_type = hdr.pop("CALTYPE")

# old files might have a freq range for gain types but we don't want them
if self.cal_type == "delay":
self.freq_range = np.array(
[list(map(float, hdr.pop("FRQRANGE").split(",")))]
)
else:
hdr.pop("FRQRANGE", None)

self.cal_style = hdr.pop("CALSTYLE")
if self.cal_style == "sky":
Expand Down
5 changes: 5 additions & 0 deletions src/pyuvdata/uvcal/calh5.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ class FastCalH5Meta(hdf5_utils.HDF5Meta):
"sky_catalog",
"instrument",
"version",
"pol_convention",
}
)

Expand Down Expand Up @@ -253,6 +254,7 @@ def _read_header(
"ref_antenna_array",
"scan_number_array",
"sky_catalog",
"pol_convention",
]

for attr in required_parameters:
Expand Down Expand Up @@ -773,6 +775,9 @@ def _write_header(self, header):
header["phase_center_id_array"] = self.phase_center_id_array
if self.Nphase is not None:
header["Nphase"] = self.Nphase
if self.pol_convention is not None:
header["pol_convention"] = np.bytes_(self.pol_convention)

if self.phase_center_catalog is not None:
pc_group = header.create_group("phase_center_catalog")
for pc, pc_dict in self.phase_center_catalog.items():
Expand Down
2 changes: 2 additions & 0 deletions src/pyuvdata/uvcal/fhd_cal.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,8 @@ def read_fhd_cal(

self._set_sky()
self.gain_convention = "divide"
self.gain_scale = "Jy"
self.pol_convetions = "sum"
self._set_gain()

# currently don't have branch info. may change in future.
Expand Down
3 changes: 3 additions & 0 deletions src/pyuvdata/uvcal/ms_cal.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@ def read_ms_cal(

self.sky_catalog = main_keywords.get("pyuvdata_sky_catalog", None)
self.gain_scale = main_keywords.get("pyuvdata_gain_scale", None)
self.pol_convention = main_keywords.get("pyuvdata_polconv", None)
self.observer = main_keywords.get("pyuvdata_observer", None)
if "pyuvdata_cal_style" in main_keywords:
self.cal_style = main_keywords["pyuvdata_cal_style"]
Expand Down Expand Up @@ -491,6 +492,8 @@ def write_ms_cal(self, filename, clobber=False):

if self.gain_scale is not None:
ms.putkeyword("pyuvdata_gain_scale", self.gain_scale)
if self.pol_convention is not None:
ms.putkeyword("pyuvdata_polconv", self.pol_convention)

if self.observer is not None:
ms.putkeyword("pyuvdata_observer", self.observer)
Expand Down
Loading
Loading