pycontrails 0.55.0__cp310-cp310-macosx_11_0_arm64.whl → 0.57.0__cp310-cp310-macosx_11_0_arm64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of pycontrails might be problematic. Click here for more details.

Files changed (31) hide show
  1. pycontrails/_version.py +3 -3
  2. pycontrails/core/airports.py +1 -1
  3. pycontrails/core/cache.py +3 -3
  4. pycontrails/core/fleet.py +1 -1
  5. pycontrails/core/flight.py +47 -43
  6. pycontrails/core/met_var.py +1 -1
  7. pycontrails/core/rgi_cython.cpython-310-darwin.so +0 -0
  8. pycontrails/core/vector.py +28 -30
  9. pycontrails/datalib/geo_utils.py +261 -0
  10. pycontrails/datalib/gfs/gfs.py +58 -64
  11. pycontrails/datalib/goes.py +193 -399
  12. pycontrails/datalib/himawari/__init__.py +27 -0
  13. pycontrails/datalib/himawari/header_struct.py +266 -0
  14. pycontrails/datalib/himawari/himawari.py +654 -0
  15. pycontrails/datalib/landsat.py +49 -26
  16. pycontrails/datalib/leo_utils/__init__.py +5 -0
  17. pycontrails/datalib/leo_utils/correction.py +266 -0
  18. pycontrails/datalib/leo_utils/landsat_metadata.py +300 -0
  19. pycontrails/datalib/{_leo_utils → leo_utils}/search.py +1 -1
  20. pycontrails/datalib/leo_utils/sentinel_metadata.py +748 -0
  21. pycontrails/datalib/sentinel.py +236 -93
  22. pycontrails/models/dry_advection.py +1 -1
  23. pycontrails/models/extended_k15.py +8 -8
  24. {pycontrails-0.55.0.dist-info → pycontrails-0.57.0.dist-info}/METADATA +4 -2
  25. {pycontrails-0.55.0.dist-info → pycontrails-0.57.0.dist-info}/RECORD +31 -23
  26. /pycontrails/datalib/{_leo_utils → leo_utils}/static/bq_roi_query.sql +0 -0
  27. /pycontrails/datalib/{_leo_utils → leo_utils}/vis.py +0 -0
  28. {pycontrails-0.55.0.dist-info → pycontrails-0.57.0.dist-info}/WHEEL +0 -0
  29. {pycontrails-0.55.0.dist-info → pycontrails-0.57.0.dist-info}/licenses/LICENSE +0 -0
  30. {pycontrails-0.55.0.dist-info → pycontrails-0.57.0.dist-info}/licenses/NOTICE +0 -0
  31. {pycontrails-0.55.0.dist-info → pycontrails-0.57.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,300 @@
1
+ """Download and parse Landsat metadata from USGS.
2
+
3
+ This modules requires `GeoPandas <https://geopandas.org/>`_.
4
+ """
5
+
6
+ import re
7
+
8
+ import numpy as np
9
+ import numpy.typing as npt
10
+ import pandas as pd
11
+ import pyproj
12
+ import xarray as xr
13
+
14
+ from pycontrails.core import cache
15
+ from pycontrails.datalib.leo_utils import correction
16
+ from pycontrails.utils import dependencies
17
+
18
+ try:
19
+ import geopandas as gpd
20
+ except ModuleNotFoundError as exc:
21
+ dependencies.raise_module_not_found_error(
22
+ name="landsat_metadata module",
23
+ package_name="geopandas",
24
+ module_not_found_error=exc,
25
+ pycontrails_optional_package="sat",
26
+ )
27
+
28
+ try:
29
+ import shapely
30
+ except ModuleNotFoundError as exc:
31
+ dependencies.raise_module_not_found_error(
32
+ name="landsat_metadata module",
33
+ package_name="shapely",
34
+ module_not_found_error=exc,
35
+ pycontrails_optional_package="sat",
36
+ )
37
+
38
+
39
+ def _split_antimeridian(polygon: shapely.Polygon) -> shapely.MultiPolygon:
40
+ """Split a polygon into two polygons at the antimeridian.
41
+
42
+ This implementation assumes that the passed polygon is actually situated
43
+ on the antimeridian and does not simultaneously cross the meridian.
44
+ """
45
+ # Shift the x-coordinates of the polygon to the right
46
+ # The `valid_poly` will not be valid if the polygon spans the meridian
47
+ valid_poly = shapely.ops.transform(lambda x, y: (x if x >= 0.0 else x + 360.0, y), polygon)
48
+ if not valid_poly.is_valid:
49
+ raise ValueError("Invalid polygon before splitting at the antimeridian.")
50
+
51
+ eastern_hemi = shapely.geometry.box(0.0, -90.0, 180.0, 90.0)
52
+ western_hemi = shapely.geometry.box(180.0, -90.0, 360.0, 90.0)
53
+
54
+ western_poly = valid_poly.intersection(western_hemi)
55
+ western_poly = shapely.ops.transform(lambda x, y: (x - 360.0, y), western_poly) # shift back
56
+ eastern_poly = valid_poly.intersection(eastern_hemi)
57
+
58
+ if not western_poly.is_valid or not eastern_poly.is_valid:
59
+ raise ValueError("Invalid polygon after splitting at the antimeridian.")
60
+
61
+ return shapely.MultiPolygon([western_poly, eastern_poly])
62
+
63
+
64
+ def _download_landsat_metadata() -> pd.DataFrame:
65
+ """Download and parse the Landsat metadata CSV file from USGS.
66
+
67
+ See `the USGS documentation <https://www.usgs.gov/landsat-missions/landsat-collection-2-metadata>`_
68
+ for more details.
69
+ """
70
+ p = "https://landsat.usgs.gov/landsat/metadata_service/bulk_metadata_files/LANDSAT_OT_C2_L1.csv.gz"
71
+
72
+ usecols = [
73
+ "Display ID",
74
+ "Ordering ID",
75
+ "Collection Category",
76
+ "Start Time",
77
+ "Stop Time",
78
+ "Day/Night Indicator",
79
+ "Satellite",
80
+ "Corner Upper Left Latitude",
81
+ "Corner Upper Left Longitude",
82
+ "Corner Upper Right Latitude",
83
+ "Corner Upper Right Longitude",
84
+ "Corner Lower Left Latitude",
85
+ "Corner Lower Left Longitude",
86
+ "Corner Lower Right Latitude",
87
+ "Corner Lower Right Longitude",
88
+ ]
89
+
90
+ df = pd.read_csv(p, compression="gzip", usecols=usecols)
91
+
92
+ # Convert column dtypes
93
+ df["Start Time"] = pd.to_datetime(df["Start Time"], format="ISO8601")
94
+ df["Stop Time"] = pd.to_datetime(df["Stop Time"], format="ISO8601")
95
+ df["Display ID"] = df["Display ID"].astype("string[pyarrow]")
96
+ df["Ordering ID"] = df["Ordering ID"].astype("string[pyarrow]")
97
+ df["Collection Category"] = df["Collection Category"].astype("string[pyarrow]")
98
+ df["Day/Night Indicator"] = df["Day/Night Indicator"].astype("string[pyarrow]")
99
+
100
+ return df
101
+
102
+
103
+ def _landsat_metadata_to_geodataframe(df: pd.DataFrame) -> gpd.GeoDataFrame:
104
+ """Convert Landsat metadata DataFrame to GeoDataFrame with polygons."""
105
+ polys = shapely.polygons(
106
+ df[
107
+ [
108
+ "Corner Upper Left Longitude",
109
+ "Corner Upper Left Latitude",
110
+ "Corner Upper Right Longitude",
111
+ "Corner Upper Right Latitude",
112
+ "Corner Lower Right Longitude",
113
+ "Corner Lower Right Latitude",
114
+ "Corner Lower Left Longitude",
115
+ "Corner Lower Left Latitude",
116
+ "Corner Upper Left Longitude",
117
+ "Corner Upper Left Latitude",
118
+ ]
119
+ ]
120
+ .to_numpy()
121
+ .reshape(-1, 5, 2)
122
+ )
123
+
124
+ out = gpd.GeoDataFrame(
125
+ df,
126
+ geometry=polys,
127
+ crs="EPSG:4326",
128
+ columns=[
129
+ "Display ID",
130
+ "Ordering ID",
131
+ "Collection Category",
132
+ "Start Time",
133
+ "Stop Time",
134
+ "Day/Night Indicator",
135
+ "Satellite",
136
+ "geometry",
137
+ ],
138
+ )
139
+
140
+ # Split polygons that cross the antimeridian
141
+ invalid = ~out.is_valid
142
+ out.loc[invalid, "geometry"] = out.loc[invalid, "geometry"].apply(_split_antimeridian)
143
+ return out
144
+
145
+
146
+ def open_landsat_metadata(
147
+ cachestore: cache.CacheStore | None = None, update_cache: bool = False
148
+ ) -> gpd.GeoDataFrame:
149
+ """Download and parse the Landsat metadata CSV file from USGS.
150
+
151
+ By default, the metadata is cached in a disk cache store.
152
+
153
+ Parameters
154
+ ----------
155
+ cachestore : cache.CacheStore | None, optional
156
+ Cache store for Landsat metadata.
157
+ Defaults to :class:`cache.DiskCacheStore`.
158
+ update_cache : bool, optional
159
+ Force update to cached Landsat metadata. The remote file is updated
160
+ daily, so this is useful to ensure you have the latest metadata.
161
+
162
+ Returns
163
+ -------
164
+ gpd.GeoDataFrame
165
+ Processed Landsat metadata. The ``geometry`` column contains polygons
166
+ representing the footprints of the Landsat scenes.
167
+ """
168
+ if cachestore is None:
169
+ cache_root = cache._get_user_cache_dir()
170
+ cache_dir = f"{cache_root}/landsat_metadata"
171
+ cachestore = cache.DiskCacheStore(cache_dir=cache_dir)
172
+
173
+ cache_key = "LANDSAT_OT_C2_L1.pq"
174
+ if cachestore.exists(cache_key) and not update_cache:
175
+ return gpd.read_parquet(cachestore.path(cache_key))
176
+
177
+ df = _download_landsat_metadata()
178
+ gdf = _landsat_metadata_to_geodataframe(df)
179
+ gdf.to_parquet(cachestore.path(cache_key), index=False)
180
+ return gdf
181
+
182
+
183
+ def parse_ephemeris_landsat(ang_content: str) -> pd.DataFrame:
184
+ """Find the EPHEMERIS group in a ANG text file and extract the data arrays.
185
+
186
+ Parameters
187
+ ----------
188
+ ang_content : str
189
+ The content of the ANG file as a string.
190
+
191
+ Returns
192
+ -------
193
+ pd.DataFrame
194
+ A :class:`pandas.DataFrame` containing the ephemeris track with columns:
195
+ - EPHEMERIS_TIME: Timestamps of the ephemeris data.
196
+ - EPHEMERIS_ECEF_X: ECEF X coordinates.
197
+ - EPHEMERIS_ECEF_Y: ECEF Y coordinates.
198
+ - EPHEMERIS_ECEF_Z: ECEF Z coordinates.
199
+ """
200
+
201
+ # Find GROUP = EPHEMERIS, capture everything non-greedily (.*?) until END_GROUP = EPHEMERIS
202
+ pattern = r"GROUP\s*=\s*EPHEMERIS\s*(.*?)\s*END_GROUP\s*=\s*EPHEMERIS"
203
+ match = re.search(pattern, ang_content, flags=re.DOTALL)
204
+ if match is None:
205
+ raise ValueError("No data found for EPHEMERIS group in the ANG content.")
206
+ ephemeris_content = match.group(1)
207
+
208
+ pattern = r"EPHEMERIS_EPOCH_YEAR\s*=\s*(\d+)"
209
+ match = re.search(pattern, ephemeris_content)
210
+ if match is None:
211
+ raise ValueError("No data found for EPHEMERIS_EPOCH_YEAR in the ANG content.")
212
+ year = int(match.group(1))
213
+
214
+ pattern = r"EPHEMERIS_EPOCH_DAY\s*=\s*(\d+)"
215
+ match = re.search(pattern, ephemeris_content)
216
+ if match is None:
217
+ raise ValueError("No data found for EPHEMERIS_EPOCH_DAY in the ANG content.")
218
+ day = int(match.group(1))
219
+
220
+ pattern = r"EPHEMERIS_EPOCH_SECONDS\s*=\s*(\d+\.\d+)"
221
+ match = re.search(pattern, ephemeris_content)
222
+ if match is None:
223
+ raise ValueError("No data found for EPHEMERIS_EPOCH_SECONDS in the ANG content.")
224
+ seconds = float(match.group(1))
225
+
226
+ t0 = (
227
+ pd.Timestamp(year=year, month=1, day=1)
228
+ + pd.Timedelta(days=day - 1)
229
+ + pd.Timedelta(seconds=seconds)
230
+ )
231
+
232
+ # Find all the EPHEMERIS_* arrays
233
+ array_patterns = {
234
+ "EPHEMERIS_TIME": r"EPHEMERIS_TIME\s*=\s*\((.*?)\)",
235
+ "EPHEMERIS_ECEF_X": r"EPHEMERIS_ECEF_X\s*=\s*\((.*?)\)",
236
+ "EPHEMERIS_ECEF_Y": r"EPHEMERIS_ECEF_Y\s*=\s*\((.*?)\)",
237
+ "EPHEMERIS_ECEF_Z": r"EPHEMERIS_ECEF_Z\s*=\s*\((.*?)\)",
238
+ }
239
+
240
+ arrays = {}
241
+ for key, pattern in array_patterns.items():
242
+ match = re.search(pattern, ephemeris_content, flags=re.DOTALL)
243
+ if match is None:
244
+ raise ValueError(f"No data found for {key} in the ANG content.")
245
+ data_str = match.group(1)
246
+
247
+ data_list = [float(x.strip()) for x in data_str.split(",")]
248
+ if key == "EPHEMERIS_TIME":
249
+ data_list = [t0 + pd.Timedelta(seconds=t) for t in data_list]
250
+ arrays[key] = data_list
251
+
252
+ return pd.DataFrame(arrays)
253
+
254
+
255
+ def get_time_delay_detector(
256
+ ds: xr.Dataset,
257
+ ephemeris: pd.DataFrame,
258
+ utm_crs: pyproj.CRS,
259
+ x: npt.NDArray[np.floating],
260
+ y: npt.NDArray[np.floating],
261
+ ) -> npt.NDArray[np.timedelta64]:
262
+ """Return the detector time delay at the given (x, y) coordinates.
263
+
264
+ Parameters
265
+ ----------
266
+ ds : xr.Dataset
267
+ The Landsat dataset containing the VAA variable.
268
+ ephemeris : pd.DataFrame
269
+ The ephemeris DataFrame containing the EPHEMERIS_TIME and ECEF coordinates.
270
+ utm_crs : pyproj.CRS
271
+ The UTM coordinate reference system for the Landsat scene.
272
+ x : npt.NDArray[np.floating]
273
+ The x-coordinates of the pixels in the dataset's coordinate system.
274
+ y : npt.NDArray[np.floating]
275
+ The y-coordinates of the pixels in the dataset's coordinate system.
276
+
277
+ Returns
278
+ -------
279
+ npt.NDArray[np.timedelta64]
280
+ The time delay for each (x, y) coordinate as a timedelta64 array.
281
+
282
+ """
283
+ x, y = np.atleast_1d(x, y)
284
+
285
+ ephemeris_utm = correction.ephemeris_ecef_to_utm(ephemeris, utm_crs)
286
+ eph_angle_radians = -np.arctan2(ephemeris_utm["y"].diff(), ephemeris_utm["x"].diff())
287
+ avg_eph_angle = (eph_angle_radians * 180.0 / np.pi).mean()
288
+
289
+ vaa = ds["VAA"].interp(x=xr.DataArray(x, dims="points"), y=xr.DataArray(y, dims="points"))
290
+
291
+ is_odd = np.isfinite(vaa) & ((vaa > avg_eph_angle) | (vaa < avg_eph_angle - 180.0))
292
+ is_even = np.isfinite(vaa) & ~is_odd
293
+
294
+ out = np.full(x.shape, fill_value=np.timedelta64("NaT", "ns"), dtype="timedelta64[ns]")
295
+ # We use an offset of +/- 2 seconds as a very rough estimate of the time delay
296
+ # This may only be accurate up to 1 second, but it's better than nothing
297
+ out[is_even] = np.timedelta64(-2000000000, "ns") # -2 seconds
298
+ out[is_odd] = np.timedelta64(2000000000, "ns") # 2 seconds
299
+
300
+ return out
@@ -13,7 +13,7 @@ try:
13
13
  import geojson
14
14
  except ModuleNotFoundError as exc:
15
15
  dependencies.raise_module_not_found_error(
16
- name="datalib._leo_utils module",
16
+ name="datalib.leo_utils module",
17
17
  package_name="geojson",
18
18
  module_not_found_error=exc,
19
19
  pycontrails_optional_package="sat",