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.

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.

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_equally_spaced_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.

from tatc.generation import generate_equally_spaced_points

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

The resulting 32 points are distributed over the globe.

import matplotlib.pyplot as plt
import cartopy.crs as ccrs

fig, ax = plt.subplots(figsize=(8, 5), subplot_kw={"projection": ccrs.PlateCarree()})

points_df.plot(color="red", ax=ax, transform=ccrs.PlateCarree())
ax.stock_img()
ax.set_global()
../_images/a78f878c1b57ae5a9b20d4a80b9a6c33073dc26565b10530faa3bb01f1ea450d.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.

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.

from tatc.analysis import collect_observations

import time

t = time.time()
results_list = [
    collect_observations(point, noaa20, start, end) for point in points
]
print(f"Sequential computation completed in {time.time() - t:.2f} seconds")
Sequential computation completed in 6.25 seconds
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, start, end) for point in points
)
print(f"Parallel computation completed in {time.time() - t:.2f} seconds")
Parallel computation completed in 5.86 seconds

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

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) NOAA 20 VIIRS 2022-07-14 23:30:13.626441+00:00 2022-07-14 23:35:24.594257+00:00 2022-07-14 23:32:49.110349+00:00 30.035469 90.529297
1 0 POINT Z (-157.51699 -67.51699 0) NOAA 20 VIIRS 2022-07-15 01:09:57.306075+00:00 2022-07-15 01:17:35.995681+00:00 2022-07-15 01:13:46.650878+00:00 84.027751 71.150137
2 0 POINT Z (-157.51699 -67.51699 0) NOAA 20 VIIRS 2022-07-15 02:50:32.107661+00:00 2022-07-15 02:56:59.112723+00:00 2022-07-15 02:53:45.610192+00:00 41.004459 223.802226
3 0 POINT Z (-157.51699 -67.51699 0) NOAA 20 VIIRS 2022-07-15 04:31:23.329315+00:00 2022-07-15 04:34:35.412039+00:00 2022-07-15 04:32:59.370677+00:00 23.798760 200.191493
4 0 POINT Z (-157.51699 -67.51699 0) NOAA 20 VIIRS 2022-07-15 07:48:30.337424+00:00 2022-07-15 07:52:58.661577+00:00 2022-07-15 07:50:44.499500500+00:00 27.400167 151.767190
... ... ... ... ... ... ... ... ... ...
4459 31 POINT Z (157.24514 67.38106 0) NOAA 20 VIIRS 2022-08-12 18:28:51.557986+00:00 2022-08-12 18:34:04.353232+00:00 2022-08-12 18:31:27.955609+00:00 31.258697 324.067710
4460 31 POINT Z (157.24514 67.38106 0) NOAA 20 VIIRS 2022-08-12 21:48:45.630367+00:00 2022-08-12 21:49:49.415577+00:00 2022-08-12 21:49:17.522972+00:00 21.115155 12.621024
4461 31 POINT Z (157.24514 67.38106 0) NOAA 20 VIIRS 2022-08-12 23:25:37.442375+00:00 2022-08-12 23:31:05.304568+00:00 2022-08-12 23:28:21.373471500+00:00 32.779326 36.304442
4462 31 POINT Z (157.24514 67.38106 0) NOAA 20 VIIRS 2022-08-13 01:04:22.215027+00:00 2022-08-13 01:11:48.221137+00:00 2022-08-13 01:08:05.218082+00:00 73.798507 55.756224
4463 31 POINT Z (157.24514 67.38106 0) NOAA 20 VIIRS 2022-08-13 02:45:33.011228+00:00 2022-08-13 02:51:56.674083+00:00 2022-08-13 02:48:44.842655500+00:00 40.285445 264.921794

4464 rows × 9 columns

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

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) 0 NOAA 20 VIIRS 2022-07-14 23:30:13.626441+00:00 2022-07-14 23:32:49.110349056+00:00 2022-07-14 23:35:24.594257+00:00 0 days 00:05:10.967816 NaT
1 POINT Z (-157.51699 -67.51699 0) 0 NOAA 20 VIIRS 2022-07-15 01:09:57.306075+00:00 2022-07-15 01:13:46.650877952+00:00 2022-07-15 01:17:35.995681+00:00 0 days 00:07:38.689606 0 days 01:34:32.711818
2 POINT Z (-157.51699 -67.51699 0) 0 NOAA 20 VIIRS 2022-07-15 02:50:32.107661+00:00 2022-07-15 02:53:45.610191872+00:00 2022-07-15 02:56:59.112723+00:00 0 days 00:06:27.005062 0 days 01:32:56.111980
3 POINT Z (-157.51699 -67.51699 0) 0 NOAA 20 VIIRS 2022-07-15 04:31:23.329315+00:00 2022-07-15 04:32:59.370676992+00:00 2022-07-15 04:34:35.412039+00:00 0 days 00:03:12.082724 0 days 01:34:24.216592
4 POINT Z (-157.51699 -67.51699 0) 0 NOAA 20 VIIRS 2022-07-15 07:48:30.337424+00:00 2022-07-15 07:50:44.499500544+00:00 2022-07-15 07:52:58.661577+00:00 0 days 00:04:28.324153 0 days 03:13:54.925385
... ... ... ... ... ... ... ... ... ...
4459 POINT Z (157.24514 67.38106 0) 31 NOAA 20 VIIRS 2022-08-12 18:28:51.557986+00:00 2022-08-12 18:31:27.955609088+00:00 2022-08-12 18:34:04.353232+00:00 0 days 00:05:12.795246 0 days 01:33:22.348289
4460 POINT Z (157.24514 67.38106 0) 31 NOAA 20 VIIRS 2022-08-12 21:48:45.630367+00:00 2022-08-12 21:49:17.522971904+00:00 2022-08-12 21:49:49.415577+00:00 0 days 00:01:03.785210 0 days 03:14:41.277135
4461 POINT Z (157.24514 67.38106 0) 31 NOAA 20 VIIRS 2022-08-12 23:25:37.442375+00:00 2022-08-12 23:28:21.373471488+00:00 2022-08-12 23:31:05.304568+00:00 0 days 00:05:27.862193 0 days 01:35:48.026798
4462 POINT Z (157.24514 67.38106 0) 31 NOAA 20 VIIRS 2022-08-13 01:04:22.215027+00:00 2022-08-13 01:08:05.218082048+00:00 2022-08-13 01:11:48.221137+00:00 0 days 00:07:26.006110 0 days 01:33:16.910459
4463 POINT Z (157.24514 67.38106 0) 31 NOAA 20 VIIRS 2022-08-13 02:45:33.011228+00:00 2022-08-13 02:48:44.842655488+00:00 2022-08-13 02:51:56.674083+00:00 0 days 00:06:23.662855 0 days 01:33:44.790091

4464 rows × 9 columns

The reduce_observations function computes descriptive statistics for each point.

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) 0 days 00:05:30.623220 0 days 03:11:45.894393 216
1 1 POINT Z (-112.55097 -67.51699 0) 0 days 00:05:27.458383 0 days 03:08:40.137703 220
2 2 POINT Z (-67.58495 -67.51699 0) 0 days 00:05:26.605462 0 days 03:08:13.864107 220
3 3 POINT Z (-22.61894 -67.51699 0) 0 days 00:05:30.603801 0 days 03:10:24.345981 218
4 4 POINT Z (22.34708 -67.51699 0) 0 days 00:05:30.457130 0 days 03:11:46.073491 216
5 5 POINT Z (67.3131 -67.51699 0) 0 days 00:05:26.053693 0 days 03:11:05.197080 220
6 6 POINT Z (112.27912 -67.51699 0) 0 days 00:05:28.794415 0 days 03:11:02.062489 220
7 7 POINT Z (157.24514 -67.51699 0) 0 days 00:05:28.645462 0 days 03:12:51.262517 218
8 8 POINT Z (-157.51699 -22.55097 0) 0 days 00:05:42.864787 0 days 10:28:27.927617 68
9 9 POINT Z (-112.55097 -22.55097 0) 0 days 00:05:53.476411 0 days 10:47:46.979011 66
10 10 POINT Z (-67.58495 -22.55097 0) 0 days 00:06:05.461238 0 days 11:06:45.038138 64
11 11 POINT Z (-22.61894 -22.55097 0) 0 days 00:05:54.254635 0 days 10:47:46.313110 66
12 12 POINT Z (22.34708 -22.55097 0) 0 days 00:05:38.389979 0 days 10:37:41.709542 66
13 13 POINT Z (67.3131 -22.55097 0) 0 days 00:05:49.560363 0 days 10:29:26.878119 68
14 14 POINT Z (112.27912 -22.55097 0) 0 days 00:06:12.242443 0 days 11:07:50.637346 64
15 15 POINT Z (157.24514 -22.55097 0) 0 days 00:05:55.908103 0 days 10:48:53.594193 66
16 16 POINT Z (-157.51699 22.41505 0) 0 days 00:06:12.222354 0 days 11:18:30.827075 64
17 17 POINT Z (-112.55097 22.41505 0) 0 days 00:05:42.024086 0 days 10:29:35.152544 68
18 18 POINT Z (-67.58495 22.41505 0) 0 days 00:05:45.371793 0 days 10:49:05.422722 66
19 19 POINT Z (-22.61894 22.41505 0) 0 days 00:05:50.506729 0 days 10:48:59.249179 66
20 20 POINT Z (22.34708 22.41505 0) 0 days 00:06:04.880874 0 days 10:57:34.171803 66
21 21 POINT Z (67.3131 22.41505 0) 0 days 00:05:49.820338 0 days 10:47:51.108372 66
22 22 POINT Z (112.27912 22.41505 0) 0 days 00:05:37.878183 0 days 10:28:32.996863 68
23 23 POINT Z (157.24514 22.41505 0) 0 days 00:05:51.738942 0 days 10:47:49.017594 66
24 24 POINT Z (-157.51699 67.38106 0) 0 days 00:05:32.915323 0 days 03:24:52.485931 206
25 25 POINT Z (-112.55097 67.38106 0) 0 days 00:05:29.659118 0 days 03:22:54.091040 208
26 26 POINT Z (-67.58495 67.38106 0) 0 days 00:05:34.500849 0 days 03:25:56.850045 204
27 27 POINT Z (-22.61894 67.38106 0) 0 days 00:05:29.413149 0 days 03:22:54.329676 208
28 28 POINT Z (22.34708 67.38106 0) 0 days 00:05:33.398720 0 days 03:24:52.018617 206
29 29 POINT Z (67.3131 67.38106 0) 0 days 00:05:29.335932 0 days 03:19:53.467370 208
30 30 POINT Z (112.27912 67.38106 0) 0 days 00:05:26.471776 0 days 03:17:58.496404 210
31 31 POINT Z (157.24514 67.38106 0) 0 days 00:05:29.072607 0 days 03:19:53.714949 208

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

import matplotlib.pyplot as plt
import cartopy.crs as ccrs

reduced_results["revisit_hr"] = reduced_results.revisit / timedelta(hours=1)

fig, ax = plt.subplots(figsize=(8, 5), subplot_kw={"projection": ccrs.PlateCarree()})
reduced_results.plot(
    column="revisit_hr",
    cmap="viridis",
    legend=True,
    legend_kwds={"label": "Revisit (hr)", "orientation": "horizontal"},
    ax=ax,
    transform=ccrs.PlateCarree()
)
ax.coastlines()
ax.set_global()
plt.show()
../_images/6492b26a3db1dfec7e5f9913ebb37142b04438a057a573f3530659696ab27009.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_equally_spaced_points generates uniformly-distributed points (in latitude/longitude), the function generate_equally_spaced_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.

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_equally_spaced_points
from tatc.generation.cells import generate_equally_spaced_cells

points_df = generate_equally_spaced_points(500e3, mask=target)
cells_df = generate_equally_spaced_cells(1000e3, mask=target)

display(cells_df)

import matplotlib.pyplot as plt
import cartopy.crs as ccrs

fig, ax = plt.subplots(figsize=(8, 5), subplot_kw={"projection": ccrs.PlateCarree()})
target_area.plot(alpha=0.5, ax=ax, transform=ccrs.PlateCarree())
points_df.plot(color="r", ax=ax, transform=ccrs.PlateCarree())
cells_df.plot(edgecolor="b", facecolor=(1, 0, 0, 0.1), ax=ax, transform=ccrs.PlateCarree())
ax.coastlines()
plt.show()
cell_id geometry
0 486 POLYGON Z ((-125 19 0, -125 26.91165 0, -117.0...
1 487 POLYGON Z ((-117.04757 19 0, -117.04757 26.911...
2 488 POLYGON Z ((-108.05437 19 0, -108.05437 26.911...
3 489 POLYGON Z ((-99.06117 19 0, -99.06117 26.91165...
4 526 POLYGON Z ((-125 26.91165 0, -125 35.90485 0, ...
5 527 POLYGON Z ((-117.04757 26.91165 0, -117.04757 ...
6 528 POLYGON Z ((-108.05437 26.91165 0, -108.05437 ...
7 529 POLYGON Z ((-99.06117 26.91165 0, -99.06117 35...
8 566 POLYGON Z ((-125 35.90485 0, -125 44.89806 0, ...
9 567 POLYGON Z ((-117.04757 35.90485 0, -117.04757 ...
10 490 POLYGON Z ((-90.06796 19 0, -90.06796 26.91165...
11 491 POLYGON Z ((-81.07476 19 0, -81.07476 26.91165...
12 492 POLYGON Z ((-72.08156 19 0, -72.08156 26.91165...
13 530 POLYGON Z ((-90.06796 26.91165 0, -90.06796 35...
14 531 POLYGON Z ((-81.07476 26.91165 0, -81.07476 35...
15 532 POLYGON Z ((-72.08156 26.91165 0, -72.08156 35...
16 569 POLYGON Z ((-99.06117 35.90485 0, -99.06117 44...
17 570 POLYGON Z ((-90.06796 35.90485 0, -90.06796 44...
18 571 POLYGON Z ((-81.07476 35.90485 0, -81.07476 44...
19 572 POLYGON Z ((-72.08156 35.90485 0, -72.08156 44...
20 568 POLYGON Z ((-108.05437 35.90485 0, -108.05437 ...
21 606 POLYGON Z ((-125 44.89806 0, -125 50 0, -117.0...
22 607 POLYGON Z ((-117.04757 44.89806 0, -117.04757 ...
23 608 POLYGON Z ((-108.05437 44.89806 0, -108.05437 ...
24 609 POLYGON Z ((-99.06117 44.89806 0, -99.06117 50...
25 610 POLYGON Z ((-90.06796 44.89806 0, -90.06796 50...
26 611 POLYGON Z ((-81.07476 44.89806 0, -81.07476 50...
27 612 POLYGON Z ((-72.08156 44.89806 0, -72.08156 50...
../_images/19e65b57f2b4d96e94d635862b5a201b85367f3f93895704e95bcbd8db0ae764.png

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

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, 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)

reduced_results["revisit_hr"] = reduced_results.apply(
    lambda r: r["revisit"] / timedelta(hours=1), axis=1
)

import matplotlib.pyplot as plt
import cartopy.crs as ccrs

fig, ax = plt.subplots(figsize=(8, 6), subplot_kw={"projection": ccrs.PlateCarree()})
reduced_results.plot(
    column="revisit_hr",
    cmap="viridis",
    legend=True,
    legend_kwds={"label": "Revisit (hr)", "orientation": "horizontal"},
    ax=ax,
    transform=ccrs.PlateCarree()
)
ax.coastlines()
plt.show()
../_images/ee57cbb6b2743ffa2661aca296a884c85fa1ef3add9cc43af87fac982733599d.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.

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 matplotlib.pyplot as plt
import cartopy.crs as ccrs

fig, ax = plt.subplots(figsize=(8, 7), subplot_kw={"projection": ccrs.PlateCarree()})
grid_results.plot(
    column="revisit_hr",
    cmap="viridis",
    edgecolor="k",
    legend=True,
    legend_kwds={"label": "Revisit (hr)", "orientation": "horizontal"},
    ax=ax,
    transform=ccrs.PlateCarree()
)
ax.coastlines()
plt.show()
../_images/006f0c0c31673630d9fcc036a41a0e00af54c5e4ea72d245102344a554e5fe3b.png