Compute Coverage

This example demonstrates how to use direct function calls of the low-level TAT-C library to perform coverage analysis over a region of interest.

Similar to the Collect Observations example, the first steps are to define the satellites for the mission. This example again uses the NOAA-20 satellite with a two-line elements model from July 2022 and a VIIRS instrument with field of regard computed based on a 834km altitude and 3000km swath width.

[1]:
from tatc import utils
from tatc.schemas import Instrument, Satellite, TwoLineElements

viirs = Instrument(
    name="VIIRS",
    field_of_regard=utils.swath_width_to_field_of_regard(834000, 3000000),
)
noaa20 = Satellite(
    name="NOAA 20",
    orbit=TwoLineElements(
        tle=[
            "1 43013U 17073A   22195.78278435  .00000038  00000+0  38919-4 0  9996",
            "2 43013  98.7169 133.9110 0001202  63.8768 296.2532 14.19561306241107",
        ]
    ),
    instruments=[viirs],
)

The mission considers a 30-day integration period starting at noon UTC on July 14, 2022.

[2]:
from datetime import datetime, timedelta, timezone

start = datetime(year=2022, month=7, day=14, hour=12, tzinfo=timezone.utc)
end = start + timedelta(days=30)

Coverage analyses typically compute metrics over a spatial region. The generate_cubed_sphere_points method in TAT-C distributes sample points over a uniform latitude-longitude grid, scaled based on a characteristic distance, set to 5000 km in this example.

[3]:
from tatc.generation import generate_cubed_sphere_points

points_df = generate_cubed_sphere_points(5000e3)
display(points_df)
point_id geometry
0 0 POINT Z (-157.51699 -67.51699 0.00000)
1 1 POINT Z (-112.55097 -67.51699 0.00000)
2 2 POINT Z (-67.58495 -67.51699 0.00000)
3 3 POINT Z (-22.61894 -67.51699 0.00000)
4 4 POINT Z (22.34708 -67.51699 0.00000)
5 5 POINT Z (67.31310 -67.51699 0.00000)
6 6 POINT Z (112.27912 -67.51699 0.00000)
7 7 POINT Z (157.24514 -67.51699 0.00000)
8 8 POINT Z (-157.51699 -22.55097 0.00000)
9 9 POINT Z (-112.55097 -22.55097 0.00000)
10 10 POINT Z (-67.58495 -22.55097 0.00000)
11 11 POINT Z (-22.61894 -22.55097 0.00000)
12 12 POINT Z (22.34708 -22.55097 0.00000)
13 13 POINT Z (67.31310 -22.55097 0.00000)
14 14 POINT Z (112.27912 -22.55097 0.00000)
15 15 POINT Z (157.24514 -22.55097 0.00000)
16 16 POINT Z (-157.51699 22.41505 0.00000)
17 17 POINT Z (-112.55097 22.41505 0.00000)
18 18 POINT Z (-67.58495 22.41505 0.00000)
19 19 POINT Z (-22.61894 22.41505 0.00000)
20 20 POINT Z (22.34708 22.41505 0.00000)
21 21 POINT Z (67.31310 22.41505 0.00000)
22 22 POINT Z (112.27912 22.41505 0.00000)
23 23 POINT Z (157.24514 22.41505 0.00000)
24 24 POINT Z (-157.51699 67.38106 0.00000)
25 25 POINT Z (-112.55097 67.38106 0.00000)
26 26 POINT Z (-67.58495 67.38106 0.00000)
27 27 POINT Z (-22.61894 67.38106 0.00000)
28 28 POINT Z (22.34708 67.38106 0.00000)
29 29 POINT Z (67.31310 67.38106 0.00000)
30 30 POINT Z (112.27912 67.38106 0.00000)
31 31 POINT Z (157.24514 67.38106 0.00000)

The resulting 32 points are distributed over the globe.

[4]:
import geoplot as gplt
import contextily as ctx

ax = gplt.pointplot(points_df, color="k")
ctx.add_basemap(ax, crs=points_df.crs)
../_images/examples_ComputeCoverage_8_0.png

As the TAT-C analysis functions require points specified in the TAT-C format, rather than a data frame, it is often convenient to convert and store the points in a separate list.

[5]:
from tatc.schemas import Point

points = points_df.apply(
    lambda r: Point(id=r.point_id, latitude=r.geometry.y, longitude=r.geometry.x),
    axis=1,
)

The collect_observations method can be called either using sequential or parallel computation (using joblib). While there is some overhead associated with parallel computation, it is usually faster on modern multi-core machines.

[6]:
from tatc.analysis import collect_observations

import time

t = time.time()
results_list = [
    collect_observations(point, noaa20, viirs, start, end) for point in points
]
print(f"Sequential computation completed in {time.time() - t:.2f} seconds")
Sequential computation completed in 10.57 seconds
[7]:
from tatc.analysis import collect_observations

import time
from joblib import Parallel, delayed

t = time.time()
results_list = Parallel(n_jobs=-1)(
    delayed(collect_observations)(point, noaa20, viirs, start, end) for point in points
)
print(f"Parallel computation completed in {time.time() - t:.2f} seconds")
Parallel computation completed in 4.44 seconds

Next, we concatenate the results into a single data frame.

[8]:
import pandas as pd

results = pd.concat(results_list, ignore_index=True)
display(results)
point_id geometry satellite instrument start end epoch sat_alt sat_az
0 0 POINT Z (-157.51699 -67.51699 0.00000) NOAA 20 VIIRS 2022-07-14 23:30:13.504937+00:00 2022-07-14 23:35:24.700151+00:00 2022-07-14 23:32:49.102544+00:00 30.035319 90.532033
1 0 POINT Z (-157.51699 -67.51699 0.00000) NOAA 20 VIIRS 2022-07-15 01:09:57.221103+00:00 2022-07-15 01:17:36.070193+00:00 2022-07-15 01:13:46.645648+00:00 84.027201 71.176629
2 0 POINT Z (-157.51699 -67.51699 0.00000) NOAA 20 VIIRS 2022-07-15 02:50:32.021280+00:00 2022-07-15 02:56:59.202403+00:00 2022-07-15 02:53:45.611841500+00:00 41.004603 223.802850
3 0 POINT Z (-157.51699 -67.51699 0.00000) NOAA 20 VIIRS 2022-07-15 04:31:23.154019+00:00 2022-07-15 04:34:35.637143+00:00 2022-07-15 04:32:59.395581+00:00 23.798801 200.198288
4 0 POINT Z (-157.51699 -67.51699 0.00000) NOAA 20 VIIRS 2022-07-15 07:48:30.230323+00:00 2022-07-15 07:52:58.807543+00:00 2022-07-15 07:50:44.518933+00:00 27.400102 151.773103
... ... ... ... ... ... ... ... ... ...
4782 31 POINT Z (157.24514 67.38106 0.00000) NOAA 20 VIIRS 2022-08-12 18:28:49.325939+00:00 2022-08-12 18:34:03.183661+00:00 2022-08-12 18:31:26.254800+00:00 31.260813 324.671157
4783 31 POINT Z (157.24514 67.38106 0.00000) NOAA 20 VIIRS 2022-08-12 21:48:42.817756+00:00 2022-08-12 21:49:48.545180+00:00 2022-08-12 21:49:15.681468+00:00 21.116289 13.096140
4784 31 POINT Z (157.24514 67.38106 0.00000) NOAA 20 VIIRS 2022-08-12 23:25:35.417650+00:00 2022-08-12 23:31:03.448822+00:00 2022-08-12 23:28:19.433236+00:00 32.782441 37.026059
4785 31 POINT Z (157.24514 67.38106 0.00000) NOAA 20 VIIRS 2022-08-13 01:04:19.959524+00:00 2022-08-13 01:11:46.499770+00:00 2022-08-13 01:08:03.229647+00:00 73.846580 59.292237
4786 31 POINT Z (157.24514 67.38106 0.00000) NOAA 20 VIIRS 2022-08-13 02:45:30.353434+00:00 2022-08-13 02:51:54.566357+00:00 2022-08-13 02:48:42.459895500+00:00 40.295326 263.804774

4787 rows × 9 columns

The aggregate_observations function computes the access and revisit statistics for each point.

[9]:
from tatc.analysis import aggregate_observations

aggregated_results = aggregate_observations(results)
display(aggregated_results)
geometry point_id satellite instrument start epoch end access revisit
0 POINT Z (-157.51699 -67.51699 0.00000) 0 NOAA 20 VIIRS 2022-07-14 23:30:13.504937+00:00 2022-07-14 23:32:49.102543872+00:00 2022-07-14 23:35:24.700151+00:00 0 days 00:05:11.195214 NaT
1 POINT Z (-157.51699 -67.51699 0.00000) 0 NOAA 20 VIIRS 2022-07-15 01:09:57.221103+00:00 2022-07-15 01:13:46.645647872+00:00 2022-07-15 01:17:36.070193+00:00 0 days 00:07:38.849090 0 days 01:34:32.520952
2 POINT Z (-157.51699 -67.51699 0.00000) 0 NOAA 20 VIIRS 2022-07-15 02:50:32.021280+00:00 2022-07-15 02:53:45.611841536+00:00 2022-07-15 02:56:59.202403+00:00 0 days 00:06:27.181123 0 days 01:32:55.951087
3 POINT Z (-157.51699 -67.51699 0.00000) 0 NOAA 20 VIIRS 2022-07-15 04:31:23.154019+00:00 2022-07-15 04:32:59.395580928+00:00 2022-07-15 04:34:35.637143+00:00 0 days 00:03:12.483124 0 days 01:34:23.951616
4 POINT Z (-157.51699 -67.51699 0.00000) 0 NOAA 20 VIIRS 2022-07-15 07:48:30.230323+00:00 2022-07-15 07:50:44.518932992+00:00 2022-07-15 07:52:58.807543+00:00 0 days 00:04:28.577220 0 days 03:13:54.593180
... ... ... ... ... ... ... ... ... ...
4782 POINT Z (157.24514 67.38106 0.00000) 31 NOAA 20 VIIRS 2022-08-12 18:28:49.325939+00:00 2022-08-12 18:31:26.254799872+00:00 2022-08-12 18:34:03.183661+00:00 0 days 00:05:13.857722 0 days 01:33:21.770501
4783 POINT Z (157.24514 67.38106 0.00000) 31 NOAA 20 VIIRS 2022-08-12 21:48:42.817756+00:00 2022-08-12 21:49:15.681467904+00:00 2022-08-12 21:49:48.545180+00:00 0 days 00:01:05.727424 0 days 03:14:39.634095
4784 POINT Z (157.24514 67.38106 0.00000) 31 NOAA 20 VIIRS 2022-08-12 23:25:35.417650+00:00 2022-08-12 23:28:19.433235968+00:00 2022-08-12 23:31:03.448822+00:00 0 days 00:05:28.031172 0 days 01:35:46.872470
4785 POINT Z (157.24514 67.38106 0.00000) 31 NOAA 20 VIIRS 2022-08-13 01:04:19.959524+00:00 2022-08-13 01:08:03.229647104+00:00 2022-08-13 01:11:46.499770+00:00 0 days 00:07:26.540246 0 days 01:33:16.510702
4786 POINT Z (157.24514 67.38106 0.00000) 31 NOAA 20 VIIRS 2022-08-13 02:45:30.353434+00:00 2022-08-13 02:48:42.459895552+00:00 2022-08-13 02:51:54.566357+00:00 0 days 00:06:24.212923 0 days 01:33:43.853664

4787 rows × 9 columns

The reduce_observations function computes descriptive statistics for each point.

[10]:
from tatc.analysis import reduce_observations

reduced_results = reduce_observations(aggregated_results)
display(reduced_results)
point_id geometry access revisit samples
0 0 POINT Z (-157.51699 -67.51699 0.00000) 0 days 00:05:28.705247 0 days 02:56:33.403049 234
1 1 POINT Z (-112.55097 -67.51699 0.00000) 0 days 00:05:27.187784 0 days 02:55:27.374120 236
2 2 POINT Z (-67.58495 -67.51699 0.00000) 0 days 00:05:28.226225 0 days 02:55:47.326095 235
3 3 POINT Z (-22.61894 -67.51699 0.00000) 0 days 00:05:28.703592 0 days 02:56:12.255015 235
4 4 POINT Z (22.34708 -67.51699 0.00000) 0 days 00:05:29.869214 0 days 02:57:19.324365 233
5 5 POINT Z (67.31310 -67.51699 0.00000) 0 days 00:05:26.638983 0 days 02:57:41.792189 236
6 6 POINT Z (112.27912 -67.51699 0.00000) 0 days 00:05:29.207116 0 days 02:58:25.816311 235
7 7 POINT Z (157.24514 -67.51699 0.00000) 0 days 00:05:29.089517 0 days 02:59:13.646230 234
8 8 POINT Z (-157.51699 -22.55097 0.00000) 0 days 00:05:48.303140 0 days 09:52:38.681883 72
9 9 POINT Z (-112.55097 -22.55097 0.00000) 0 days 00:05:52.743758 0 days 10:01:06.235517 71
10 10 POINT Z (-67.58495 -22.55097 0.00000) 0 days 00:05:58.830235 0 days 10:08:21.257912 70
11 11 POINT Z (-22.61894 -22.55097 0.00000) 0 days 00:05:53.433621 0 days 10:01:05.661949 71
12 12 POINT Z (22.34708 -22.55097 0.00000) 0 days 00:05:45.642137 0 days 09:51:37.182239 71
13 13 POINT Z (67.31310 -22.55097 0.00000) 0 days 00:05:51.746116 0 days 09:53:37.228465 72
14 14 POINT Z (112.27912 -22.55097 0.00000) 0 days 00:06:01.018137 0 days 10:09:25.218953 70
15 15 POINT Z (157.24514 -22.55097 0.00000) 0 days 00:05:58.445041 0 days 10:10:53.365711 70
16 16 POINT Z (-157.51699 22.41505 0.00000) 0 days 00:06:03.724390 0 days 10:28:18.609280 69
17 17 POINT Z (-112.55097 22.41505 0.00000) 0 days 00:05:45.936852 0 days 09:53:43.729175 72
18 18 POINT Z (-67.58495 22.41505 0.00000) 0 days 00:05:51.188100 0 days 10:11:01.776398 70
19 19 POINT Z (-22.61894 22.41505 0.00000) 0 days 00:05:53.721279 0 days 10:10:58.296466 70
20 20 POINT Z (22.34708 22.41505 0.00000) 0 days 00:05:56.431973 0 days 10:10:18.485633 71
21 21 POINT Z (67.31310 22.41505 0.00000) 0 days 00:05:53.187639 0 days 10:09:53.996686 70
22 22 POINT Z (112.27912 22.41505 0.00000) 0 days 00:05:43.524953 0 days 09:52:43.535228 72
23 23 POINT Z (157.24514 22.41505 0.00000) 0 days 00:05:50.324973 0 days 10:01:08.941317 71
24 24 POINT Z (-157.51699 67.38106 0.00000) 0 days 00:05:32.508810 0 days 03:10:32.061516 221
25 25 POINT Z (-112.55097 67.38106 0.00000) 0 days 00:05:30.892440 0 days 03:09:40.755148 222
26 26 POINT Z (-67.58495 67.38106 0.00000) 0 days 00:05:33.057176 0 days 03:10:31.072390 220
27 27 POINT Z (-22.61894 67.38106 0.00000) 0 days 00:05:30.675408 0 days 03:09:40.962655 222
28 28 POINT Z (22.34708 67.38106 0.00000) 0 days 00:05:29.730143 0 days 03:08:48.883590 223
29 29 POINT Z (67.31310 67.38106 0.00000) 0 days 00:05:30.703513 0 days 03:06:51.457476 222
30 30 POINT Z (112.27912 67.38106 0.00000) 0 days 00:05:27.964761 0 days 03:05:10.758904 224
31 31 POINT Z (157.24514 67.38106 0.00000) 0 days 00:05:29.196438 0 days 03:06:00.960764 223

Finally, we can visualize results, first converting metrics to numeric formats.

[11]:
import geoplot as gplt
import contextily as ctx

reduced_results["revisit_hr"] = reduced_results.revisit / timedelta(hours=1)
ax = gplt.pointplot(
    reduced_results,
    hue="revisit_hr",
    legend=True,
    legend_kwargs={"label": "Revisit (hr)", "orientation": "horizontal"},
)
ctx.add_basemap(ax, crs=reduced_results.crs)
../_images/examples_ComputeCoverage_21_0.png

Coverage analyses can also be aggregated to spatial regions for improved visualizations. This example focuses on a smaller spatial region covering the Continental United States (CONUS) defined by a Polygon. Similar to how the generate_cubed_sphere_points generates uniformly-distributed points (in latitude/longitude), the function generate_cubed_sphere_cells generates uniformly distributed cells. This example uses twice the characteristic distance (1000 km vs. 500 km) such that each cell covers about four points.

[12]:
from shapely.geometry import Polygon

target = Polygon([(-125, 50), (-67, 50), (-67, 19), (-125, 19), (-125, 50)])

import geopandas as gpd

target_area = gpd.GeoDataFrame({"geometry": target}, index=[0], crs="EPSG:4326")

from tatc.generation.points import generate_cubed_sphere_points
from tatc.generation.cells import generate_cubed_sphere_cells

points_df = generate_cubed_sphere_points(500e3, mask=target)
cells_df = generate_cubed_sphere_cells(1000e3, mask=target)

display(cells_df)

import geoplot as gplt
import contextily as ctx

ax = gplt.pointplot(points_df, color="k")
gplt.polyplot(cells_df, ax=ax, edgecolor="r", facecolor=(1, 0, 0, 0.1), zorder=1)
gplt.polyplot(target_area, ax=ax, zorder=2)
ctx.add_basemap(
    ax,
    crs=points_df.crs,
)
cell_id geometry
0 486 POLYGON Z ((-125.00000 19.00000 0.00000, -125....
1 526 POLYGON Z ((-125.00000 26.91165 0.00000, -125....
2 566 POLYGON Z ((-125.00000 35.90485 0.00000, -125....
3 606 POLYGON Z ((-125.00000 44.89806 0.00000, -125....
4 487 POLYGON Z ((-117.04757 19.00000 0.00000, -117....
5 527 POLYGON Z ((-117.04757 26.91165 0.00000, -117....
6 567 POLYGON Z ((-117.04757 35.90485 0.00000, -117....
7 607 POLYGON Z ((-117.04757 44.89806 0.00000, -117....
8 488 POLYGON Z ((-108.05437 19.00000 0.00000, -108....
9 528 POLYGON Z ((-108.05437 26.91165 0.00000, -108....
10 568 POLYGON Z ((-108.05437 35.90485 0.00000, -108....
11 608 POLYGON Z ((-108.05437 44.89806 0.00000, -108....
12 489 POLYGON Z ((-99.06117 19.00000 0.00000, -99.06...
13 529 POLYGON Z ((-99.06117 26.91165 0.00000, -99.06...
14 569 POLYGON Z ((-99.06117 35.90485 0.00000, -99.06...
15 609 POLYGON Z ((-99.06117 44.89806 0.00000, -99.06...
16 490 POLYGON Z ((-90.06796 19.00000 0.00000, -90.06...
17 530 POLYGON Z ((-90.06796 26.91165 0.00000, -90.06...
18 570 POLYGON Z ((-90.06796 35.90485 0.00000, -90.06...
19 610 POLYGON Z ((-90.06796 44.89806 0.00000, -90.06...
20 491 POLYGON Z ((-81.07476 19.00000 0.00000, -81.07...
21 531 POLYGON Z ((-81.07476 26.91165 0.00000, -81.07...
22 571 POLYGON Z ((-81.07476 35.90485 0.00000, -81.07...
23 611 POLYGON Z ((-81.07476 44.89806 0.00000, -81.07...
24 492 POLYGON Z ((-72.08156 19.00000 0.00000, -72.08...
25 532 POLYGON Z ((-72.08156 26.91165 0.00000, -72.08...
26 572 POLYGON Z ((-72.08156 35.90485 0.00000, -72.08...
27 612 POLYGON Z ((-72.08156 44.89806 0.00000, -72.08...
../_images/examples_ComputeCoverage_23_1.png

Similar to the prior case, coverage analysis can be performed for each point.

[13]:
from tatc.schemas import Point

points = points_df.apply(
    lambda r: Point(id=r.point_id, latitude=r.geometry.y, longitude=r.geometry.x),
    axis=1,
)

from joblib import Parallel, delayed
from tatc.analysis.coverage import collect_observations

results_list = Parallel(n_jobs=-1)(
    delayed(collect_observations)(point, noaa20, viirs, start, end) for point in points
)
results = pd.concat(results_list, ignore_index=True)

from tatc.analysis import aggregate_observations

aggregated_results = aggregate_observations(results)

from tatc.analysis.coverage import reduce_observations

reduced_results = reduce_observations(aggregated_results)

import geoplot as gplt
import contextily as ctx

reduced_results["revisit_hr"] = reduced_results.apply(
    lambda r: r["revisit"] / timedelta(hours=1), axis=1
)
ax = gplt.pointplot(
    reduced_results,
    hue="revisit_hr",
    legend=True,
    legend_kwargs={"label": "Revisit (hr)", "orientation": "horizontal"},
)
ctx.add_basemap(
    ax,
    crs=reduced_results.crs,
)
../_images/examples_ComputeCoverage_25_0.png

In addition, the results can be merged into the cell specification using a spatial join and dissolve operation. Note that, when working on the reduced results, the revisit aggregation function must perform a weighted average based on the number of samples for each point.

[14]:
import numpy as np

grid_results = (
    cells_df.sjoin(reduced_results, how="inner", predicate="contains")
    .dissolve(
        by="cell_id",
        aggfunc={
            "samples": "sum",
            "revisit_hr": lambda r: np.average(
                r, weights=reduced_results.loc[r.index, "samples"]
            ),
        },
    )
    .reset_index()
)

import geoplot as gplt
import contextily as ctx

ax = gplt.choropleth(
    grid_results,
    hue="revisit_hr",
    alpha=0.5,
    legend=True,
    legend_kwargs={"label": "Revisit (hr)", "orientation": "horizontal"},
)
ctx.add_basemap(
    ax,
    crs=reduced_results.crs,
)
../_images/examples_ComputeCoverage_27_0.png