pycontrails 0.59.0__cp314-cp314-macosx_10_15_x86_64.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 (123) hide show
  1. pycontrails/__init__.py +70 -0
  2. pycontrails/_version.py +34 -0
  3. pycontrails/core/__init__.py +30 -0
  4. pycontrails/core/aircraft_performance.py +679 -0
  5. pycontrails/core/airports.py +228 -0
  6. pycontrails/core/cache.py +889 -0
  7. pycontrails/core/coordinates.py +174 -0
  8. pycontrails/core/fleet.py +483 -0
  9. pycontrails/core/flight.py +2185 -0
  10. pycontrails/core/flightplan.py +228 -0
  11. pycontrails/core/fuel.py +140 -0
  12. pycontrails/core/interpolation.py +702 -0
  13. pycontrails/core/met.py +2936 -0
  14. pycontrails/core/met_var.py +387 -0
  15. pycontrails/core/models.py +1321 -0
  16. pycontrails/core/polygon.py +549 -0
  17. pycontrails/core/rgi_cython.cpython-314-darwin.so +0 -0
  18. pycontrails/core/vector.py +2249 -0
  19. pycontrails/datalib/__init__.py +12 -0
  20. pycontrails/datalib/_met_utils/metsource.py +746 -0
  21. pycontrails/datalib/ecmwf/__init__.py +73 -0
  22. pycontrails/datalib/ecmwf/arco_era5.py +345 -0
  23. pycontrails/datalib/ecmwf/common.py +114 -0
  24. pycontrails/datalib/ecmwf/era5.py +554 -0
  25. pycontrails/datalib/ecmwf/era5_model_level.py +490 -0
  26. pycontrails/datalib/ecmwf/hres.py +804 -0
  27. pycontrails/datalib/ecmwf/hres_model_level.py +466 -0
  28. pycontrails/datalib/ecmwf/ifs.py +287 -0
  29. pycontrails/datalib/ecmwf/model_levels.py +435 -0
  30. pycontrails/datalib/ecmwf/static/model_level_dataframe_v20240418.csv +139 -0
  31. pycontrails/datalib/ecmwf/variables.py +268 -0
  32. pycontrails/datalib/geo_utils.py +261 -0
  33. pycontrails/datalib/gfs/__init__.py +28 -0
  34. pycontrails/datalib/gfs/gfs.py +656 -0
  35. pycontrails/datalib/gfs/variables.py +104 -0
  36. pycontrails/datalib/goes.py +764 -0
  37. pycontrails/datalib/gruan.py +343 -0
  38. pycontrails/datalib/himawari/__init__.py +27 -0
  39. pycontrails/datalib/himawari/header_struct.py +266 -0
  40. pycontrails/datalib/himawari/himawari.py +671 -0
  41. pycontrails/datalib/landsat.py +589 -0
  42. pycontrails/datalib/leo_utils/__init__.py +5 -0
  43. pycontrails/datalib/leo_utils/correction.py +266 -0
  44. pycontrails/datalib/leo_utils/landsat_metadata.py +300 -0
  45. pycontrails/datalib/leo_utils/search.py +250 -0
  46. pycontrails/datalib/leo_utils/sentinel_metadata.py +748 -0
  47. pycontrails/datalib/leo_utils/static/bq_roi_query.sql +6 -0
  48. pycontrails/datalib/leo_utils/vis.py +59 -0
  49. pycontrails/datalib/sentinel.py +650 -0
  50. pycontrails/datalib/spire/__init__.py +5 -0
  51. pycontrails/datalib/spire/exceptions.py +62 -0
  52. pycontrails/datalib/spire/spire.py +604 -0
  53. pycontrails/ext/bada.py +42 -0
  54. pycontrails/ext/cirium.py +14 -0
  55. pycontrails/ext/empirical_grid.py +140 -0
  56. pycontrails/ext/synthetic_flight.py +431 -0
  57. pycontrails/models/__init__.py +1 -0
  58. pycontrails/models/accf.py +425 -0
  59. pycontrails/models/apcemm/__init__.py +8 -0
  60. pycontrails/models/apcemm/apcemm.py +983 -0
  61. pycontrails/models/apcemm/inputs.py +226 -0
  62. pycontrails/models/apcemm/static/apcemm_yaml_template.yaml +183 -0
  63. pycontrails/models/apcemm/utils.py +437 -0
  64. pycontrails/models/cocip/__init__.py +29 -0
  65. pycontrails/models/cocip/cocip.py +2742 -0
  66. pycontrails/models/cocip/cocip_params.py +305 -0
  67. pycontrails/models/cocip/cocip_uncertainty.py +291 -0
  68. pycontrails/models/cocip/contrail_properties.py +1530 -0
  69. pycontrails/models/cocip/output_formats.py +2270 -0
  70. pycontrails/models/cocip/radiative_forcing.py +1260 -0
  71. pycontrails/models/cocip/radiative_heating.py +520 -0
  72. pycontrails/models/cocip/unterstrasser_wake_vortex.py +508 -0
  73. pycontrails/models/cocip/wake_vortex.py +396 -0
  74. pycontrails/models/cocip/wind_shear.py +120 -0
  75. pycontrails/models/cocipgrid/__init__.py +9 -0
  76. pycontrails/models/cocipgrid/cocip_grid.py +2552 -0
  77. pycontrails/models/cocipgrid/cocip_grid_params.py +138 -0
  78. pycontrails/models/dry_advection.py +602 -0
  79. pycontrails/models/emissions/__init__.py +21 -0
  80. pycontrails/models/emissions/black_carbon.py +599 -0
  81. pycontrails/models/emissions/emissions.py +1353 -0
  82. pycontrails/models/emissions/ffm2.py +336 -0
  83. pycontrails/models/emissions/static/default-engine-uids.csv +239 -0
  84. pycontrails/models/emissions/static/edb-gaseous-v29b-engines.csv +596 -0
  85. pycontrails/models/emissions/static/edb-nvpm-v29b-engines.csv +215 -0
  86. pycontrails/models/extended_k15.py +1327 -0
  87. pycontrails/models/humidity_scaling/__init__.py +37 -0
  88. pycontrails/models/humidity_scaling/humidity_scaling.py +1075 -0
  89. pycontrails/models/humidity_scaling/quantiles/era5-model-level-quantiles.pq +0 -0
  90. pycontrails/models/humidity_scaling/quantiles/era5-pressure-level-quantiles.pq +0 -0
  91. pycontrails/models/issr.py +210 -0
  92. pycontrails/models/pcc.py +326 -0
  93. pycontrails/models/pcr.py +154 -0
  94. pycontrails/models/ps_model/__init__.py +18 -0
  95. pycontrails/models/ps_model/ps_aircraft_params.py +381 -0
  96. pycontrails/models/ps_model/ps_grid.py +701 -0
  97. pycontrails/models/ps_model/ps_model.py +1000 -0
  98. pycontrails/models/ps_model/ps_operational_limits.py +525 -0
  99. pycontrails/models/ps_model/static/ps-aircraft-params-20250328.csv +69 -0
  100. pycontrails/models/ps_model/static/ps-synonym-list-20250328.csv +104 -0
  101. pycontrails/models/sac.py +442 -0
  102. pycontrails/models/tau_cirrus.py +183 -0
  103. pycontrails/physics/__init__.py +1 -0
  104. pycontrails/physics/constants.py +117 -0
  105. pycontrails/physics/geo.py +1138 -0
  106. pycontrails/physics/jet.py +968 -0
  107. pycontrails/physics/static/iata-cargo-load-factors-20250221.csv +74 -0
  108. pycontrails/physics/static/iata-passenger-load-factors-20250221.csv +74 -0
  109. pycontrails/physics/thermo.py +551 -0
  110. pycontrails/physics/units.py +472 -0
  111. pycontrails/py.typed +0 -0
  112. pycontrails/utils/__init__.py +1 -0
  113. pycontrails/utils/dependencies.py +66 -0
  114. pycontrails/utils/iteration.py +13 -0
  115. pycontrails/utils/json.py +187 -0
  116. pycontrails/utils/temp.py +50 -0
  117. pycontrails/utils/types.py +163 -0
  118. pycontrails-0.59.0.dist-info/METADATA +179 -0
  119. pycontrails-0.59.0.dist-info/RECORD +123 -0
  120. pycontrails-0.59.0.dist-info/WHEEL +6 -0
  121. pycontrails-0.59.0.dist-info/licenses/LICENSE +178 -0
  122. pycontrails-0.59.0.dist-info/licenses/NOTICE +43 -0
  123. pycontrails-0.59.0.dist-info/top_level.txt +3 -0
@@ -0,0 +1,589 @@
1
+ """Support for Landsat 8 Collection 1 imagery retrieval through Google Cloud Platform."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterable
6
+
7
+ import numpy as np
8
+ import numpy.typing as npt
9
+ import pandas as pd
10
+ import xarray as xr
11
+
12
+ from pycontrails.core import Flight, cache
13
+ from pycontrails.datalib.leo_utils import search
14
+ from pycontrails.datalib.leo_utils.vis import equalize, normalize
15
+ from pycontrails.utils import dependencies
16
+
17
+ try:
18
+ import gcsfs
19
+ except ModuleNotFoundError as exc:
20
+ dependencies.raise_module_not_found_error(
21
+ name="landsat module",
22
+ package_name="gcsfs",
23
+ module_not_found_error=exc,
24
+ pycontrails_optional_package="sat",
25
+ )
26
+
27
+ try:
28
+ import pyproj
29
+ except ModuleNotFoundError as exc:
30
+ dependencies.raise_module_not_found_error(
31
+ name="landsat module",
32
+ package_name="pyproj",
33
+ module_not_found_error=exc,
34
+ pycontrails_optional_package="sat",
35
+ )
36
+
37
+
38
+ #: BigQuery table with imagery metadata
39
+ BQ_TABLE = "bigquery-public-data.cloud_storage_geo_index.landsat_index"
40
+
41
+ #: Default columns to include in queries
42
+ BQ_DEFAULT_COLUMNS = ["base_url", "sensing_time"]
43
+
44
+ #: Default spatial extent for queries
45
+ BQ_DEFAULT_EXTENT = search.GLOBAL_EXTENT
46
+
47
+ #: Extra filters for BigQuery queries
48
+ BQ_EXTRA_FILTERS = 'AND spacecraft_id in ("LANDSAT_8", "LANDSAT_9")'
49
+
50
+ #: Default Landsat channels to use if none are specified.
51
+ #: These are visible bands for producing a true color composite.
52
+ DEFAULT_BANDS = ["B2", "B3", "B4"]
53
+
54
+ #: Strip this prefix from GCP URLs when caching Landsat files locally
55
+ GCP_STRIP_PREFIX = "gs://gcp-public-data-landsat/"
56
+
57
+
58
+ def query(
59
+ start_time: np.datetime64,
60
+ end_time: np.datetime64,
61
+ extent: str | None = None,
62
+ columns: list[str] | None = None,
63
+ ) -> pd.DataFrame:
64
+ """Find Landsat 8 Collection 1 imagery within spatiotemporal region of interest.
65
+
66
+ This function requires access to the
67
+ `Google BigQuery API <https://cloud.google.com/bigquery?hl=en>`__
68
+ and uses the `BigQuery python library <https://cloud.google.com/python/docs/reference/bigquery/latest/index.html>`__.
69
+
70
+ See :func:`pycontrails.datalib.leo_utils.landsat_metadata.open_landsat_metadata`
71
+ to download and parse the daily bulk Landsat metadata CSV file from USGS. This CSV holds
72
+ Collection 2 metadata, so includes the most recent scenes from Landsat 8 and 9.
73
+
74
+ Parameters
75
+ ----------
76
+ start_time : np.datetime64
77
+ Start of time period for search
78
+ end_time : np.datetime64
79
+ End of time period for search
80
+ extent : str | None, optional
81
+ Spatial region of interest as a GeoJSON string. If not provided, defaults
82
+ to a global extent.
83
+ columns : list[str] | None, optional
84
+ Columns to return from Google
85
+ `BigQuery table <https://console.cloud.google.com/bigquery?p=bigquery-public-data&d=cloud_storage_geo_index&t=landsat_index&page=table>`__.
86
+ By default, returns imagery base URL and sensing time.
87
+
88
+ Returns
89
+ -------
90
+ pd.DataFrame
91
+ Query results in pandas DataFrame
92
+
93
+ See Also
94
+ --------
95
+ :func:`search.query`
96
+ """
97
+ extent = extent or BQ_DEFAULT_EXTENT
98
+ roi = search.ROI(start_time, end_time, extent)
99
+ columns = columns or BQ_DEFAULT_COLUMNS
100
+ return search.query(BQ_TABLE, roi, columns, BQ_EXTRA_FILTERS)
101
+
102
+
103
+ def intersect(
104
+ flight: Flight,
105
+ columns: list[str] | None = None,
106
+ ) -> pd.DataFrame:
107
+ """Find Landsat 8 Collection 1 imagery intersecting with flight track.
108
+
109
+ This function will return all scenes with a bounding box that includes flight waypoints
110
+ both before and after the sensing time.
111
+
112
+ This function requires access to the
113
+ `Google BigQuery API <https://cloud.google.com/bigquery?hl=en>`__
114
+ and uses the `BigQuery python library <https://cloud.google.com/python/docs/reference/bigquery/latest/index.html>`__.
115
+
116
+ See :func:`pycontrails.datalib.leo_utils.landsat_metadata.open_landsat_metadata`
117
+ to download and parse the daily bulk Landsat metadata CSV file from USGS. This CSV holds
118
+ Collection 2 metadata, so includes the most recent scenes from Landsat 8 and 9.
119
+
120
+ Parameters
121
+ ----------
122
+ flight : Flight
123
+ Flight for intersection
124
+ columns : list[str] | None, optional
125
+ Columns to return from Google
126
+ `BigQuery table <https://console.cloud.google.com/bigquery?p=bigquery-public-data&d=cloud_storage_geo_index&t=landsat_index&page=table>`__.
127
+ By default, returns imagery base URL and sensing time.
128
+
129
+ Returns
130
+ -------
131
+ pd.DataFrame
132
+ Query results in pandas DataFrame
133
+
134
+ See Also
135
+ --------
136
+ :func:`search.intersect`
137
+ """
138
+ columns = columns or BQ_DEFAULT_COLUMNS
139
+ return search.intersect(BQ_TABLE, flight, columns, BQ_EXTRA_FILTERS)
140
+
141
+
142
+ class Landsat:
143
+ """Support for Landsat 8 Collection 1 data handling.
144
+
145
+ This interface does not support Landsat Collection 2, which includes all new
146
+ scenes from Landsat 8 and 9 and benefits from improved calibration and processing
147
+ algorithms. The USGS stopped updating Collection 1 in 2021, so this interface
148
+ works only with legacy data. In addition, Collection 1 does not include viewing angle
149
+ data, preventing scan angle or sensing time corrections.
150
+
151
+ To access Landsat Collection 2 data, use one of the following tools:
152
+
153
+ - `USGS M2M API <https://m2m.cr.usgs.gov/>`__ (requires registration)
154
+ - `USGS Earth Explorer <https://earthexplorer.usgs.gov/>`__ (requires registration;
155
+ includes a web interface)
156
+ - `Amazon Web Services (AWS) <https://registry.opendata.aws/usgs-landsat/>`__ (requester pays)
157
+ - `Google Earth Engine <https://developers.google.com/earth-engine/datasets/catalog/landsat>`__
158
+ (requires registration; stricter usage limits than the other options)
159
+
160
+ These services are not yet integrated with pycontrails.
161
+
162
+ Parameters
163
+ ----------
164
+ base_url : str
165
+ Base URL of Landsat scene. To find URLs for Landsat scenes at
166
+ specific locations and times, see :func:`query` and :func:`intersect`.
167
+ bands : str | Iterable[str] | None
168
+ Set of bands to retrieve. The 11 possible bands are represented by
169
+ the string "B1" to "B11". For the Google Landsat contrails color scheme,
170
+ set ``bands=("B9", "B10", "B11")``. For the true color scheme, set
171
+ ``bands=("B2", "B3", "B4")``. By default, bands for the true color scheme
172
+ are used. Bands must share a common resolution. The resolutions of each band are:
173
+
174
+ - B1-B7, B9: 30 m
175
+ - B8: 15 m
176
+ - B10, B11: 30 m (upsampled from true resolution of 100 m)
177
+
178
+ cachestore : cache.CacheStore | None, optional
179
+ Cache store for Landsat data. If None, a :class:`DiskCacheStore` is used.
180
+
181
+ See Also
182
+ --------
183
+ query
184
+ intersect
185
+ """
186
+
187
+ def __init__(
188
+ self,
189
+ base_url: str,
190
+ bands: str | Iterable[str] | None = None,
191
+ cachestore: cache.CacheStore | None = None,
192
+ ) -> None:
193
+ self.base_url = base_url
194
+ self.bands = _parse_bands(bands)
195
+ _check_band_resolution(self.bands)
196
+ self.fs = gcsfs.GCSFileSystem(token="anon")
197
+
198
+ if cachestore is None:
199
+ cache_root = cache._get_user_cache_dir()
200
+ cache_dir = f"{cache_root}/landsat"
201
+ cachestore = cache.DiskCacheStore(cache_dir=cache_dir)
202
+ self.cachestore = cachestore
203
+
204
+ def __repr__(self) -> str:
205
+ """Return string representation."""
206
+ return f"Landsat(base_url='{self.base_url}', bands={sorted(self.bands)})"
207
+
208
+ @property
209
+ def reflective_bands(self) -> list[str]:
210
+ """List of reflective bands."""
211
+ return [b for b in self.bands if b not in ["B10", "B11"]]
212
+
213
+ @property
214
+ def thermal_bands(self) -> list[str]:
215
+ """List of thermal bands."""
216
+ return [b for b in self.bands if b in ["B10", "B11"]]
217
+
218
+ def get(
219
+ self, reflective: str = "reflectance", thermal: str = "brightness_temperature"
220
+ ) -> xr.Dataset:
221
+ """Retrieve Landsat imagery.
222
+
223
+ Parameters
224
+ ----------
225
+ reflective : str, optional
226
+ One of {"raw", "radiance", "reflectance"}.
227
+ Whether to return raw values or rescaled radiances or reflectances for reflective bands.
228
+ By default, return reflectances.
229
+ thermal : str, optional
230
+ One of {"raw", "radiance", "brightness_temperature"}.
231
+ Whether to return raw values or rescaled radiances or brightness temperatures
232
+ for thermal bands. By default, return brightness temperatures.
233
+
234
+ Returns
235
+ -------
236
+ xr.Dataset
237
+ Dataset of Landsat data.
238
+ """
239
+ if reflective not in ["raw", "radiance", "reflectance"]:
240
+ msg = "reflective band processing must be one of ['raw', 'radiance', 'reflectance']"
241
+ raise ValueError(msg)
242
+
243
+ if thermal not in ["raw", "radiance", "brightness_temperature"]:
244
+ msg = (
245
+ "thermal band processing must be one of "
246
+ "['raw', 'radiance', 'brighness_temperature']"
247
+ )
248
+ raise ValueError(msg)
249
+
250
+ ds = xr.Dataset()
251
+ for band in self.reflective_bands:
252
+ ds[band] = self._get(band, reflective)
253
+ for band in self.thermal_bands:
254
+ ds[band] = self._get(band, thermal)
255
+ return ds
256
+
257
+ def _get(self, band: str, processing: str) -> xr.DataArray:
258
+ """Download Landsat band to the :attr:`cachestore` and return processed data."""
259
+ tiff_path = self._get_tiff(band)
260
+ meta_path = self._get_meta()
261
+ return _read(tiff_path, meta_path, band, processing)
262
+
263
+ def _get_tiff(self, band: str) -> str:
264
+ """Download Landsat GeoTIFF imagery and return path to cached file."""
265
+ fs = self.fs
266
+ base_url = self.base_url
267
+ product_id = base_url.split("/")[-1]
268
+ fname = f"{product_id}_{band}.TIF"
269
+ url = f"{base_url}/{fname}"
270
+
271
+ sink = self.cachestore.path(url.removeprefix(GCP_STRIP_PREFIX))
272
+ if not self.cachestore.exists(sink):
273
+ fs.get(url, sink)
274
+ return sink
275
+
276
+ def _get_meta(self) -> str:
277
+ """Download Landsat metadata file and return path to cached file."""
278
+ fs = self.fs
279
+ base_url = self.base_url
280
+ product_id = base_url.split("/")[-1]
281
+ fname = f"{product_id}_MTL.txt"
282
+ url = f"{base_url}/{fname}"
283
+
284
+ sink = self.cachestore.path(url.removeprefix(GCP_STRIP_PREFIX))
285
+ if not self.cachestore.exists(sink):
286
+ fs.get(url, sink)
287
+ return sink
288
+
289
+
290
+ def _parse_bands(bands: str | Iterable[str] | None) -> set[str]:
291
+ """Check that the bands are valid and return as a set."""
292
+ if bands is None:
293
+ return set(DEFAULT_BANDS)
294
+
295
+ if isinstance(bands, str):
296
+ bands = (bands,)
297
+
298
+ available = {f"B{i}" for i in range(1, 12)}
299
+ bands = {b.upper() for b in bands}
300
+ if len(bands) == 0:
301
+ msg = "At least one band must be provided"
302
+ raise ValueError(msg)
303
+ if not bands.issubset(available):
304
+ msg = f"Bands must be in {sorted(available)}"
305
+ raise ValueError(msg)
306
+ return bands
307
+
308
+
309
+ def _check_band_resolution(bands: set[str]) -> None:
310
+ """Confirm requested bands have a common horizontal resolution.
311
+
312
+ All bands have 30 m resolution except the panchromatic band, so
313
+ there are two valid cases: only band 8, or any bands except band 8.
314
+ """
315
+ groups = [
316
+ {"B8"}, # 15 m
317
+ {f"B{i}" for i in range(1, 12) if i != 8}, # 30 m
318
+ ]
319
+ if not any(bands.issubset(group) for group in groups):
320
+ msg = "Bands must have a common horizontal resolution."
321
+ raise ValueError(msg)
322
+
323
+
324
+ def _read(path: str, meta: str, band: str, processing: str) -> xr.DataArray:
325
+ """Read imagery data from Landsat files."""
326
+ try:
327
+ import rasterio
328
+ except ModuleNotFoundError as exc:
329
+ dependencies.raise_module_not_found_error(
330
+ name="landsat module",
331
+ package_name="rasterio",
332
+ module_not_found_error=exc,
333
+ pycontrails_optional_package="sat",
334
+ )
335
+
336
+ with rasterio.open(path) as src:
337
+ img = src.read(1)
338
+ crs = pyproj.CRS.from_epsg(src.crs.to_epsg())
339
+
340
+ if processing == "reflectance":
341
+ mult, add = _read_band_reflectance_rescaling(meta, band)
342
+ img = np.where(img == 0, np.nan, img * mult + add).astype("float32")
343
+ if processing in ("radiance", "brightness_temperature"):
344
+ mult, add = _read_band_radiance_rescaling(meta, band)
345
+ img = np.where(img == 0, np.nan, img * mult + add).astype("float32")
346
+ if processing == "brightness_temperature":
347
+ k1, k2 = _read_band_thermal_constants(meta, band)
348
+ img = k2 / np.log(k1 / img + 1)
349
+
350
+ x, y = _read_image_coordinates(meta, band)
351
+
352
+ da = xr.DataArray(
353
+ data=img,
354
+ coords={"y": y, "x": x},
355
+ dims=("y", "x"),
356
+ attrs={
357
+ "long_name": f"{band} {processing.replace('_', ' ')}",
358
+ "units": (
359
+ "W/m^2/sr/um"
360
+ if processing == "radiance"
361
+ else (
362
+ "nondim"
363
+ if processing == "reflectance"
364
+ else "K"
365
+ if processing == "brightness_temperature"
366
+ else "none"
367
+ )
368
+ ),
369
+ "crs": crs,
370
+ },
371
+ )
372
+ da["x"].attrs = {"long_name": "easting", "units": "m"}
373
+ da["y"].attrs = {"long_name": "northing", "units": "m"}
374
+ return da
375
+
376
+
377
+ def _read_meta(meta: str, key: str) -> float:
378
+ """Read values from metadata file."""
379
+ with open(meta) as f:
380
+ for line in f:
381
+ if line.strip().startswith(key):
382
+ split = line.split("=")
383
+ return float(split[1].strip())
384
+
385
+ msg = f"Could not find {key} in Landsat metadata"
386
+ raise ValueError(msg)
387
+
388
+
389
+ def _read_band_radiance_rescaling(meta: str, band: str) -> tuple[float, float]:
390
+ """Read radiance rescaling factors from metadata file."""
391
+ band = band[1:] # strip leading B
392
+ mult = _read_meta(meta, f"RADIANCE_MULT_BAND_{band}")
393
+ add = _read_meta(meta, f"RADIANCE_ADD_BAND_{band}")
394
+ return mult, add
395
+
396
+
397
+ def _read_band_reflectance_rescaling(meta: str, band: str) -> tuple[float, float]:
398
+ """Read reflectance rescaling factors from metadata file."""
399
+ band = band[1:] # strip leading B
400
+ mult = _read_meta(meta, f"REFLECTANCE_MULT_BAND_{band}")
401
+ add = _read_meta(meta, f"REFLECTANCE_ADD_BAND_{band}")
402
+ return mult, add
403
+
404
+
405
+ def _read_band_thermal_constants(meta: str, band: str) -> tuple[float, float]:
406
+ """Read constants for radiance to brightness temperature conversion from metadata file."""
407
+ band = band[1:] # strip leading B
408
+ k1 = _read_meta(meta, f"K1_CONSTANT_BAND_{band}")
409
+ k2 = _read_meta(meta, f"K2_CONSTANT_BAND_{band}")
410
+ return k1, k2
411
+
412
+
413
+ def _read_image_coordinates(meta: str, band: str) -> tuple[np.ndarray, np.ndarray]:
414
+ """Read image x and y coordinates."""
415
+
416
+ # Get coordinates of corners
417
+ ulx = _read_meta(meta, "CORNER_UL_PROJECTION_X_PRODUCT")
418
+ uly = _read_meta(meta, "CORNER_UL_PROJECTION_Y_PRODUCT")
419
+ urx = _read_meta(meta, "CORNER_UR_PROJECTION_X_PRODUCT")
420
+ ury = _read_meta(meta, "CORNER_UR_PROJECTION_Y_PRODUCT")
421
+ llx = _read_meta(meta, "CORNER_LL_PROJECTION_X_PRODUCT")
422
+ lly = _read_meta(meta, "CORNER_LL_PROJECTION_Y_PRODUCT")
423
+ lrx = _read_meta(meta, "CORNER_LR_PROJECTION_X_PRODUCT")
424
+ lry = _read_meta(meta, "CORNER_LR_PROJECTION_Y_PRODUCT")
425
+ if ulx != llx or urx != lrx or uly != ury or lly != lry:
426
+ msg = "Retrieved Landsat image is not aligned with X and Y coordinates"
427
+ raise ValueError(msg)
428
+ xlim = (ulx, urx)
429
+ ylim = (uly, lly)
430
+
431
+ # Get size of pixels
432
+ category = (
433
+ "PANCHROMATIC" if band == "B8" else "THERMAL" if band in ("B10", "B11") else "REFLECTIVE"
434
+ )
435
+ pixel_size = _read_meta(meta, f"GRID_CELL_SIZE_{category}")
436
+
437
+ # Compute pixel coordinates
438
+ nx = np.round((xlim[1] - xlim[0]) / pixel_size).astype(int) + 1
439
+ ny = np.round((ylim[0] - ylim[1]) / pixel_size).astype(int) + 1
440
+ x = np.linspace(xlim[0], xlim[1], nx)
441
+ y = np.linspace(ylim[0], ylim[1], ny)
442
+
443
+ return x, y
444
+
445
+
446
+ def extract_landsat_visualization(
447
+ ds: xr.Dataset, color_scheme: str = "true"
448
+ ) -> tuple[npt.NDArray[np.float32], pyproj.CRS, tuple[float, float, float, float]]:
449
+ """Extract artifacts for visualizing Landsat data with the given color scheme.
450
+
451
+ Parameters
452
+ ----------
453
+ ds : xr.Dataset
454
+ Dataset of Landsat data as returned by :meth:`Landsat.get`.
455
+ color_scheme : str, optional
456
+ One of {"true", "google_contrails"}.
457
+ Color scheme to use for visualization. The true color scheme
458
+ requires reflectances for bands B2, B3, and B4; and the
459
+ `Google contrails color scheme <https://research.google/pubs/a-human-labeled-landsat-contrails-dataset>`__
460
+ requires reflectance for band B9 and brightness temperatures for bands B10 and B11.
461
+
462
+ Returns
463
+ -------
464
+ rgb : npt.NDArray[np.float32]
465
+ 3D RGB array of shape ``(height, width, 3)``.
466
+ src_crs : pyproj.CRS
467
+ Imagery projection
468
+ src_extent : tuple[float, float, float, float]
469
+ Imagery extent in projected coordinates
470
+
471
+ References
472
+ ----------
473
+ :cite:`mccloskeyHumanlabeledLandsatContrails2021`
474
+ """
475
+
476
+ if color_scheme == "true":
477
+ rgb, src_crs = to_true_color(ds)
478
+ elif color_scheme == "google_contrails":
479
+ rgb, src_crs = to_google_contrails(ds)
480
+ else:
481
+ raise ValueError(f"Color scheme must be 'true' or 'google_contrails', not '{color_scheme}'")
482
+
483
+ x = ds["x"].values
484
+ y = ds["y"].values
485
+ src_extent = x.min(), x.max(), y.min(), y.max()
486
+
487
+ return rgb, src_crs, src_extent
488
+
489
+
490
+ def to_true_color(ds: xr.Dataset) -> tuple[np.ndarray, pyproj.CRS]:
491
+ """Compute 3d RGB array for the true color scheme.
492
+
493
+ Parameters
494
+ ----------
495
+ ds : xr.Dataset
496
+ DataArray of Landsat data with reflectances for bands B2, B3, and B4.
497
+
498
+ Returns
499
+ -------
500
+ np.ndarray
501
+ 3d RGB array with true color scheme.
502
+
503
+ src_crs : pyproj.CRS
504
+ Imagery projection
505
+ """
506
+ red = ds["B4"]
507
+ green = ds["B3"]
508
+ blue = ds["B2"]
509
+
510
+ crs = red.attrs["crs"]
511
+ if not (crs.equals(green.attrs["crs"]) and crs.equals(blue.attrs["crs"])):
512
+ msg = "Bands B2, B3, and B4 do not share a common projection."
513
+ raise ValueError(msg)
514
+
515
+ if any("reflectance" not in band.attrs["long_name"] for band in (red, green, blue)):
516
+ msg = "Bands B2, B3, and B4 must contain reflectances."
517
+ raise ValueError(msg)
518
+
519
+ img = np.dstack(
520
+ [equalize(normalize(band.values), clip_limit=0.03) for band in (red, green, blue)]
521
+ )
522
+
523
+ return img, crs
524
+
525
+
526
+ def to_google_contrails(ds: xr.Dataset) -> tuple[np.ndarray, pyproj.CRS]:
527
+ """Compute 3d RGB array for the Google contrails color scheme.
528
+
529
+ Parameters
530
+ ----------
531
+ ds : xr.Dataset
532
+ DataArray of Landsat data with reflectance for band B9 and brightness
533
+ temperature for bands B10 and B11.
534
+
535
+ Returns
536
+ -------
537
+ np.ndarray
538
+ 3d RGB array with Google landsat color scheme.
539
+
540
+ src_crs : pyproj.CRS
541
+ Imagery projection
542
+
543
+ References
544
+ ----------
545
+ - `Google human-labeled Landsat contrails dataset <https://research.google/pubs/a-human-labeled-landsat-contrails-dataset/>`__
546
+ - :cite:`mccloskeyHumanlabeledLandsatContrails2021`
547
+ """
548
+ rc = ds["B9"] # cirrus band reflectance
549
+ tb11 = ds["B10"] # 11 um brightness temperature
550
+ tb12 = ds["B11"] # 12 um brightness temperature
551
+
552
+ crs = rc.attrs["crs"]
553
+ if not (crs.equals(tb11.attrs["crs"]) and crs.equals(tb12.attrs["crs"])):
554
+ msg = "Bands B9, B10, and B11 do not share a common projection."
555
+ raise ValueError(msg)
556
+
557
+ if "reflectance" not in rc.attrs["long_name"]:
558
+ msg = "Band B9 must contain reflectance."
559
+ raise ValueError(msg)
560
+
561
+ if any("brightness temperature" not in band.attrs["long_name"] for band in (tb11, tb12)):
562
+ msg = "Bands B10 and B11 must contain brightness temperature."
563
+ raise ValueError(msg)
564
+
565
+ def adapt(channel: np.ndarray) -> np.ndarray:
566
+ if np.all(np.isclose(channel, 0, atol=1e-3)) or np.all(np.isclose(channel, 1, atol=1e-3)):
567
+ return channel
568
+ return equalize(channel, clip_limit=0.03)
569
+
570
+ # red: 12um - 11um brightness temperature difference
571
+ signal = tb12.values - tb11.values
572
+ lower = -5.5
573
+ upper = 1.0
574
+ red = ((signal - lower) / (upper - lower)).clip(0.0, 1.0)
575
+
576
+ # green: cirrus band transmittance
577
+ signal = 1.0 - rc.values
578
+ lower = 0.8
579
+ upper = 1.0
580
+ green = adapt(((signal - lower) / (upper - lower)).clip(0.0, 1.0))
581
+
582
+ # blue: 12um brightness temperature
583
+ signal = tb12.values
584
+ lower = 283.0
585
+ upper = 303.0
586
+ blue = adapt(((signal - lower) / (upper - lower)).clip(0.0, 1.0))
587
+
588
+ img = np.dstack([red, green, blue])
589
+ return img, crs
@@ -0,0 +1,5 @@
1
+ """Tools for working with Sentinel-2 and Landsat data."""
2
+
3
+ from pycontrails.datalib.leo_utils.correction import estimate_scan_time, scan_angle_correction
4
+
5
+ __all__ = ["estimate_scan_time", "scan_angle_correction"]