Skip to content

Commit b7fb290

Browse files
Merge pull request #99 from HERA-Team/fix-analytic-beam
maint: update API to conform to new pyuvdata conventions.
2 parents 2994591 + 9717524 commit b7fb290

15 files changed

Lines changed: 159 additions & 170 deletions

docs/tutorials/matvis_tutorial.ipynb

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
},
4949
{
5050
"cell_type": "code",
51-
"execution_count": 12,
51+
"execution_count": null,
5252
"metadata": {},
5353
"outputs": [],
5454
"source": [
@@ -59,7 +59,7 @@
5959
"from astropy.time import Time\n",
6060
"\n",
6161
"from matvis import simulate_vis\n",
62-
"from pyuvsim.analyticbeam import AnalyticBeam"
62+
"from pyuvdata.analytic_beam import GaussianBeam, UniformBeam"
6363
]
6464
},
6565
{
@@ -107,17 +107,18 @@
107107
"source": [
108108
"Now, let's define the beams for each of these antennas. `matvis` allows us to specify\n",
109109
"different beams for each antenna, but we need only specify *unique* beams. Each beam\n",
110-
"must be either a `UVBeam` or `AnalyticBeam`. Here, for simplicity we just use the \n",
111-
"`AnalyticBeam` class. We do this as follows:"
110+
"must be either a `UVBeam` or `AnalyticBeam`. Here, for simplicity we just use some\n",
111+
"subclasses of the `AnalyticBeam` class. You can create your own custom `AnalyticBeam`\n",
112+
"classes to use as well (see the [pyuvdata tutorial](https://pyuvdata.readthedocs.io/en/latest/analytic_beam_tutorial.html#defining-new-analytic-beams))."
112113
]
113114
},
114115
{
115116
"cell_type": "code",
116-
"execution_count": 3,
117+
"execution_count": null,
117118
"metadata": {},
118119
"outputs": [],
119120
"source": [
120-
"beams = [AnalyticBeam(\"gaussian\", sigma=0.5), AnalyticBeam(\"uniform\")]\n",
121+
"beams = [GaussianBeam(sigma=0.5), UniformBeam()]\n",
121122
"beam_idx = [0, 0, 1]"
122123
]
123124
},
@@ -135,9 +136,7 @@
135136
"metadata": {},
136137
"source": [
137138
"We also need to tell `matvis` the observational configuration, such as the frequency\n",
138-
"channels and times to use. In this example, we don't worry too much about the exact date\n",
139-
"of observation, but rather the LST. In general, the exact date of observation does make\n",
140-
"a little difference."
139+
"channels and times to use:"
141140
]
142141
},
143142
{
@@ -422,7 +421,7 @@
422421
],
423422
"metadata": {
424423
"kernelspec": {
425-
"display_name": "Python 3 (ipykernel)",
424+
"display_name": ".venv",
426425
"language": "python",
427426
"name": "python3"
428427
},
@@ -436,12 +435,7 @@
436435
"name": "python",
437436
"nbconvert_exporter": "python",
438437
"pygments_lexer": "ipython3",
439-
"version": "3.12.5"
440-
},
441-
"vscode": {
442-
"interpreter": {
443-
"hash": "ad4f6c198aebbf84691339a9a95c2c9f143d88558d502a312c8b0f70f8363bfd"
444-
}
438+
"version": "3.12.6"
445439
}
446440
},
447441
"nbformat": 4,

setup.cfg

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,10 @@ install_requires =
2525
line-profiler
2626
numpy>=2.0
2727
psutil
28-
pyuvdata>=3.1.0
28+
pyuvdata>=3.1.2
2929
rich
3030
scipy
31-
python_requires = >=3.9
31+
python_requires = >=3.10
3232
include_package_data = True
3333
package_dir =
3434
=src
Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,17 @@
55
from astropy.coordinates import EarthLocation, Latitude, Longitude
66
from astropy.time import Time
77
from astropy.units import Quantity
8+
from dataclasses import replace
89
from pathlib import Path
910
from pyradiosky import SkyModel
1011
from pyuvdata import UVBeam
12+
from pyuvdata.analytic_beam import GaussianBeam
13+
from pyuvdata.beam_interface import BeamInterface
1114
from pyuvdata.telescopes import get_telescope
12-
from pyuvsim import AnalyticBeam, simsetup
15+
from pyuvsim import simsetup
1316
from pyuvsim.telescope import BeamList
1417

15-
from matvis import DATA_PATH, coordinates
18+
from matvis import DATA_PATH
1619

1720
nfreq = 1
1821
ntime = 5 # 20
@@ -39,34 +42,36 @@ def get_standard_sim_params(
3942
# Beam model
4043
if use_analytic_beam:
4144
n_freq = nfreq
42-
beam = AnalyticBeam("gaussian", diameter=14.0)
45+
beam = BeamInterface(
46+
GaussianBeam(diameter=14.0), beam_type="efield" if polarized else "power"
47+
)
4348
else:
4449
n_freq = min(nfreq, 2)
4550
# This is a peak-normalized e-field beam file at 100 and 101 MHz,
4651
# downsampled to roughly 4 square-degree resolution.
4752
beam = UVBeam()
4853
beam.read_beamfits(beam_file)
49-
if not polarized:
50-
uvsim_beam = beam.copy()
51-
beam.efield_to_power(calc_cross_pols=False, inplace=True)
52-
beam.select(polarizations=["xx"], inplace=True)
54+
# Even though we sometimes want a power beam for matvis, we always need
55+
# an efield beam for pyuvsim, so we create an efield beam here, and let matvis
56+
# take care of conversion later.
57+
beam = BeamInterface(beam, beam_type="efield")
5358

5459
# Now, the beam we have on file doesn't actually properly wrap around in azimuth.
5560
# This is fine -- the uvbeam.interp() handles the interpolation well. However, when
5661
# comparing to the GPU interpolation, which first has to interpolate to a regular
5762
# grid that ends right at 2pi, it's better to compare like for like, so we
5863
# interpolate to such a grid here.
59-
beam = beam.interp(
60-
az_array=np.linspace(0, 2 * np.pi, 181),
61-
za_array=np.linspace(0, np.pi / 2, 46),
62-
az_za_grid=True,
63-
new_object=True,
64+
beam = beam.clone(
65+
beam=beam.beam.interp(
66+
az_array=np.linspace(0, 2 * np.pi, 181),
67+
za_array=np.linspace(0, np.pi / 2, 46),
68+
az_za_grid=True,
69+
new_object=True,
70+
)
6471
)
6572

66-
if polarized or use_analytic_beam:
67-
uvsim_beams = BeamList([beam])
68-
else:
69-
uvsim_beams = BeamList([uvsim_beam])
73+
# The UVSim beams must be efield all the time.
74+
uvsim_beams = BeamList([beam])
7075

7176
beam_dict = {str(i): 0 for i in range(nants)}
7277

src/matvis/core/beams.py

Lines changed: 32 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -3,43 +3,41 @@
33
import numpy as np
44
from abc import ABC, abstractmethod
55
from copy import deepcopy
6+
from dataclasses import replace
67
from pyuvdata import UVBeam
8+
from pyuvdata.analytic_beam import AnalyticBeam
9+
from pyuvdata.beam_interface import BeamInterface
710
from pyuvdata.utils.pol import polstr2num
811
from typing import Any, Literal
912

1013

11-
def prepare_beam_unpolarized(uvbeam, use_pol: Literal["xy", "xx", "yx", "yy"] = "xx"):
12-
"""Given a UVBeam or AnalyticBeam, prepare it for an un-polarized simulation."""
13-
if uvbeam.beam_type == "power" and getattr(uvbeam, "Npols", 1) == 1:
14-
return uvbeam
14+
def prepare_beam_unpolarized(
15+
beam: BeamInterface,
16+
use_feed: Literal["x", "y"] = "x",
17+
allow_beam_mutation: bool = False,
18+
) -> BeamInterface:
19+
"""Given a BeamInterface, prepare it for an un-polarized simulation."""
20+
if beam.beam_type == "power" and beam.Npols == 1:
21+
return beam
1522

16-
uvbeam_ = uvbeam.copy() if isinstance(uvbeam, UVBeam) else deepcopy(uvbeam)
17-
18-
if uvbeam_.beam_type == "efield":
19-
if isinstance(uvbeam, UVBeam):
20-
uvbeam_.efield_to_power(calc_cross_pols=False)
21-
else:
22-
uvbeam_.efield_to_power()
23-
24-
if getattr(uvbeam_, "Npols", 1) > 1:
25-
pol = polstr2num(use_pol)
23+
if beam.beam_type == "efield":
24+
beam = beam.as_power_beam(
25+
include_cross_pols=False, allow_beam_mutation=allow_beam_mutation
26+
)
2627

27-
if pol not in uvbeam_.polarization_array:
28-
raise ValueError(
29-
f"You want to use {use_pol} pol, but it does not exist in the UVBeam"
30-
)
31-
uvbeam_.select(polarizations=[pol])
28+
if beam.Npols > 1:
29+
beam = beam.with_feeds([use_feed])
3230

33-
return uvbeam_
31+
return beam
3432

3533

3634
def _wrangle_beams(
3735
beam_idx: np.ndarray | None,
38-
beam_list: list[UVBeam],
36+
beam_list: list[BeamInterface | UVBeam | AnalyticBeam],
3937
polarized: bool,
4038
nant: int,
4139
freq: float,
42-
) -> tuple[list[UVBeam], int, np.ndarray]:
40+
) -> tuple[list[BeamInterface], int, np.ndarray]:
4341
"""Perform all the operations and checks on the input beams.
4442
4543
Checks that the beam indices match the number of antennas, pre-interpolates to the
@@ -61,6 +59,10 @@ def _wrangle_beams(
6159
"""
6260
# Get the number of unique beams
6361
nbeam = len(beam_list)
62+
beam_list = [BeamInterface(beam) for beam in beam_list]
63+
64+
if not polarized:
65+
beam_list = [prepare_beam_unpolarized(beam) for beam in beam_list]
6466

6567
# Check the beam indices
6668
if beam_idx is None and nbeam not in (1, nant):
@@ -78,8 +80,12 @@ def _wrangle_beams(
7880
# make sure we interpolate to the right frequency first.
7981
beam_list = [
8082
(
81-
bm.interp(freq_array=np.array([freq]), new_object=True, run_check=False)
82-
if isinstance(bm, UVBeam)
83+
bm.clone(
84+
beam=bm.beam.interp(
85+
freq_array=np.array([freq]), new_object=True, run_check=False
86+
)
87+
)
88+
if bm._isuvbeam
8389
else bm
8490
)
8591
for bm in beam_list
@@ -89,19 +95,6 @@ def _wrangle_beams(
8995
if any(b.beam_type != "efield" for b in beam_list):
9096
raise ValueError("beam type must be efield if using polarized=True")
9197

92-
# The following applies if we're not polarized
93-
elif any(
94-
(
95-
b.beam_type != "power"
96-
or getattr(b, "Npols", 1) > 1
97-
or b.polarization_array[0] not in [-5, -6]
98-
)
99-
for b in beam_list
100-
):
101-
raise ValueError(
102-
"beam type must be power and have only one pol (either xx or yy) if polarized=False"
103-
)
104-
10598
return beam_list, nbeam, beam_idx
10699

107100

@@ -132,7 +125,7 @@ class BeamInterpolator(ABC):
132125

133126
def __init__(
134127
self,
135-
beam_list: list[UVBeam],
128+
beam_list: list[BeamInterface],
136129
beam_idx: np.ndarray,
137130
polarized: bool,
138131
nant: int,
@@ -142,6 +135,7 @@ def __init__(
142135
precision: int = 1,
143136
):
144137
self.polarized = polarized
138+
145139
self.beam_list, self.nbeam, self.beam_idx = _wrangle_beams(
146140
beam_idx=beam_idx,
147141
beam_list=beam_list,

src/matvis/cpu/beams.py

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -24,24 +24,19 @@ def interp(self, tx: np.ndarray, ty: np.ndarray, out: np.ndarray) -> np.ndarray:
2424
# Primary beam pattern using direct interpolation of UVBeam object
2525
az, za = enu_to_az_za(enu_e=tx, enu_n=ty, orientation="uvbeam")
2626

27+
kw = {
28+
"reuse_spline": True,
29+
"check_azza_domain": False,
30+
"interpolation_function": "az_za_map_coordinates",
31+
"spline_opts": self.spline_opts,
32+
}
2733
for i, bm in enumerate(self.beam_list):
28-
kw = (
29-
{
30-
"reuse_spline": True,
31-
"check_azza_domain": False,
32-
"interpolation_function": "az_za_map_coordinates",
33-
"spline_opts": self.spline_opts,
34-
}
35-
if isinstance(bm, UVBeam)
36-
else {}
37-
)
38-
39-
interp_beam = bm.interp(
34+
interp_beam = bm.compute_response(
4035
az_array=az,
4136
za_array=za,
4237
freq_array=np.array([self.freq]),
4338
**kw,
44-
)[0]
39+
)
4540

4641
if self.polarized:
4742
interp_beam = interp_beam[:, :, 0, :].transpose((1, 0, 2))

src/matvis/cpu/cpu.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
from astropy.time import Time
1212
from collections.abc import Sequence
1313
from pyuvdata import UVBeam
14+
from pyuvdata.analytic_beam import AnalyticBeam
15+
from pyuvdata.beam_interface import BeamInterface
1416
from typing import Callable, Literal
1517

1618
from .._utils import get_desired_chunks, get_dtypes, log_progress, logdebug, memtrace
@@ -32,7 +34,7 @@ def simulate(
3234
skycoords: SkyCoord,
3335
telescope_loc: EarthLocation,
3436
I_sky: np.ndarray,
35-
beam_list: Sequence[UVBeam | Callable] | None,
37+
beam_list: Sequence[UVBeam | AnalyticBeam | BeamInterface] | None,
3638
antpairs: np.ndarray | list[tuple[int, int]] | None = None,
3739
precision: int = 1,
3840
polarized: bool = False,

src/matvis/gpu/beams.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from ..cpu.beams import UVBeamInterpolator
1212

1313

14-
def prepare_for_map_coords(uvbeam):
14+
def prepare_for_map_coords(uvbeam: UVBeam):
1515
"""Obtain coordinates for doing map_coordinates interpolation from a UVBeam."""
1616
d0, az, za = uvbeam._prepare_coordinate_data(uvbeam.data_array)
1717
d0 = d0[:, :, 0] # only one frequency
@@ -30,8 +30,8 @@ def setup(self):
3030
Decides if the beam_list is a list of UVBeam objects or AnalyticBeam objects,
3131
and dispatches accordingly.
3232
"""
33-
self.use_interp = isinstance(self.beam_list[0], UVBeam)
34-
if self.use_interp and not all(isinstance(b, UVBeam) for b in self.beam_list):
33+
self.use_interp = self.beam_list[0]._isuvbeam
34+
if self.use_interp and not all(b._isuvbeam for b in self.beam_list):
3535
raise ValueError(
3636
"GPUBeamInterpolator only supports beam_lists with either all UVBeam or all AnalyticBeam objects."
3737
)
@@ -48,15 +48,15 @@ def setup(self):
4848
# we might want to do cubic interpolation with pyuvbeam onto a much higher-res
4949
# grid, then use linear interpolation on the GPU with that high-res grid.
5050
# We can explore this later...
51-
if any(bm.pixel_coordinate_system != "az_za" for bm in self.beam_list):
51+
if any(bm.beam.pixel_coordinate_system != "az_za" for bm in self.beam_list):
5252
raise ValueError('pixel coordinate system must be "az_za"')
5353

5454
self.daz = np.zeros(len(self.beam_list))
5555
self.dza = np.zeros(len(self.beam_list))
5656
self.azmin = np.zeros(len(self.beam_list))
5757

5858
d0, self.daz[0], self.dza[0], self.azmin[0] = prepare_for_map_coords(
59-
self.beam_list[0]
59+
self.beam_list[0].beam
6060
)
6161

6262
self.beam_data = cp.zeros(
@@ -68,7 +68,7 @@ def setup(self):
6868
if len(self.beam_list) > 1:
6969
for i, b in enumerate(self.beam_list[1:]):
7070
d, self.daz[i + 1], self.dza[i + 1], self.azmin[i + 1] = (
71-
prepare_for_map_coords(b)
71+
prepare_for_map_coords(b.beam)
7272
)
7373
self.beam_data[i + 1].set(d)
7474
else:

0 commit comments

Comments
 (0)