Skip to content

Commit 9e2b24f

Browse files
authored
Merge pull request #29 from EarthObservationSimulator/cbpa
Added CBPA to PointCoverage coverage calculator
2 parents 10981ad + e6a3ab2 commit 9e2b24f

4 files changed

Lines changed: 250 additions & 12 deletions

File tree

extern/CoverageKinematics

orbitpy/coverage.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,15 @@ def __init__(
5656
self.coverage = coverage
5757
self.grid_points = grid_points
5858

59+
def __eq__(self, other):
60+
if not isinstance(other, DiscreteCoverageTP):
61+
return NotImplemented
62+
return (
63+
self.time == other.time and
64+
self.coverage == other.coverage and
65+
self.grid_points == other.grid_points
66+
)
67+
5968
def to_dict(self) -> Dict[str, Any]:
6069
"""
6170
Serializes this object to a dictionary.

orbitpy/coveragecalculator.py

Lines changed: 171 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@
2323
CircularFieldOfView,
2424
RectangularFieldOfView,
2525
PolygonFieldOfView,
26-
OmnidirectionalFieldOfView
26+
OmnidirectionalFieldOfView,
27+
cone_footprint_area
2728
)
2829
from eosimutils.framegraph import FrameGraph
2930
from 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

Comments
 (0)