@@ -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
0 commit comments