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.
- pycontrails/__init__.py +70 -0
- pycontrails/_version.py +34 -0
- pycontrails/core/__init__.py +30 -0
- pycontrails/core/aircraft_performance.py +679 -0
- pycontrails/core/airports.py +228 -0
- pycontrails/core/cache.py +889 -0
- pycontrails/core/coordinates.py +174 -0
- pycontrails/core/fleet.py +483 -0
- pycontrails/core/flight.py +2185 -0
- pycontrails/core/flightplan.py +228 -0
- pycontrails/core/fuel.py +140 -0
- pycontrails/core/interpolation.py +702 -0
- pycontrails/core/met.py +2936 -0
- pycontrails/core/met_var.py +387 -0
- pycontrails/core/models.py +1321 -0
- pycontrails/core/polygon.py +549 -0
- pycontrails/core/rgi_cython.cpython-314-darwin.so +0 -0
- pycontrails/core/vector.py +2249 -0
- pycontrails/datalib/__init__.py +12 -0
- pycontrails/datalib/_met_utils/metsource.py +746 -0
- pycontrails/datalib/ecmwf/__init__.py +73 -0
- pycontrails/datalib/ecmwf/arco_era5.py +345 -0
- pycontrails/datalib/ecmwf/common.py +114 -0
- pycontrails/datalib/ecmwf/era5.py +554 -0
- pycontrails/datalib/ecmwf/era5_model_level.py +490 -0
- pycontrails/datalib/ecmwf/hres.py +804 -0
- pycontrails/datalib/ecmwf/hres_model_level.py +466 -0
- pycontrails/datalib/ecmwf/ifs.py +287 -0
- pycontrails/datalib/ecmwf/model_levels.py +435 -0
- pycontrails/datalib/ecmwf/static/model_level_dataframe_v20240418.csv +139 -0
- pycontrails/datalib/ecmwf/variables.py +268 -0
- pycontrails/datalib/geo_utils.py +261 -0
- pycontrails/datalib/gfs/__init__.py +28 -0
- pycontrails/datalib/gfs/gfs.py +656 -0
- pycontrails/datalib/gfs/variables.py +104 -0
- pycontrails/datalib/goes.py +764 -0
- pycontrails/datalib/gruan.py +343 -0
- pycontrails/datalib/himawari/__init__.py +27 -0
- pycontrails/datalib/himawari/header_struct.py +266 -0
- pycontrails/datalib/himawari/himawari.py +671 -0
- pycontrails/datalib/landsat.py +589 -0
- pycontrails/datalib/leo_utils/__init__.py +5 -0
- pycontrails/datalib/leo_utils/correction.py +266 -0
- pycontrails/datalib/leo_utils/landsat_metadata.py +300 -0
- pycontrails/datalib/leo_utils/search.py +250 -0
- pycontrails/datalib/leo_utils/sentinel_metadata.py +748 -0
- pycontrails/datalib/leo_utils/static/bq_roi_query.sql +6 -0
- pycontrails/datalib/leo_utils/vis.py +59 -0
- pycontrails/datalib/sentinel.py +650 -0
- pycontrails/datalib/spire/__init__.py +5 -0
- pycontrails/datalib/spire/exceptions.py +62 -0
- pycontrails/datalib/spire/spire.py +604 -0
- pycontrails/ext/bada.py +42 -0
- pycontrails/ext/cirium.py +14 -0
- pycontrails/ext/empirical_grid.py +140 -0
- pycontrails/ext/synthetic_flight.py +431 -0
- pycontrails/models/__init__.py +1 -0
- pycontrails/models/accf.py +425 -0
- pycontrails/models/apcemm/__init__.py +8 -0
- pycontrails/models/apcemm/apcemm.py +983 -0
- pycontrails/models/apcemm/inputs.py +226 -0
- pycontrails/models/apcemm/static/apcemm_yaml_template.yaml +183 -0
- pycontrails/models/apcemm/utils.py +437 -0
- pycontrails/models/cocip/__init__.py +29 -0
- pycontrails/models/cocip/cocip.py +2742 -0
- pycontrails/models/cocip/cocip_params.py +305 -0
- pycontrails/models/cocip/cocip_uncertainty.py +291 -0
- pycontrails/models/cocip/contrail_properties.py +1530 -0
- pycontrails/models/cocip/output_formats.py +2270 -0
- pycontrails/models/cocip/radiative_forcing.py +1260 -0
- pycontrails/models/cocip/radiative_heating.py +520 -0
- pycontrails/models/cocip/unterstrasser_wake_vortex.py +508 -0
- pycontrails/models/cocip/wake_vortex.py +396 -0
- pycontrails/models/cocip/wind_shear.py +120 -0
- pycontrails/models/cocipgrid/__init__.py +9 -0
- pycontrails/models/cocipgrid/cocip_grid.py +2552 -0
- pycontrails/models/cocipgrid/cocip_grid_params.py +138 -0
- pycontrails/models/dry_advection.py +602 -0
- pycontrails/models/emissions/__init__.py +21 -0
- pycontrails/models/emissions/black_carbon.py +599 -0
- pycontrails/models/emissions/emissions.py +1353 -0
- pycontrails/models/emissions/ffm2.py +336 -0
- pycontrails/models/emissions/static/default-engine-uids.csv +239 -0
- pycontrails/models/emissions/static/edb-gaseous-v29b-engines.csv +596 -0
- pycontrails/models/emissions/static/edb-nvpm-v29b-engines.csv +215 -0
- pycontrails/models/extended_k15.py +1327 -0
- pycontrails/models/humidity_scaling/__init__.py +37 -0
- pycontrails/models/humidity_scaling/humidity_scaling.py +1075 -0
- pycontrails/models/humidity_scaling/quantiles/era5-model-level-quantiles.pq +0 -0
- pycontrails/models/humidity_scaling/quantiles/era5-pressure-level-quantiles.pq +0 -0
- pycontrails/models/issr.py +210 -0
- pycontrails/models/pcc.py +326 -0
- pycontrails/models/pcr.py +154 -0
- pycontrails/models/ps_model/__init__.py +18 -0
- pycontrails/models/ps_model/ps_aircraft_params.py +381 -0
- pycontrails/models/ps_model/ps_grid.py +701 -0
- pycontrails/models/ps_model/ps_model.py +1000 -0
- pycontrails/models/ps_model/ps_operational_limits.py +525 -0
- pycontrails/models/ps_model/static/ps-aircraft-params-20250328.csv +69 -0
- pycontrails/models/ps_model/static/ps-synonym-list-20250328.csv +104 -0
- pycontrails/models/sac.py +442 -0
- pycontrails/models/tau_cirrus.py +183 -0
- pycontrails/physics/__init__.py +1 -0
- pycontrails/physics/constants.py +117 -0
- pycontrails/physics/geo.py +1138 -0
- pycontrails/physics/jet.py +968 -0
- pycontrails/physics/static/iata-cargo-load-factors-20250221.csv +74 -0
- pycontrails/physics/static/iata-passenger-load-factors-20250221.csv +74 -0
- pycontrails/physics/thermo.py +551 -0
- pycontrails/physics/units.py +472 -0
- pycontrails/py.typed +0 -0
- pycontrails/utils/__init__.py +1 -0
- pycontrails/utils/dependencies.py +66 -0
- pycontrails/utils/iteration.py +13 -0
- pycontrails/utils/json.py +187 -0
- pycontrails/utils/temp.py +50 -0
- pycontrails/utils/types.py +163 -0
- pycontrails-0.59.0.dist-info/METADATA +179 -0
- pycontrails-0.59.0.dist-info/RECORD +123 -0
- pycontrails-0.59.0.dist-info/WHEEL +6 -0
- pycontrails-0.59.0.dist-info/licenses/LICENSE +178 -0
- pycontrails-0.59.0.dist-info/licenses/NOTICE +43 -0
- 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
|