Area Coverage

This tutorial demonstrates how to model sensor geometries, obtain the nadir sensor projections on the Earth's surface and compute intersections to cover a polygonal area of interest

Setup

import numpy as np
import pandas as pd

import plotly.graph_objs as go

from ostk.mathematics.geometry.d2.object import Point as Point2d
from ostk.mathematics.geometry.d2.object import LineString as LineString2d
from ostk.mathematics.geometry.d2.object import Polygon as Polygon2d
from ostk.mathematics.geometry.d3.object import Point as Point3d
from ostk.mathematics.geometry.d3.object import Polygon as Polygon3d
from ostk.mathematics.geometry.d3.object import Pyramid
from ostk.mathematics.geometry.d3.object import Cone
from ostk.mathematics.geometry import Angle as MathAngle

from ostk.physics import Environment
from ostk.physics.time import Duration
from ostk.physics.time import Interval
from ostk.physics.coordinate.spherical import LLA
from ostk.physics.coordinate import Frame
from ostk.physics.environment.object import Geometry

from ostk.astrodynamics.flight import Profile
from ostk.astrodynamics.trajectory import Orbit
from ostk.astrodynamics.trajectory.orbit.model import SGP4
from ostk.astrodynamics.trajectory.orbit.model.sgp4 import TLE

Computation

Environment

We first set up a simple scene, with the Earth only (the default Environment will suffice here):

environment = Environment.default()

Then, we access the Earth object (managed by the Environment):

earth = environment.access_celestial_object_with_name("Earth")

Once the Earth has been obtained, we can also get its geometry (an Ellipsoid, defined in ITRF in this case):

earth_geometry = earth.get_geometry_in(Frame.ITRF(), environment.get_instant())

Satellite

Let's define the orbit of a satellite in LEO. In this example, we're modeling the orbit using a TLE.

tle = TLE(
    "1 39419U 13066D   18260.77424112  .00000022  00000-0  72885-5 0  9996",
    "2 39419  97.6300 326.6556 0013847 175.2842 184.8495 14.93888428262811",
)
sgp4_model = SGP4(tle)

We then obtain the satellite orbit (which is a trajectory):

satellite_orbit = Orbit(sgp4_model, earth)
start_instant = tle.get_epoch() + Duration.hours(7.0)
end_instant = start_instant + Duration.hours(2.0)

interval = Interval.closed(start_instant, end_instant)

We select an arbitrary step duration, convenient for the following display

step = Duration.seconds(100.0)
instants = interval.generate_grid(step)
states = [satellite_orbit.get_state_at(instant) for instant in instants]
states_lla = [
    LLA.cartesian(
        state.in_frame(Frame.ITRF()).get_position().get_coordinates(),
        earth.get_equatorial_radius(),
        earth.get_flattening(),
    )
    for state in states
]
states_line_string = LineString2d(
    [
        Point2d(
            state_lla.get_longitude().in_degrees(),
            state_lla.get_latitude().in_degrees(),
        )
        for state_lla in states_lla
    ]
)
ground_track_df = pd.DataFrame(
    [
        [
            float(state_lla.get_longitude().in_degrees()),
            float(state_lla.get_latitude().in_degrees()),
        ]
        for state_lla in states_lla
    ],
    columns=["Longitude", "Latitude"],
)
ground_track_df.head()
Longitude Latitude
0 136.034969 51.823142
1 133.713053 45.712842
2 131.766703 39.568573
3 130.068987 33.399306
4 128.540605 27.211158

Area Target

Let's define a target Polygon2d (Longitude, Latitude) representing an area of interest (covering part of Europe):

target_coordinates = [
    [8.26171875, 53.64463782485651],
    [5.625, 53.225768435790194],
    [5.09765625, 52.3755991766591],
    [3.33984375, 51.17934297928927],
    [1.7578125, 50.736455137010665],
    [0.3515625, 49.38237278700955],
    [-0.87890625, 49.26780455063753],
    [-4.5703125, 48.3416461723746],
    [-0.52734375, 46.437856895024204],
    [-1.23046875, 44.465151013519616],
    [-1.9335937499999998, 43.197167282501276],
    [3.33984375, 42.4234565179383],
    [3.33984375, 43.58039085560784],
    [6.328125, 42.94033923363181],
    [7.55859375, 43.70759350405294],
    [10.1953125, 44.08758502824516],
    [11.6015625, 42.032974332441405],
    [14.765625, 40.58058466412761],
    [15.99609375, 37.996162679728116],
    [18.28125, 40.17887331434696],
    [11.953125, 44.33956524809713],
    [13.18359375, 45.9511496866914],
    [17.578125, 42.94033923363181],
    [22.32421875, 37.020098201368114],
    [23.203125, 40.44694705960048],
    [25.839843749999996, 40.44694705960048],
    [27.24609375, 41.11246878918088],
    [28.30078125, 43.32517767999296],
    [30.234375, 46.92025531537451],
    [35.68359375, 46.6795944656402],
    [38.3203125, 47.27922900257082],
    [39.90234375, 49.61070993807422],
    [35.859375, 50.84757295365389],
    [33.92578125, 52.3755991766591],
    [31.9921875, 52.26815737376817],
    [31.46484375, 53.85252660044951],
    [30.234375, 55.87531083569679],
    [28.30078125, 56.17002298293205],
    [27.59765625, 59.085738569819505],
    [24.78515625, 59.17592824927136],
    [22.67578125, 59.17592824927136],
    [24.08203125, 57.98480801923985],
    [24.2578125, 57.231502991478926],
    [22.148437499999996, 57.70414723434193],
    [21.26953125, 57.136239319177434],
    [21.09375, 55.87531083569679],
    [20.214843749999996, 54.57206165565852],
    [17.75390625, 54.87660665410869],
    [14.94140625, 53.9560855309879],
    [12.65625, 54.36775852406841],
    [10.1953125, 54.36775852406841],
    [10.01953125, 57.231502991478926],
    [8.0859375, 57.136239319177434],
    [8.26171875, 53.64463782485651],
]

or the continental US for instance:

target_coordinates = [
    [-124.541015625, 48.28319289548349],
    [-123.837890625, 44.902577996288876],
    [-124.45312499999999, 42.61779143282346],
    [-124.365234375, 40.38002840251183],
    [-121.11328124999999, 35.02999636902566],
    [-118.037109375, 33.94335994657882],
    [-117.24609374999999, 32.54681317351514],
    [-114.43359375, 32.84267363195431],
    [-111.005859375, 31.353636941500987],
    [-108.6328125, 31.203404950917395],
    [-108.28125, 32.02670629333614],
    [-106.5234375, 31.57853542647338],
    [-103.095703125, 28.76765910569123],
    [-102.3046875, 29.6880527498568],
    [-101.162109375, 29.6880527498568],
    [-97.646484375, 25.799891182088334],
    [-96.6796875, 28.76765910569123],
    [-94.482421875, 29.611670115197377],
    [-92.900390625, 29.84064389983441],
    [-90.087890625, 29.305561325527698],
    [-90, 30.14512718337613],
    [-88.06640625, 30.44867367928756],
    [-85.69335937499999, 30.29701788337205],
    [-84.90234375, 29.6880527498568],
    [-84.287109375, 30.14512718337613],
    [-82.96875, 29.305561325527698],
    [-80.947265625, 25.085598897064752],
    [-80.244140625, 25.48295117535531],
    [-81.474609375, 30.826780904779774],
    [-76.11328125, 35.67514743608467],
    [-73.564453125, 40.84706035607122],
    [-70.3125, 41.64007838467894],
    [-70.3125, 43.197167282501276],
    [-65.21484375, 45.583289756006316],
    [-68.5546875, 47.517200697839414],
    [-70.57617187499999, 45.460130637921004],
    [-74.70703125, 44.84029065139799],
    [-76.904296875, 43.51668853502906],
    [-79.189453125, 43.197167282501276],
    [-81.650390625, 41.83682786072714],
    [-83.408203125, 41.902277040963696],
    [-81.73828125, 44.402391829093915],
    [-83.232421875, 46.01222384063236],
    [-88.154296875, 48.28319289548349],
    [-90.87890625, 48.22467264956519],
    [-94.833984375, 49.095452162534826],
    [-123.3984375, 49.095452162534826],
    [-124.541015625, 48.28319289548349],
]

We create the Polygon2d using those coordinates

target_geometry = Polygon2d(
    [
        Point2d(float(point_coordinates[0]), float(point_coordinates[1]))
        for point_coordinates in target_coordinates
    ]
)

Sensor

We define our orbital frame:

orbital_profile = Profile.local_orbital_frame_pointing(satellite_orbit, Orbit.FrameType.VVLH)
if Frame.exists("Body"):
    Frame.destruct("Body")

body_frame = orbital_profile.get_body_frame("Body")
def calculate_intersection(state, sensor_geometry):
    observer_geometry_ITRF = sensor_geometry.in_frame(Frame.ITRF(), state.get_instant())

    intersection_ITRF = observer_geometry_ITRF.intersection_with(earth_geometry)

    if not intersection_ITRF.is_defined():
        return None

    intersection_points = [
        Point2d(lla.get_longitude().in_degrees(), lla.get_latitude().in_degrees())
        for lla in [
            LLA.cartesian(
                point_ITRF.as_vector(),
                earth.get_equatorial_radius(),
                earth.get_flattening(),
            )
            for point_ITRF in intersection_ITRF.access_composite()
            .access_object_at(0)
            .as_line_string()
        ]
    ]

    if len(intersection_points) < 3:
        return None

    return intersection_ITRF

Let's define a pyramidal geometry:

apex_B = Point3d(0.0, 0.0, 0.0)
base_B = Polygon3d(
    Polygon2d(
        [
            Point2d(-0.5, -0.5),
            Point2d(+0.5, -0.5),
            Point2d(+0.5, +0.5),
            Point2d(-0.5, +0.5),
        ]
    ),
    apex_B + np.array((0.0, 0.0, 1.0)),
    np.array((1.0, 0.0, 0.0)),
    np.array((0.0, 1.0, 0.0)),
)

pyramid_B = Pyramid(base_B, apex_B)

We could also use a conical geometry:

cone_B = Cone(apex_B, np.array((0.0, 0.0, 1.0)), MathAngle.degrees(30.0))

The satellite body frame and the geometry allow us to define a complete sensor geometry

sensor_geometry = Geometry(cone_B, body_frame)

Assuming nadir pointing over the full course of the orbit, we obtain the intersections between the sensor geometry and the Earth's surface for all instants

intersections_ITRF = [
    calculate_intersection(state, sensor_geometry) for state in states
]
intersections_ITRF = [
    intersection_ITRF
    for intersection_ITRF in intersections_ITRF
    if intersection_ITRF is not None
]
intersections_LLs = [
    [
        Point2d(lla.get_longitude().in_degrees(), lla.get_latitude().in_degrees())
        for lla in [
            LLA.cartesian(
                point_ITRF.as_vector(),
                earth.get_equatorial_radius(),
                earth.get_flattening(),
            )
            for point_ITRF in intersection_ITRF.access_composite()
            .access_object_at(0)
            .as_line_string()
        ]
    ]
    for intersection_ITRF in intersections_ITRF
]

Those intersections represent the nadir sensor projections, that we store in a list of pandas dataframes

intersection_dfs = [
    pd.DataFrame(
        [
            [float(intersection_point.x()), float(intersection_point.y())]
            for intersection_point in intersection_LLs
        ],
        columns=["Longitude", "Latitude"],
    )
    for intersection_LLs in intersections_LLs
]
intersection_dfs[0].head()
Longitude Latitude
0 131.975213 53.728721
1 132.510504 54.098646
2 133.146970 54.411552
3 133.868321 54.658295
4 134.654567 54.831497

Target Intersection

We first construct the Polygon2d representing the Sensor projections computed above

intersections_polygons_2d = [
    Polygon2d(coordinates) for coordinates in intersections_LLs
]

Let's compute the intersection regions between the Sensor projections and the area of interest (our target). The output of this operations will be a set of Geometries (mainly Polygon2d as well) representing the portions of the area of interest covered by our sensor (assuming nadir pointing)

intersections_with_target_polygons = [
    sensor_projection.intersection_with(target_geometry)
    if sensor_projection.intersects(target_geometry)
    else None
    for sensor_projection in intersections_polygons_2d
]

Let's convert those intersection geometries in Longitude, Latitude coordinates

intersections_with_target_lls = []

for target_intersection in intersections_with_target_polygons:
    if target_intersection:
        composite = target_intersection.access_composite()

        object_count = composite.get_object_count()

        polygons_lls = []

        for i in range(object_count):
            polygon = composite.access_object_at(i)

            intersections_with_target_lls.append(polygon.get_vertices())

Finally we create the dataframe to store those intersections

target_intersection_dfs = [
    pd.DataFrame(
        [
            [float(intersection_point.x()), float(intersection_point.y())]
            for intersection_point in intersection_LLs
        ],
        columns=["Longitude", "Latitude"],
    )
    for intersection_LLs in intersections_with_target_lls
]

Visualization

2D plot:

data = []

# Satellite Ground Track

data.append(
    go.Scattergeo(
        lon=ground_track_df["Longitude"],
        lat=ground_track_df["Latitude"],
        mode="lines",
        line=dict(
            width=1,
            color="rgba(255, 0, 0, 0.5)",
        ),
    )
)

# Area Target Geometry

data.append(
    go.Scattergeo(
        lon=[coordinate[0] for coordinate in target_coordinates],
        lat=[coordinate[1] for coordinate in target_coordinates],
        mode="lines",
        line=dict(
            width=2,
            color="rgba(0, 200, 200, 1.0)",
        ),
    )
)

# Satellite Sensor Projections

for intersection_df in intersection_dfs:
    data.append(
        go.Scattergeo(
            lon=intersection_df["Longitude"],
            lat=intersection_df["Latitude"],
            mode="lines",
            line=dict(
                width=1,
                color="rgba(0, 0, 255, 0.4)",
            ),
        )
    )

# Satellite Sensor Projections Intersection with Area of Interest

for intersection_df in target_intersection_dfs:
    data.append(
        go.Scattergeo(
            lon=intersection_df["Longitude"],
            lat=intersection_df["Latitude"],
            mode="lines",
            line=dict(
                width=1,
                color="rgba(0, 255, 0, 0.8)",
            ),
        )
    )

figure = go.Figure(
    data=data,
    layout=go.Layout(
        title=None,
        showlegend=False,
        height=600,
        width=1200,
        geo=go.layout.Geo(
            showland=True,
            landcolor="rgb(243, 243, 243)",
            countrycolor="rgb(204, 204, 204)",
            lonaxis=dict(showgrid=True, gridcolor="rgb(102, 102, 102)", gridwidth=0.1),
            lataxis=dict(showgrid=True, gridcolor="rgb(102, 102, 102)", gridwidth=0.1),
        ),
    ),
)

figure.show("svg")
../_images/e0e2ea4327d6f3d9b3b0673dd0a5946fb5865c9e54746d664aa96e474ebe5020.svg

3D plot:

data = []

# Satellite Ground Track

data.append(
    go.Scattergeo(
        lon=ground_track_df["Longitude"],
        lat=ground_track_df["Latitude"],
        mode="lines",
        line=dict(
            width=1,
            color="rgba(255, 0, 0, 0.5)",
        ),
    )
)

# Area Target Geometry

data.append(
    go.Scattergeo(
        lon=[coordinate[0] for coordinate in target_coordinates],
        lat=[coordinate[1] for coordinate in target_coordinates],
        mode="lines",
        line=dict(
            width=2,
            color="rgba(0, 200, 200, 1.0)",
        ),
    )
)

# Satellite Sensor Projections

for intersection_df in intersection_dfs:
    data.append(
        go.Scattergeo(
            lon=intersection_df["Longitude"],
            lat=intersection_df["Latitude"],
            mode="lines",
            line=dict(
                width=1,
                color="rgba(0, 0, 255, 0.4)",
            ),
        )
    )

# Satellite Sensor Projections Intersection with Area of Interest

for intersection_df in target_intersection_dfs:
    data.append(
        go.Scattergeo(
            lon=intersection_df["Longitude"],
            lat=intersection_df["Latitude"],
            mode="lines",
            line=dict(
                width=1,
                color="rgba(0, 255, 0, 0.6)",
            ),
        )
    )

figure = go.Figure(
    data=data,
    layout=go.Layout(
        title=None,
        showlegend=False,
        height=1000,
        geo=go.layout.Geo(
            showland=True,
            showlakes=True,
            showcountries=False,
            showocean=True,
            countrywidth=0.0,
            landcolor="rgb(100, 100, 100)",
            lakecolor="rgb(240, 240, 240)",
            oceancolor="rgb(240, 240, 240)",
            projection=dict(type="orthographic", rotation=dict(lon=120, lat=0, roll=0)),
            lonaxis=dict(showgrid=True, gridcolor="rgb(102, 102, 102)", gridwidth=0.5),
            lataxis=dict(showgrid=True, gridcolor="rgb(102, 102, 102)", gridwidth=0.5),
        ),
    ),
)

figure.show("svg")
../_images/3ebeac9c9ba1df28387a73c94ce23d9abda23e25c31b3b5312dc2938865d930e.svg