Skip to content

Commit ff500c2

Browse files
committed
add testing, fix bugs in analytic beam implementation, uvbeam.new
1 parent e720c48 commit ff500c2

File tree

8 files changed

+192
-93
lines changed

8 files changed

+192
-93
lines changed

src/pyuvdata/analytic_beam.py

Lines changed: 58 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -388,7 +388,11 @@ def _check_eval_inputs(
388388
raise ValueError("az_array and za_array must have the same shape.")
389389

390390
def _get_empty_data_array(
391-
self, grid_shape: tuple[int, int], beam_type: str = "efield"
391+
self,
392+
grid_shape: tuple[int, int],
393+
beam_type: Literal[
394+
"efield", "power", "feed_iresponse", "feed_projection"
395+
] = "efield",
392396
) -> FloatArray:
393397
"""Get the empty data to fill in the eval methods."""
394398
if beam_type in ["efield", "feed_projection"]:
@@ -447,9 +451,9 @@ def efield_eval(
447451

448452
data_array = self._get_empty_data_array(az_grid.shape)
449453

450-
for fn in np.arange(self.Nfeeds):
451-
data_array[0, fn, :, :] = np.sqrt(power_vals[fn] / 2.0)
452-
data_array[1, fn, :, :] = np.sqrt(power_vals[fn] / 2.0)
454+
for feed_i in np.arange(self.Nfeeds):
455+
data_array[0, feed_i, :, :] = np.sqrt(power_vals[feed_i] / 2.0)
456+
data_array[1, feed_i, :, :] = np.sqrt(power_vals[feed_i] / 2.0)
453457

454458
return data_array
455459

@@ -540,23 +544,35 @@ def feed_iresponse_eval(
540544
az_array=az_array, za_array=za_array, freq_array=freq_array
541545
)
542546

543-
za_grid, _ = np.meshgrid(za_array, freq_array)
544-
az_grid, f_grid = np.meshgrid(az_array, freq_array)
545-
546547
if hasattr(self, "_feed_iresponse_eval"):
548+
za_grid, _ = np.meshgrid(za_array, freq_array)
549+
az_grid, f_grid = np.meshgrid(az_array, freq_array)
550+
547551
return self._feed_iresponse_eval(
548552
az_grid=az_grid, za_grid=za_grid, f_grid=f_grid
549553
).astype(complex)
550554
else:
551-
efield_vals = self._efield_eval(
552-
az_grid=az_grid, za_grid=za_grid, f_grid=f_grid
555+
efield_vals = self.efield_eval(
556+
za_array=za_array, az_array=az_array, freq_array=freq_array
553557
)
554558

555-
# set f to the magnitude of the I response, assume no time delays
556-
data_array = np.sqrt(np.sum(np.abs(efield_vals) ** 2, axis=0))
559+
unpolarized = True
560+
for feed_i in np.arange(self.Nfeeds):
561+
if not np.allclose(efield_vals[0, feed_i], efield_vals[1, feed_i]):
562+
unpolarized = False
563+
564+
if unpolarized:
565+
# for unpolarized beams, where the response to all vector direction
566+
# is identical, f is the sum over that direction divided by sqrt(2)
567+
# this works better than what is below because it handles negatives
568+
# in e.g. AiryBeams
569+
data_array = np.sum(efield_vals, axis=0) / np.sqrt(2)
570+
else:
571+
# if polarized default f to the magnitude, assume zero time delay
572+
data_array = np.sqrt(np.sum(np.abs(efield_vals) ** 2, axis=0))
557573
data_array = data_array[np.newaxis]
558574

559-
return data_array
575+
return data_array.astype(complex)
560576

561577
def feed_projection_eval(
562578
self, *, az_array: FloatArray, za_array: FloatArray, freq_array: FloatArray
@@ -594,8 +610,8 @@ def feed_projection_eval(
594610
az_grid=az_grid, za_grid=za_grid, f_grid=f_grid
595611
).astype(complex)
596612
else:
597-
efield_vals = self._efield_eval(
598-
az_grid=az_grid, za_grid=za_grid, f_grid=f_grid
613+
efield_vals = self.efield_eval(
614+
az_array=az_array, za_array=za_array, freq_array=freq_array
599615
)
600616

601617
# set f to the magnitude of the I response, assume no time delays
@@ -614,7 +630,9 @@ def feed_projection_eval(
614630
def to_uvbeam(
615631
self,
616632
freq_array: FloatArray,
617-
beam_type: Literal["efield", "power"] = "efield",
633+
beam_type: Literal[
634+
"efield", "power", "feed_iresponse", "feed_projection"
635+
] = "efield",
618636
pixel_coordinate_system: (
619637
Literal["az_za", "orthoslant_zenith", "healpix"] | None
620638
) = None,
@@ -635,7 +653,7 @@ def to_uvbeam(
635653
freq_array : ndarray of float
636654
Array of frequencies in Hz to evaluate the beam at.
637655
beam_type : str
638-
Beam type, either "efield" or "power".
656+
Beam type, one of "efield", "power", "feed_iresponse" or "feed_projection".
639657
pixel_coordinate_system : str
640658
Pixel coordinate system, options are "az_za", "orthoslant_zenith" and
641659
"healpix". Forced to be "healpix" if ``nside`` is given and by
@@ -658,13 +676,14 @@ def to_uvbeam(
658676
Healpix ordering parameter, defaults to "ring" if nside is provided.
659677
660678
"""
661-
if beam_type not in ["efield", "power"]:
662-
raise ValueError("Beam type must be 'efield' or 'power'")
679+
allowed_beam_types = ["efield", "power", "feed_iresponse", "feed_projection"]
680+
if beam_type not in allowed_beam_types:
681+
raise ValueError(f"Beam type must be one of {allowed_beam_types}")
663682

664-
if beam_type == "efield":
665-
polarization_array = None
666-
else:
683+
if beam_type == "power":
667684
polarization_array = self.polarization_array
685+
else:
686+
polarization_array = None
668687

669688
mount_type = self.mount_type if hasattr(self, "mount_type") else None
670689

@@ -684,6 +703,7 @@ def to_uvbeam(
684703

685704
uvb = UVBeam.new(
686705
telescope_name="Analytic Beam",
706+
beam_type=beam_type,
687707
data_normalization="physical",
688708
feed_name=self.__repr__(),
689709
feed_version="1.0",
@@ -723,10 +743,7 @@ def to_uvbeam(
723743
az_array = az_array.flatten()
724744
za_array = za_array.flatten()
725745

726-
if beam_type == "efield":
727-
eval_function = "efield_eval"
728-
else:
729-
eval_function = "power_eval"
746+
eval_function = f"{beam_type}_eval"
730747

731748
data_array = getattr(self, eval_function)(
732749
az_array=az_array, za_array=za_array, freq_array=freq_array
@@ -743,7 +760,9 @@ def to_uvbeam(
743760
def plot(
744761
self,
745762
*,
746-
beam_type: str,
763+
beam_type: Literal[
764+
"efield", "power", "feed_iresponse", "feed_projection"
765+
] = "efield",
747766
freq: float,
748767
complex_type: str = "real",
749768
colormap: str | None = None,
@@ -761,6 +780,8 @@ def plot(
761780
762781
Parameters
763782
----------
783+
beam_type : str
784+
Beam type, one of "efield", "power", "feed_iresponse" or "feed_projection".
764785
freq : int
765786
The frequency index to plot.
766787
complex_type : str
@@ -1332,10 +1353,13 @@ def _efield_eval(
13321353

13331354
# The first dimension is for [azimuth, zenith angle] in that order
13341355
# the second dimension is for feed [e, n] in that order
1335-
data_array[0, self.east_ind] = -np.sin(az_grid)
1336-
data_array[0, self.north_ind] = np.cos(az_grid)
1337-
data_array[1, self.east_ind] = np.cos(za_grid) * np.cos(az_grid)
1338-
data_array[1, self.north_ind] = np.cos(za_grid) * np.sin(az_grid)
1356+
cos_za = np.cos(za_grid)
1357+
sin_az = np.sin(az_grid)
1358+
cos_az = np.cos(az_grid)
1359+
data_array[0, self.east_ind] = -sin_az
1360+
data_array[0, self.north_ind] = cos_az
1361+
data_array[1, self.east_ind] = cos_za * cos_az
1362+
data_array[1, self.north_ind] = cos_za * sin_az
13391363

13401364
return data_array
13411365

@@ -1347,12 +1371,13 @@ def _power_eval(
13471371

13481372
# these are just the sum in quadrature of the efield components.
13491373
# some trig work is done to reduce the number of cos/sin evaluations
1350-
data_array[0, 0] = 1 - (np.sin(za_grid) * np.cos(az_grid)) ** 2
1351-
data_array[0, 1] = 1 - (np.sin(za_grid) * np.sin(az_grid)) ** 2
1374+
sin_za = np.sin(za_grid)
1375+
data_array[0, 0] = 1 - (sin_za * np.cos(az_grid)) ** 2
1376+
data_array[0, 1] = 1 - (sin_za * np.sin(az_grid)) ** 2
13521377

13531378
if self.Npols > self.Nfeeds:
13541379
# cross pols are included
1355-
data_array[0, 2] = -(np.sin(za_grid) ** 2) * np.sin(2.0 * az_grid) / 2.0
1380+
data_array[0, 2] = -(sin_za**2) * np.sin(2.0 * az_grid) / 2.0
13561381
data_array[0, 3] = data_array[0, 2]
13571382

13581383
return data_array

src/pyuvdata/beam_interface.py

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ class BeamInterface:
3838
Beam object to use for computations. If a BeamInterface is passed, a new
3939
view of the same object is created.
4040
beam_type : str
41-
The beam type, either "efield" or "power".
41+
The beam type, one of "efield", "power", "feed_iresponse" or "feed_projection".
4242
include_cross_pols : bool
4343
Option to include the cross polarized beams (e.g. xy and yx or en and ne).
4444
Used if beam is a UVBeam and and the input UVBeam is an Efield beam but
@@ -98,7 +98,8 @@ def __post_init__(self, include_cross_pols: bool):
9898
f"Input beam is a {self.beam.beam_type} UVBeam but beam_type "
9999
f"is specified as {self.beam_type}. It's not possible to "
100100
f"convert a {self.beam.beam_type} beam to {self.beam_type}, "
101-
"either provide an efield UVBeam or do not specify `beam_type`."
101+
f"either provide a UVBeam with a compatible type or do not "
102+
"specify `beam_type`."
102103
)
103104
elif self.beam_type is None:
104105
self.beam_type = "efield"
@@ -327,13 +328,10 @@ def compute_response(
327328
az_array_use = copy.copy(az_array)
328329
za_array_use = copy.copy(za_array)
329330

330-
if self.beam_type == "efield":
331-
interp_data = self.beam.efield_eval(
332-
az_array=az_array_use, za_array=za_array_use, freq_array=freq_array
333-
)
334-
else:
335-
interp_data = self.beam.power_eval(
336-
az_array=az_array_use, za_array=za_array_use, freq_array=freq_array
337-
)
331+
eval_function = f"{self.beam_type}_eval"
332+
333+
interp_data = getattr(self.beam, eval_function)(
334+
az_array=az_array_use, za_array=za_array_use, freq_array=freq_array
335+
)
338336

339337
return interp_data

src/pyuvdata/utils/plotting.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -410,7 +410,7 @@ def beam_plot(
410410
freq_title = freq
411411

412412
naxes_vec = beam_obj.Naxes_vec
413-
if beam_type == "power":
413+
if beam_type in ["power", "feed_iresponse"]:
414414
naxes_vec = 1
415415
reg_grid = True
416416

src/pyuvdata/uvbeam/initializers.py

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ def new_uvbeam(
2424
feed_version: str = "0.0",
2525
model_name: str = "default",
2626
model_version: str = "0.0",
27+
beam_type: Literal["efield", "power", "feed_iresponse", "feed_projection"] = None,
2728
feed_array: StrArray | None = None,
2829
feed_angle: FloatArray | None = None,
2930
mount_type: str | None = "fixed",
@@ -78,6 +79,9 @@ def new_uvbeam(
7879
Name of the beam model.
7980
model_version: str
8081
Version of the beam model.
82+
beam_type : str
83+
Beam type, one of "efield", "power", "feed_iresponse" or "feed_projection".
84+
Defaults to "power" if polarization_array is provided and "efield" otherwise.
8185
feed_array : ndarray of str
8286
Array of feed orientations. Options are: n/e or x/y or r/l.
8387
feed_angle : ndarray of float
@@ -177,18 +181,23 @@ def new_uvbeam(
177181

178182
uvb = UVBeam()
179183

180-
if polarization_array is None:
181-
uvb.beam_type = "efield"
182-
uvb._set_efield()
183-
else:
184-
uvb.beam_type = "power"
184+
if beam_type is None:
185+
if polarization_array is None:
186+
beam_type = "efield"
187+
else:
188+
beam_type = "power"
189+
190+
if beam_type == "power":
185191
polarization_array = np.asarray(polarization_array)
186192
if polarization_array.dtype.kind != "i":
187193
polarization_array = np.asarray(utils.polstr2num(polarization_array))
188194
uvb.polarization_array = polarization_array
189195

190196
uvb.Npols = uvb.polarization_array.size
191197
uvb._set_power()
198+
else:
199+
set_function = f"_set_{beam_type}"
200+
getattr(uvb, set_function)()
192201

193202
if feed_array is not None:
194203
uvb.feed_array = np.asarray(feed_array)
@@ -259,7 +268,7 @@ def new_uvbeam(
259268
"Either nside or both axis1_array and axis2_array must be provided."
260269
)
261270

262-
if uvb.beam_type == "power":
271+
if uvb.beam_type in ["power", "feed_iresponse"]:
263272
uvb.Naxes_vec = 1
264273

265274
uvb._set_cs_params()
@@ -296,7 +305,7 @@ def new_uvbeam(
296305
f"expected shape {bv_shape}."
297306
)
298307
uvb.basis_vector_array = basis_vector_array
299-
elif uvb.beam_type == "efield":
308+
elif uvb.beam_type in ["efield", "feed_projection"]:
300309
if uvb.pixel_coordinate_system == "healpix":
301310
basis_vector_array = np.zeros(
302311
(uvb.Naxes_vec, uvb.Ncomponents_vec, uvb.Npixels), dtype=float
@@ -384,15 +393,15 @@ def new_uvbeam(
384393
uvb._set_simple()
385394

386395
# Set data parameters
387-
if uvb.beam_type == "efield":
388-
data_type = complex
389-
polax = uvb.Nfeeds
390-
else:
396+
if uvb.beam_type == "power":
391397
data_type = float
392398
for pol in uvb.polarization_array:
393399
if pol in [-3, -4, -7, -8]:
394400
data_type = complex
395401
polax = uvb.Npols
402+
else:
403+
data_type = complex
404+
polax = uvb.Nfeeds
396405

397406
if uvb.pixel_coordinate_system == "healpix":
398407
pixax = (uvb.Npixels,)

tests/conftest.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,14 @@ def az_za_coords():
171171
return az_array, za_array
172172

173173

174+
@pytest.fixture()
175+
def az_za_coords_fine():
176+
az_array = np.deg2rad(np.linspace(0, 359, 360))
177+
za_array = np.deg2rad(np.linspace(0, 90, 91))
178+
179+
return az_array, za_array
180+
181+
174182
@pytest.fixture()
175183
def az_za_deg_grid(az_za_coords):
176184
az_array, za_array = az_za_coords

tests/test_analytic_beam.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -276,9 +276,8 @@ def _feed_iresponse_eval(
276276
az_grid.shape, beam_type="feed_iresponse"
277277
)
278278

279-
# set f to the magnitude of the I response, assume no time delays
280279
for feed_i in range(self.Nfeeds):
281-
data_array[0, feed_i] = np.abs(np.cos(self.width * za_grid))
280+
data_array[0, feed_i] = np.cos(self.width * za_grid)
282281

283282
return data_array
284283

@@ -290,11 +289,6 @@ def _feed_projection_eval(
290289
)
291290

292291
data_array.fill(1 / np.sqrt(2))
293-
# find where it goes negative
294-
wh_neg = np.nonzero(np.cos(self.width * za_grid) < 0)
295-
for bv_i in range(self.Naxes_vec):
296-
for feed_i in range(self.Nfeeds):
297-
data_array[bv_i, feed_i, *wh_neg] *= -1
298292

299293
return data_array
300294

@@ -569,7 +563,13 @@ def test_beam_equality(beam1, beam2, equal):
569563
def test_to_uvbeam_errors():
570564
beam = GaussianBeam(sigma=0.02)
571565

572-
with pytest.raises(ValueError, match="Beam type must be 'efield' or 'power'"):
566+
with pytest.raises(
567+
ValueError,
568+
match=re.escape(
569+
"Beam type must be one of ['efield', 'power', 'feed_iresponse', "
570+
"'feed_projection']"
571+
),
572+
):
573573
beam.to_uvbeam(
574574
freq_array=np.linspace(100, 200, 5),
575575
beam_type="foo",
@@ -790,6 +790,8 @@ def test_set_x_orientation_deprecation():
790790
None,
791791
),
792792
(ShortDipoleBeam(), "efield", "real", None, 90.0, None, None),
793+
(ShortDipoleBeam(), "feed_iresponse", "phase", None, 90.0, None, None),
794+
(ShortDipoleBeam(), "feed_projection", "real", None, 90.0, None, None),
793795
(ShortDipoleBeam(), "power", "real", None, 90.0, None, None),
794796
(UniformBeam(), "power", "real", None, 90.0, None, None),
795797
],

0 commit comments

Comments
 (0)