2323 CircularFieldOfView ,
2424 RectangularFieldOfView ,
2525 PolygonFieldOfView ,
26- OmnidirectionalFieldOfView
26+ OmnidirectionalFieldOfView ,
27+ cone_footprint_area
2728)
2829from eosimutils .framegraph import FrameGraph
2930from eosimutils .time import AbsoluteDateArray
@@ -433,6 +434,87 @@ def from_dict(
433434 ) -> "PointCoverage" :
434435 # Empty since class does not require any initialization parameters
435436 return cls ()
437+
438+ @staticmethod
439+ def scale_cbpa_cells (area : float , num_pts : int ,
440+ ref_area : float , ref_num_pts : int , ref_s : float ) -> int :
441+ """Scales the number of CBPA cells using the scaling laws from the CBPA paper.
442+ That is, assuming that ref_s is the optimal cell parameter found for a coverage simulation using a
443+ sensor whose footprint area is "ref_area" on the unit sphere and "ref_num_pts" target points,
444+ then the optimal number of cells for a sensor with footprint area "area" and "num_pts"
445+ target points is calculated using this function.
446+
447+ Args:
448+ area (float): The area of the sensor footprint on the unit sphere in steradians.
449+ num_pts (int): The number of target points.
450+ ref_area (float): The area of the sensor footprint on the unit sphere
451+ in steradians for the reference case.
452+ ref_num_pts (int): The number of target points for the reference case.
453+ ref_s (int): The optimal cell parameter s for the reference case.
454+
455+ Returns:
456+ int: The optimal number of CBPA cells.
457+ """
458+
459+ # Cell grid parameter s, see paper for details
460+ s = ref_s * (num_pts / ref_num_pts )** (2 / 3 )
461+ cbpa_cells = int (4.0 * np .pi * s / (area / ref_area ))
462+ return cbpa_cells
463+
464+ @staticmethod
465+ def compute_cbpa_cells (fov : Union [CircularFieldOfView , RectangularFieldOfView ], distance : float , num_pts : int ) -> int :
466+ """Computes an optimal number of CBPA cells based on the FOV area, fov vertex distance
467+ from surface of the Earth, and number of target points.
468+
469+ Calculates an approximation of the sensor footprint area by assuming:
470+ (1) A conical field of view (if the field of view is rectangular, the maximum cone angle is used).
471+ (2) A spherical earth.
472+ (3) Nadir pointing.
473+
474+ Assumes the given distance is representative of the distance over the course of the simulation. Ideally,
475+ provide an average distance for best results with all orbit types.
476+
477+ Args:
478+ fov (Union[CircularFieldOfView, RectangularFieldOfView]): The field of view to use for coverage calculation.
479+ distance (float): The distance of the sensor from the Earth's center.
480+ num_pts (int): The number of target points.
481+
482+ Returns:
483+ int: The computed number of CBPA cells.
484+ """
485+
486+ if (isinstance (fov , CircularFieldOfView )):
487+ radius = fov .diameter / 2.0
488+ elif (isinstance (fov , RectangularFieldOfView )):
489+ ref_angle = np .deg2rad (fov .ref_angle )
490+ cross_angle = np .deg2rad (fov .cross_angle )
491+ # Compute maximum half-cone angle for rectangular sensor
492+ radius = np .arctan (np .sqrt (np .tan (ref_angle )** 2 + np .tan (cross_angle )** 2 ))
493+ radius = np .rad2deg (radius )
494+
495+ area = cone_footprint_area (
496+ theta = radius ,
497+ d = distance ,
498+ Re = SPHERICAL_EARTH_MEAN_RADIUS ,
499+ )
500+
501+ # Parameters from IEEE paper tuning
502+ # In the IEEE paper, it was found that for a circular FOV with
503+ # half-angle of 22.5 deg, orbital radius of 7080.48, a coverage
504+ # simulation with 100,000 points had optimal cell parameter s=20.
505+ radius_ref = 22.5
506+ distance_ref = 7080.48
507+ area_ref = cone_footprint_area (
508+ theta = radius_ref ,
509+ d = distance_ref ,
510+ Re = SPHERICAL_EARTH_MEAN_RADIUS ,
511+ )
512+ ref_num_pts = 100000
513+ ref_s = 20
514+
515+ return PointCoverage .scale_cbpa_cells (
516+ area , num_pts , area_ref , ref_num_pts , ref_s
517+ )
436518
437519 def calculate_coverage (
438520 self ,
@@ -441,7 +523,9 @@ def calculate_coverage(
441523 frame_graph : FrameGraph ,
442524 times : AbsoluteDateArray ,
443525 surface : SurfaceType = SurfaceType .SPHERE ,
444- buff_size = 86401 ,
526+ use_cbpa : bool = False ,
527+ cbpa_cells = None ,
528+ buff_size = None ,
445529 ) -> DiscreteCoverageTP :
446530 """Calculates the coverage over an array of target points given a field of view.
447531
@@ -457,6 +541,23 @@ def calculate_coverage(
457541 method, which takes the simulation time index as input and updates the object for that
458542 time step.
459543
544+ Notes on CBPA: The cell-based preprocessing algorithm (CBPA) is used to speed up coverage
545+ calculation, especially for small-FOV sensors. It works by pre-loading all of the target
546+ points into a spherical grid with equal latitude spacing and equal longitude spacing withing
547+ each latitude band. During each time step, the sensor profile is projected onto the sphere
548+ and a tight spherical bounding box is created, which is used to quickly lookup the target
549+ points which fall in the box in sublinear time. The algorithm has been tested extensively
550+ and is exact for a spherical earth. On the ellipsoidal earth, the algorithm is run using
551+ a mean-radius approximation of the ellipsoid, which can lead to small errors which are
552+ corrected by adding a 25% increase in the box length in both dimensions. The 25% growth
553+ was selected using probabilistic unit tests as follows. (1) Generate 10 million random
554+ points on WGS-84; (2) Generate a satellite at a random direction and altitude between
555+ 100 and 35000 km; (3) Generate random pointing angles between 0.001 and pi-0.001; (4)
556+ Generate random FOV angles between 0.001 and pi-0.001; (5) Check that every point
557+ inside the FOV is inside the bounding box.
558+
559+ CBPA is described in detail in the publication https://doi.org/10.1109/AERO58975.2024.10521431
560+
460561 Args:
461562 target_point_array (Cartesian3DPositionArray): An array of target points in a Cartesian
462563 coordinate system.
@@ -471,13 +572,22 @@ def calculate_coverage(
471572 checks that the points aren't occluded by the surface. Points in target_point_array
472573 are not necessarily required to be on the surface. Surface is assumed to be fixed
473574 to the frame of the target points.
575+ use_cbpa (bool): If True, use the cell-based preprocessing algorithm (CBPA).
576+ cbpa_cells (int): Only supported for circular and rectangular FOVs. If set,
577+ the program will use CBPA with the number of cells in the grid approximately
578+ equal to the specified value. If this value is not set, a default optimal cell
579+ count will be calculated based on the number of target points and the FOV
580+ footprint area.
474581 buff_size (int): Specifies the buffer size for parallel computation. For optimal
475582 performance, it should be at least equal to the number of CPU cores available
476- on the executing machine.
583+ on the executing machine. Defaults to len(times).
477584 Returns:
478585 DiscreteCoverageTP: An object reporting the grid points covered at each time point.
479586 """
480587
588+ if buff_size is None :
589+ buff_size = len (times )
590+
481591 fov_frame = fov .frame
482592 target_frame = target_point_array .frame
483593
@@ -504,6 +614,14 @@ def calculate_coverage(
504614 ]
505615 fov_to_target_source = kcl .ListSourceMatrix3x3d (fov_to_target_gte )
506616
617+ # If number of cells is not provided
618+ # calculate it optimially using the formulas from IEEE Aero paper.
619+ if use_cbpa and cbpa_cells is None :
620+ # Average distance of satellite from Earth's center
621+ distance = np .linalg .norm (pos_fov_target , axis = 1 ).mean ()
622+ cbpa_cells = PointCoverage .compute_cbpa_cells (fov , distance , len (target_point_array ))
623+
624+ sphere_source = None
507625 # Setup horizon source. A point and a ellipsoid/sphere are sufficient to define a polar
508626 # plane, which divides the ellipsoid/sphere into a region visible from that point
509627 # and a region not visible (no LOS). The halfspace supported by that plane can be used
@@ -547,6 +665,7 @@ def calculate_coverage(
547665 else :
548666 raise ValueError (f"Unsupported surface type: { surface } " )
549667
668+ box_source = None
550669 if isinstance (fov , CircularFieldOfView ):
551670 deg2rad = math .pi / 180.0
552671 half_angle_rad = fov .diameter * 0.5 * deg2rad
@@ -580,6 +699,23 @@ def calculate_coverage(
580699 # Add variables to list
581700 variables .append (boresight_target_source )
582701 variables .append (fov_source )
702+ if use_cbpa :
703+
704+ earth_sphere_cbpa = gte .Sphere3d (
705+ gte .Vector3d .Zero (), SPHERICAL_EARTH_MEAN_RADIUS )
706+ # The sphere source is a constant source, since the sphere's
707+ # parameters remain the same for all time steps
708+ sphere_source_cbpa = kcl .ConstantSourceSphere3d (earth_sphere_cbpa )
709+
710+ box_source = kcl .Cone3dSourceAlignedBoxS2d (
711+ fov_source , sphere_source_cbpa , buff_size
712+ )
713+
714+ if (surface == SurfaceType .WGS84 ):
715+ # Increase box size by 25% to account for ellipsoidal earth approximation
716+ box_source .setFactor (.25 )
717+
718+ variables .append (box_source )
583719
584720 elif isinstance (fov , RectangularFieldOfView ):
585721 deg2rad = math .pi / 180.0
@@ -632,6 +768,23 @@ def calculate_coverage(
632768 variables .append (up_target_source )
633769 variables .append (right_target_source )
634770 variables .append (fov_source )
771+ if use_cbpa :
772+
773+ earth_sphere_cbpa = gte .Sphere3d (
774+ gte .Vector3d .Zero (), SPHERICAL_EARTH_MEAN_RADIUS )
775+ # The sphere source is a constant source, since the sphere's
776+ # parameters remain the same for all time steps
777+ sphere_source_cbpa = kcl .ConstantSourceSphere3d (earth_sphere_cbpa )
778+
779+ box_source = kcl .RectView3dSourceAlignedBoxS2d (
780+ fov_source , sphere_source_cbpa , buff_size
781+ )
782+
783+ if (surface == SurfaceType .WGS84 ):
784+ # Increase box size by 25% to account for ellipsoidal earth approximation
785+ box_source .setFactor (.25 )
786+
787+ variables .append (box_source )
635788
636789 elif isinstance (fov , PolygonFieldOfView ):
637790
@@ -653,6 +806,11 @@ def calculate_coverage(
653806 # Add variables to list
654807 variables .append (fov_source )
655808
809+ if (use_cbpa ):
810+ raise ValueError (
811+ "CBPA preprocessor is only supported for circular or rectangular FOVs."
812+ )
813+
656814 elif isinstance (fov , OmnidirectionalFieldOfView ):
657815 fov_viewer = None
658816 else :
@@ -681,6 +839,16 @@ def calculate_coverage(
681839 buff_size_coverage = len (times )
682840
683841 preprocessor = None
842+ if use_cbpa :
843+ if box_source is None :
844+ raise ValueError (
845+ "CBPA preprocessor is only supported for circular or rectangular FOVs."
846+ )
847+ cellgrid = kcl .CellGrid (cbpa_cells )
848+ cellgrid .AddPoints (target_points_list_gte )
849+ preprocessor = kcl .RangeCovSource (cellgrid , box_source , buff_size )
850+ variables .append (preprocessor )
851+
684852 cov_source = kcl .CovSource (
685853 grid_source , preprocessor , total_view , buff_size_coverage
686854 )
0 commit comments