pycontrails 0.55.0__cp310-cp310-win_amd64.whl → 0.56.0__cp310-cp310-win_amd64.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/_version.py +3 -3
- pycontrails/core/airports.py +1 -1
- pycontrails/core/cache.py +3 -3
- pycontrails/core/fleet.py +1 -1
- pycontrails/core/flight.py +47 -43
- pycontrails/core/met_var.py +1 -1
- pycontrails/core/rgi_cython.cp310-win_amd64.pyd +0 -0
- pycontrails/core/vector.py +28 -30
- pycontrails/datalib/landsat.py +49 -26
- 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 → leo_utils}/search.py +1 -1
- pycontrails/datalib/leo_utils/sentinel_metadata.py +748 -0
- pycontrails/datalib/sentinel.py +236 -93
- pycontrails/models/dry_advection.py +1 -1
- pycontrails/models/extended_k15.py +8 -8
- {pycontrails-0.55.0.dist-info → pycontrails-0.56.0.dist-info}/METADATA +3 -1
- {pycontrails-0.55.0.dist-info → pycontrails-0.56.0.dist-info}/RECORD +25 -21
- /pycontrails/datalib/{_leo_utils → leo_utils}/static/bq_roi_query.sql +0 -0
- /pycontrails/datalib/{_leo_utils → leo_utils}/vis.py +0 -0
- {pycontrails-0.55.0.dist-info → pycontrails-0.56.0.dist-info}/WHEEL +0 -0
- {pycontrails-0.55.0.dist-info → pycontrails-0.56.0.dist-info}/licenses/LICENSE +0 -0
- {pycontrails-0.55.0.dist-info → pycontrails-0.56.0.dist-info}/licenses/NOTICE +0 -0
- {pycontrails-0.55.0.dist-info → pycontrails-0.56.0.dist-info}/top_level.txt +0 -0
pycontrails/datalib/sentinel.py
CHANGED
|
@@ -7,12 +7,23 @@ from collections.abc import Iterable
|
|
|
7
7
|
from xml.etree import ElementTree
|
|
8
8
|
|
|
9
9
|
import numpy as np
|
|
10
|
+
import numpy.typing as npt
|
|
10
11
|
import pandas as pd
|
|
11
12
|
import xarray as xr
|
|
12
13
|
|
|
13
14
|
from pycontrails.core import Flight, cache
|
|
14
|
-
from pycontrails.datalib.
|
|
15
|
-
from pycontrails.datalib.
|
|
15
|
+
from pycontrails.datalib.leo_utils import correction, search
|
|
16
|
+
from pycontrails.datalib.leo_utils.sentinel_metadata import (
|
|
17
|
+
_band_id,
|
|
18
|
+
get_detector_id,
|
|
19
|
+
get_time_delay_detectors,
|
|
20
|
+
parse_ephemeris_sentinel,
|
|
21
|
+
parse_high_res_viewing_incidence_angles,
|
|
22
|
+
parse_sensing_time,
|
|
23
|
+
parse_sentinel_crs,
|
|
24
|
+
read_image_coordinates,
|
|
25
|
+
)
|
|
26
|
+
from pycontrails.datalib.leo_utils.vis import equalize, normalize
|
|
16
27
|
from pycontrails.utils import dependencies
|
|
17
28
|
|
|
18
29
|
try:
|
|
@@ -53,7 +64,7 @@ ROI_QUERY_FILENAME = _path_to_static / "sentinel_roi_query.sql"
|
|
|
53
64
|
BQ_TABLE = "bigquery-public-data.cloud_storage_geo_index.sentinel_2_index"
|
|
54
65
|
|
|
55
66
|
#: Default columns to include in queries
|
|
56
|
-
BQ_DEFAULT_COLUMNS = ["base_url", "granule_id", "sensing_time"]
|
|
67
|
+
BQ_DEFAULT_COLUMNS = ["base_url", "granule_id", "sensing_time", "source_url"]
|
|
57
68
|
|
|
58
69
|
#: Default spatial extent for queries
|
|
59
70
|
BQ_DEFAULT_EXTENT = search.GLOBAL_EXTENT
|
|
@@ -84,10 +95,10 @@ def query(
|
|
|
84
95
|
Start of time period for search
|
|
85
96
|
end_time : np.datetime64
|
|
86
97
|
End of time period for search
|
|
87
|
-
extent : str, optional
|
|
98
|
+
extent : str | None, optional
|
|
88
99
|
Spatial region of interest as a GeoJSON string. If not provided, defaults
|
|
89
100
|
to a global extent.
|
|
90
|
-
columns : list[str], optional
|
|
101
|
+
columns : list[str] | None, optional
|
|
91
102
|
Columns to return from Google
|
|
92
103
|
`BigQuery table <https://console.cloud.google.com/bigquery?p=bigquery-public-data&d=cloud_storage_geo_index&t=landsat_index&page=table&_ga=2.90807450.1051800793.1716904050-255800408.1705955196>`__.
|
|
93
104
|
By default, returns imagery base URL, granule ID, and sensing time.
|
|
@@ -124,7 +135,7 @@ def intersect(
|
|
|
124
135
|
----------
|
|
125
136
|
flight : Flight
|
|
126
137
|
Flight for intersection
|
|
127
|
-
columns : list[str], optional
|
|
138
|
+
columns : list[str] | None, optional
|
|
128
139
|
Columns to return from Google
|
|
129
140
|
`BigQuery table <https://console.cloud.google.com/bigquery?p=bigquery-public-data&d=cloud_storage_geo_index&t=landsat_index&page=table&_ga=2.90807450.1051800793.1716904050-255800408.1705955196>`__.
|
|
130
141
|
By default, returns imagery base URL, granule ID, and sensing time.
|
|
@@ -139,18 +150,18 @@ def intersect(
|
|
|
139
150
|
:func:`search.intersect`
|
|
140
151
|
"""
|
|
141
152
|
columns = columns or BQ_DEFAULT_COLUMNS
|
|
142
|
-
|
|
153
|
+
scenes = search.intersect(BQ_TABLE, flight, columns)
|
|
154
|
+
|
|
155
|
+
# overwrite the base_url with source_url.
|
|
156
|
+
# After 2024-03-14 there is a mistake in the Google BigQuery table,
|
|
157
|
+
# such that the base_url is written to the source_url column
|
|
158
|
+
scenes["base_url"] = scenes["base_url"].fillna(scenes["source_url"])
|
|
159
|
+
return scenes
|
|
143
160
|
|
|
144
161
|
|
|
145
162
|
class Sentinel:
|
|
146
163
|
"""Support for Sentinel-2 data handling.
|
|
147
164
|
|
|
148
|
-
This class uses the `PROJ <https://proj.org/en/9.4/index.html>`__ coordinate
|
|
149
|
-
transformation software through the
|
|
150
|
-
`pyproj <https://pyproj4.github.io/pyproj/stable/index.html>`__ python interface.
|
|
151
|
-
pyproj is installed as part of the ``sat`` set of optional dependencies
|
|
152
|
-
(``pip install pycontrails[sat]``), but PROJ must be installed manually.
|
|
153
|
-
|
|
154
165
|
Parameters
|
|
155
166
|
----------
|
|
156
167
|
base_url : str
|
|
@@ -159,7 +170,7 @@ class Sentinel:
|
|
|
159
170
|
granule_id : str
|
|
160
171
|
Granule ID of Sentinel-2 scene. To find URLs for Sentinel-2 scenes at
|
|
161
172
|
specific locations and times, see :func:`query` and :func:`intersect`.
|
|
162
|
-
bands : str |
|
|
173
|
+
bands : str | Iterable[str] | None
|
|
163
174
|
Set of bands to retrieve. The 13 possible bands are represented by
|
|
164
175
|
the string "B01" to "B12" plus "B8A". For the true color scheme, set
|
|
165
176
|
``bands=("B02", "B03", "B04")``. By default, bands for the true color scheme
|
|
@@ -169,7 +180,7 @@ class Sentinel:
|
|
|
169
180
|
- B05-B07, B8A, B11, B12: 20 m
|
|
170
181
|
- B01, B09, B10: 60 m
|
|
171
182
|
|
|
172
|
-
cachestore : cache.CacheStore, optional
|
|
183
|
+
cachestore : cache.CacheStore | None, optional
|
|
173
184
|
Cache store for Landsat data. If None, a :class:`DiskCacheStore` is used.
|
|
174
185
|
|
|
175
186
|
See Also
|
|
@@ -210,23 +221,163 @@ class Sentinel:
|
|
|
210
221
|
|
|
211
222
|
Parameters
|
|
212
223
|
----------
|
|
213
|
-
reflective : str
|
|
214
|
-
|
|
224
|
+
reflective : str, optional
|
|
225
|
+
Set to "raw" to return raw values or "reflectance" for rescaled reflectances.
|
|
215
226
|
By default, return reflectances.
|
|
216
227
|
|
|
217
228
|
Returns
|
|
218
229
|
-------
|
|
219
|
-
xr.
|
|
220
|
-
|
|
230
|
+
xr.Dataset
|
|
231
|
+
Dataset of Sentinel-2 data.
|
|
221
232
|
"""
|
|
222
|
-
|
|
223
|
-
|
|
233
|
+
available = ("raw", "reflectance")
|
|
234
|
+
if reflective not in available:
|
|
235
|
+
msg = f"reflective band processing must be one of {available}"
|
|
224
236
|
raise ValueError(msg)
|
|
225
237
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
238
|
+
data = {band: self._get(band, reflective) for band in self.bands}
|
|
239
|
+
return xr.Dataset(data)
|
|
240
|
+
|
|
241
|
+
def get_viewing_angle_metadata(self, scale: int = 10) -> xr.Dataset:
|
|
242
|
+
"""Return the dataset with viewing angles.
|
|
243
|
+
|
|
244
|
+
See :func:`parse_high_res_viewing_incidence_angles` for details.
|
|
245
|
+
"""
|
|
246
|
+
granule_meta_path, _ = self._get_meta()
|
|
247
|
+
_, detector_band_path = self._get_correction_meta()
|
|
248
|
+
return parse_high_res_viewing_incidence_angles(
|
|
249
|
+
granule_meta_path, detector_band_path, scale=scale
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
def get_detector_id(
|
|
253
|
+
self, x: npt.NDArray[np.floating], y: npt.NDArray[np.floating]
|
|
254
|
+
) -> npt.NDArray[np.integer]:
|
|
255
|
+
"""Return the detector_id of the Sentinel-2 detector that imaged the given points.
|
|
256
|
+
|
|
257
|
+
Parameters
|
|
258
|
+
----------
|
|
259
|
+
x : npt.NDArray[np.floating]
|
|
260
|
+
x coordinates of points in the Sentinel-2 image CRS
|
|
261
|
+
y : npt.NDArray[np.floating]
|
|
262
|
+
y coordinates of points in the Sentinel-2 image CRS
|
|
263
|
+
|
|
264
|
+
Returns
|
|
265
|
+
-------
|
|
266
|
+
npt.NDArray[np.integer]
|
|
267
|
+
Detector IDs for each point. If a point is outside the image, the detector ID is 0.
|
|
268
|
+
"""
|
|
269
|
+
granule_sink, _ = self._get_meta()
|
|
270
|
+
_, detector_band_sink = self._get_correction_meta()
|
|
271
|
+
return get_detector_id(detector_band_sink, granule_sink, x, y)
|
|
272
|
+
|
|
273
|
+
def get_time_delay_detector(
|
|
274
|
+
self, detector_id: npt.NDArray[np.integer], band: str = "B03"
|
|
275
|
+
) -> npt.NDArray[np.timedelta64]:
|
|
276
|
+
"""Return the time delay for the given detector IDs."""
|
|
277
|
+
datastrip_sink, _ = self._get_correction_meta()
|
|
278
|
+
delays = get_time_delay_detectors(datastrip_sink, band)
|
|
279
|
+
|
|
280
|
+
# Map the delays to the input array
|
|
281
|
+
unique_ids, inverse = np.unique(detector_id, return_inverse=True)
|
|
282
|
+
|
|
283
|
+
# Get offsets for each unique detector
|
|
284
|
+
offset_list = []
|
|
285
|
+
for did in unique_ids:
|
|
286
|
+
if did == 0:
|
|
287
|
+
offset_list.append(np.timedelta64("NaT"))
|
|
288
|
+
else:
|
|
289
|
+
offset_list.append(delays.get(did, np.timedelta64("NaT")))
|
|
290
|
+
offset_values = np.array(offset_list, dtype="timedelta64[ns]")
|
|
291
|
+
|
|
292
|
+
# Map back to the original array
|
|
293
|
+
return offset_values[inverse]
|
|
294
|
+
|
|
295
|
+
def get_ephemeris(self) -> pd.DataFrame:
|
|
296
|
+
"""Return the satellite ephemeris as a :class:`pandas.DataFrame`."""
|
|
297
|
+
datastrip_sink, _ = self._get_correction_meta()
|
|
298
|
+
|
|
299
|
+
return parse_ephemeris_sentinel(datastrip_sink)
|
|
300
|
+
|
|
301
|
+
def get_crs(self) -> pyproj.CRS:
|
|
302
|
+
"""Return the CRS of the satellite image."""
|
|
303
|
+
granule_meta_path, _ = self._get_meta()
|
|
304
|
+
return parse_sentinel_crs(granule_meta_path)
|
|
305
|
+
|
|
306
|
+
def get_sensing_time(self) -> pd.Timestamp:
|
|
307
|
+
"""Return the sensing_time of the satellite image."""
|
|
308
|
+
granule_meta_path, _ = self._get_meta()
|
|
309
|
+
return parse_sensing_time(granule_meta_path)
|
|
310
|
+
|
|
311
|
+
def colocate_flight(
|
|
312
|
+
self,
|
|
313
|
+
flight: Flight,
|
|
314
|
+
band: str = "B03",
|
|
315
|
+
) -> tuple[float, float, np.datetime64]:
|
|
316
|
+
"""Colocate a flight track with satellite image pixels.
|
|
317
|
+
|
|
318
|
+
This function first projects flight waypoints into the UTM coordinate system
|
|
319
|
+
of the satellite image, then applies a viewing angle correction to estimate
|
|
320
|
+
the actual ground position imaged by the satellite. Next, the scan time for
|
|
321
|
+
each point is estimated based on the satellite meta data. Finally, the
|
|
322
|
+
point along the flight track is found where the flight time matches the
|
|
323
|
+
satellite scan time.
|
|
324
|
+
|
|
325
|
+
Parameters
|
|
326
|
+
----------
|
|
327
|
+
flight : Flight
|
|
328
|
+
The flight object.
|
|
329
|
+
band : str, optional
|
|
330
|
+
Spectral band to use for geometry parsing. Default is "B03".
|
|
331
|
+
|
|
332
|
+
Returns
|
|
333
|
+
-------
|
|
334
|
+
tuple[float, float, np.datetime64]
|
|
335
|
+
A tuple containing the x and y coordinates of the flight position in the
|
|
336
|
+
satellite image CRS, and the corrected sensing_time of the satellite image.
|
|
337
|
+
"""
|
|
338
|
+
utm_crs = self.get_crs()
|
|
339
|
+
|
|
340
|
+
# Project from WGS84 to the x and y coordinates in the UTM coordinate system
|
|
341
|
+
transformer = pyproj.Transformer.from_crs("EPSG:4326", utm_crs, always_xy=True)
|
|
342
|
+
x, y = transformer.transform(flight["longitude"], flight["latitude"])
|
|
343
|
+
z = flight.altitude
|
|
344
|
+
|
|
345
|
+
# Apply sensing angle correction
|
|
346
|
+
ds_angles = self.get_viewing_angle_metadata()
|
|
347
|
+
x_proj, y_proj = correction.scan_angle_correction(ds_angles, x, y, z, maxiter=3)
|
|
348
|
+
|
|
349
|
+
# Get the satellite ephemeris data prepared
|
|
350
|
+
ephemeris = self.get_ephemeris()
|
|
351
|
+
|
|
352
|
+
# Estimate the scan time for each point
|
|
353
|
+
scan_time_no_offset = correction.estimate_scan_time(ephemeris, utm_crs, x_proj, y_proj)
|
|
354
|
+
|
|
355
|
+
detector_id = self.get_detector_id(x_proj, y_proj)
|
|
356
|
+
offset = self.get_time_delay_detector(detector_id, band=band)
|
|
357
|
+
scan_time = scan_time_no_offset + offset
|
|
358
|
+
|
|
359
|
+
# To finish up, find the point along the project flight track at which the satellite
|
|
360
|
+
# scan time equals the flight time
|
|
361
|
+
# It's possible that if the flight has a really fine time resolution, that
|
|
362
|
+
# the diff_times array will have multiple zeros. This is okay, as np.interp
|
|
363
|
+
# will just return the first one.
|
|
364
|
+
valid = np.isfinite(x_proj) & np.isfinite(y_proj) & np.isfinite(scan_time)
|
|
365
|
+
|
|
366
|
+
diff_times = flight["time"][valid] - scan_time[valid]
|
|
367
|
+
if diff_times[0] > np.timedelta64(0) or diff_times[-1] < np.timedelta64(0):
|
|
368
|
+
msg = "Flight track does not overlap with satellite image in time."
|
|
369
|
+
raise ValueError(msg)
|
|
370
|
+
|
|
371
|
+
index = np.arange(len(diff_times))
|
|
372
|
+
frac_idx = np.interp(0, diff_times.astype(int), index)
|
|
373
|
+
|
|
374
|
+
x1 = np.interp(frac_idx, index, x_proj[valid]).item()
|
|
375
|
+
y1 = np.interp(frac_idx, index, y_proj[valid]).item()
|
|
376
|
+
t1 = np.interp(frac_idx, index, scan_time[valid].astype(int)).item()
|
|
377
|
+
|
|
378
|
+
return x1, y1, np.datetime64(int(t1), "ns")
|
|
379
|
+
|
|
380
|
+
# -------------------------------------------------------------------------------------
|
|
230
381
|
|
|
231
382
|
def _get(self, band: str, processing: str) -> xr.DataArray:
|
|
232
383
|
"""Download Sentinel-2 band to the :attr:`cachestore` and return processed data."""
|
|
@@ -278,6 +429,61 @@ class Sentinel:
|
|
|
278
429
|
|
|
279
430
|
return granule_sink, safe_sink
|
|
280
431
|
|
|
432
|
+
def _get_correction_meta(self) -> tuple[str, str]:
|
|
433
|
+
"""Download Sentinel-2 metadata files and return path to cached files.
|
|
434
|
+
|
|
435
|
+
Note that two XML files must be retrieved: one inside the GRANULE
|
|
436
|
+
subdirectory, and one at the top level of the SAFE archive.
|
|
437
|
+
"""
|
|
438
|
+
fs = self.fs
|
|
439
|
+
base_url = self.base_url
|
|
440
|
+
granule_id = self.granule_id
|
|
441
|
+
|
|
442
|
+
# Resolve the unknown subfolder in DATASTRIP using glob
|
|
443
|
+
# Probably there is a better method, but this worked for now
|
|
444
|
+
pattern = f"{base_url}/DATASTRIP/*"
|
|
445
|
+
matches = fs.glob(pattern)
|
|
446
|
+
if not matches:
|
|
447
|
+
raise FileNotFoundError(f"No DATASTRIP MTD_MDS.xml file found at pattern: {pattern}")
|
|
448
|
+
if len(matches) > 2:
|
|
449
|
+
raise RuntimeError(f"Multiple DATASTRIP MTD_MDS.xml files found: {matches}")
|
|
450
|
+
|
|
451
|
+
datastrip_id = matches[0].split("/")[-1].replace("_$folder$", "")
|
|
452
|
+
url = f"{base_url}/DATASTRIP/{datastrip_id}/MTD_DS.xml"
|
|
453
|
+
datastrip_sink = self.cachestore.path(url.removeprefix(GCP_STRIP_PREFIX))
|
|
454
|
+
if not self.cachestore.exists(datastrip_sink):
|
|
455
|
+
fs.get(url, datastrip_sink)
|
|
456
|
+
|
|
457
|
+
# Path to the QI_DATA folder
|
|
458
|
+
qi_data_url = f"{base_url}/GRANULE/{granule_id}/QI_DATA/"
|
|
459
|
+
|
|
460
|
+
# List files in QI_DATA (assuming fs can list directories)
|
|
461
|
+
files = fs.ls(qi_data_url)
|
|
462
|
+
|
|
463
|
+
# Filter for the detector mask files
|
|
464
|
+
mask_files = [f for f in files if "MSK_DETFOO_B03" in f]
|
|
465
|
+
|
|
466
|
+
if not mask_files:
|
|
467
|
+
raise FileNotFoundError(f"No detector mask found in {qi_data_url}")
|
|
468
|
+
|
|
469
|
+
# Choose the first available file (could prioritize .jp2 over .gml if needed)
|
|
470
|
+
mask_file = None
|
|
471
|
+
for ext in [".jp2", ".gml"]:
|
|
472
|
+
for f in mask_files:
|
|
473
|
+
if f.endswith(ext):
|
|
474
|
+
mask_file = f
|
|
475
|
+
break
|
|
476
|
+
if mask_file:
|
|
477
|
+
break
|
|
478
|
+
|
|
479
|
+
if mask_file is not None:
|
|
480
|
+
# Path where the file will be cached
|
|
481
|
+
detector_band_sink = self.cachestore.path(mask_file.removeprefix(GCP_STRIP_PREFIX))
|
|
482
|
+
if not self.cachestore.exists(detector_band_sink):
|
|
483
|
+
fs.get(mask_file, detector_band_sink)
|
|
484
|
+
|
|
485
|
+
return datastrip_sink, detector_band_sink
|
|
486
|
+
|
|
281
487
|
|
|
282
488
|
def _parse_bands(bands: str | Iterable[str] | None) -> set[str]:
|
|
283
489
|
"""Check that the bands are valid and return as a set."""
|
|
@@ -328,7 +534,7 @@ def _read(path: str, granule_meta: str, safe_meta: str, band: str, processing: s
|
|
|
328
534
|
epsg = int(elem.text.split(":")[1])
|
|
329
535
|
crs = pyproj.CRS.from_epsg(epsg)
|
|
330
536
|
|
|
331
|
-
x, y =
|
|
537
|
+
x, y = read_image_coordinates(granule_meta, band)
|
|
332
538
|
|
|
333
539
|
da = xr.DataArray(
|
|
334
540
|
data=img,
|
|
@@ -345,22 +551,6 @@ def _read(path: str, granule_meta: str, safe_meta: str, band: str, processing: s
|
|
|
345
551
|
return da
|
|
346
552
|
|
|
347
553
|
|
|
348
|
-
def _band_resolution(band: str) -> int:
|
|
349
|
-
"""Get band resolution in meters."""
|
|
350
|
-
return (
|
|
351
|
-
60 if band in ("B01", "B09", "B10") else 10 if band in ("B02", "B03", "B04", "B08") else 20
|
|
352
|
-
)
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
def _band_id(band: str) -> int:
|
|
356
|
-
"""Get band ID used in some metadata files."""
|
|
357
|
-
if band in (f"B{i:2d}" for i in range(1, 9)):
|
|
358
|
-
return int(band[1:]) - 1
|
|
359
|
-
if band == "B8A":
|
|
360
|
-
return 8
|
|
361
|
-
return int(band[1:])
|
|
362
|
-
|
|
363
|
-
|
|
364
554
|
def _read_band_reflectance_rescaling(meta: str, band: str) -> tuple[float, float]:
|
|
365
555
|
"""Read reflectance rescaling factors from metadata file.
|
|
366
556
|
|
|
@@ -393,65 +583,18 @@ def _read_band_reflectance_rescaling(meta: str, band: str) -> tuple[float, float
|
|
|
393
583
|
raise ValueError(msg)
|
|
394
584
|
|
|
395
585
|
|
|
396
|
-
def _read_image_coordinates(meta: str, band: str) -> tuple[np.ndarray, np.ndarray]:
|
|
397
|
-
"""Read image x and y coordinates."""
|
|
398
|
-
|
|
399
|
-
# convenience function that satisfies mypy
|
|
400
|
-
def _text_from_tag(parent: ElementTree.Element, tag: str) -> str:
|
|
401
|
-
elem = parent.find(tag)
|
|
402
|
-
if elem is None or elem.text is None:
|
|
403
|
-
msg = f"Could not find text in {tag} element"
|
|
404
|
-
raise ValueError(msg)
|
|
405
|
-
return elem.text
|
|
406
|
-
|
|
407
|
-
resolution = _band_resolution(band)
|
|
408
|
-
|
|
409
|
-
# find coordinates of upper left corner and pixel size
|
|
410
|
-
tree = ElementTree.parse(meta)
|
|
411
|
-
elems = tree.findall(".//Geoposition")
|
|
412
|
-
for elem in elems:
|
|
413
|
-
if int(elem.attrib["resolution"]) == resolution:
|
|
414
|
-
ulx = float(_text_from_tag(elem, "ULX"))
|
|
415
|
-
uly = float(_text_from_tag(elem, "ULY"))
|
|
416
|
-
dx = float(_text_from_tag(elem, "XDIM"))
|
|
417
|
-
dy = float(_text_from_tag(elem, "YDIM"))
|
|
418
|
-
break
|
|
419
|
-
else:
|
|
420
|
-
msg = f"Could not find image geoposition for resolution of {resolution} m"
|
|
421
|
-
raise ValueError(msg)
|
|
422
|
-
|
|
423
|
-
# find image size
|
|
424
|
-
elems = tree.findall(".//Size")
|
|
425
|
-
for elem in elems:
|
|
426
|
-
if int(elem.attrib["resolution"]) == resolution:
|
|
427
|
-
nx = int(_text_from_tag(elem, "NCOLS"))
|
|
428
|
-
ny = int(_text_from_tag(elem, "NROWS"))
|
|
429
|
-
break
|
|
430
|
-
else:
|
|
431
|
-
msg = f"Could not find image size for resolution of {resolution} m"
|
|
432
|
-
raise ValueError(msg)
|
|
433
|
-
|
|
434
|
-
# compute pixel coordinates
|
|
435
|
-
xlim = (ulx, ulx + (nx - 1) * dx)
|
|
436
|
-
ylim = (uly, uly + (ny - 1) * dy) # dy is < 0
|
|
437
|
-
x = np.linspace(xlim[0], xlim[1], nx)
|
|
438
|
-
y = np.linspace(ylim[0], ylim[1], ny)
|
|
439
|
-
|
|
440
|
-
return x, y
|
|
441
|
-
|
|
442
|
-
|
|
443
586
|
def extract_sentinel_visualization(
|
|
444
587
|
ds: xr.Dataset, color_scheme: str = "true"
|
|
445
|
-
) -> tuple[np.
|
|
588
|
+
) -> tuple[npt.NDArray[np.float32], pyproj.CRS, tuple[float, float, float, float]]:
|
|
446
589
|
"""Extract artifacts for visualizing Sentinel data with the given color scheme.
|
|
447
590
|
|
|
448
591
|
Parameters
|
|
449
592
|
----------
|
|
450
593
|
ds : xr.Dataset
|
|
451
594
|
Dataset of Sentinel data as returned by :meth:`Sentinel.get`.
|
|
452
|
-
color_scheme : str
|
|
595
|
+
color_scheme : str, optional
|
|
453
596
|
Color scheme to use for visualization. The true color scheme
|
|
454
|
-
(the only option currently implemented) requires bands B02, B03, and B04.
|
|
597
|
+
(`"true"`, the only option currently implemented) requires bands B02, B03, and B04.
|
|
455
598
|
|
|
456
599
|
Returns
|
|
457
600
|
-------
|
|
@@ -459,7 +602,7 @@ def extract_sentinel_visualization(
|
|
|
459
602
|
3D RGB array of shape ``(height, width, 3)``.
|
|
460
603
|
src_crs : pyproj.CRS
|
|
461
604
|
Imagery projection
|
|
462
|
-
src_extent : tuple[float,float,float,float]
|
|
605
|
+
src_extent : tuple[float, float, float, float]
|
|
463
606
|
Imagery extent in projected coordinates
|
|
464
607
|
"""
|
|
465
608
|
|
|
@@ -171,7 +171,6 @@ class DryAdvection(models.Model):
|
|
|
171
171
|
|
|
172
172
|
interp_kwargs = self.interp_kwargs
|
|
173
173
|
|
|
174
|
-
dt_integration = self.params["dt_integration"]
|
|
175
174
|
sedimentation_rate = self.params["sedimentation_rate"]
|
|
176
175
|
dz_m = self.params["dz_m"]
|
|
177
176
|
max_depth = self.params["max_depth"]
|
|
@@ -179,6 +178,7 @@ class DryAdvection(models.Model):
|
|
|
179
178
|
source_time = self.source["time"]
|
|
180
179
|
|
|
181
180
|
if timesteps is None:
|
|
181
|
+
dt_integration = self.params["dt_integration"]
|
|
182
182
|
t0 = pd.Timestamp(source_time.min()).floor(pd.Timedelta(dt_integration)).to_numpy()
|
|
183
183
|
t1 = source_time.max()
|
|
184
184
|
timesteps = np.arange(
|
|
@@ -410,7 +410,7 @@ def droplet_apparent_emission_index(
|
|
|
410
410
|
T_exhaust: npt.NDArray[np.floating],
|
|
411
411
|
air_pressure: npt.NDArray[np.floating],
|
|
412
412
|
nvpm_ei_n: npt.NDArray[np.floating],
|
|
413
|
-
vpm_ei_n:
|
|
413
|
+
vpm_ei_n: float,
|
|
414
414
|
G: npt.NDArray[np.floating],
|
|
415
415
|
particles: list[Particle] | None = None,
|
|
416
416
|
n_plume_points: int = 50,
|
|
@@ -429,7 +429,7 @@ def droplet_apparent_emission_index(
|
|
|
429
429
|
Pressure altitude at each waypoint, [:math:`Pa`]
|
|
430
430
|
nvpm_ei_n : npt.NDArray[np.floating]
|
|
431
431
|
nvPM number emissions index, [:math:`kg^{-1}`]
|
|
432
|
-
vpm_ei_n :
|
|
432
|
+
vpm_ei_n : float
|
|
433
433
|
vPM number emissions index, [:math:`kg^{-1}`]
|
|
434
434
|
G : npt.NDArray[np.floating]
|
|
435
435
|
Slope of the mixing line in a temperature-humidity diagram.
|
|
@@ -471,8 +471,8 @@ def droplet_apparent_emission_index(
|
|
|
471
471
|
particles = particles or _default_particles()
|
|
472
472
|
|
|
473
473
|
# Confirm all parameters are broadcastable
|
|
474
|
-
specific_humidity, T_ambient, T_exhaust, air_pressure, G, nvpm_ei_n
|
|
475
|
-
specific_humidity, T_ambient, T_exhaust, air_pressure, G, nvpm_ei_n
|
|
474
|
+
specific_humidity, T_ambient, T_exhaust, air_pressure, G, nvpm_ei_n = np.atleast_1d(
|
|
475
|
+
specific_humidity, T_ambient, T_exhaust, air_pressure, G, nvpm_ei_n
|
|
476
476
|
)
|
|
477
477
|
try:
|
|
478
478
|
np.broadcast(specific_humidity, T_ambient, T_exhaust, air_pressure, G, nvpm_ei_n, vpm_ei_n)
|
|
@@ -665,7 +665,7 @@ def water_droplet_activation(
|
|
|
665
665
|
T_plume: npt.NDArray[np.floating],
|
|
666
666
|
T_ambient: npt.NDArray[np.floating],
|
|
667
667
|
nvpm_ei_n: npt.NDArray[np.floating],
|
|
668
|
-
vpm_ei_n:
|
|
668
|
+
vpm_ei_n: float,
|
|
669
669
|
S_mw: npt.NDArray[np.floating],
|
|
670
670
|
dilution: npt.NDArray[np.floating],
|
|
671
671
|
rho_air: npt.NDArray[np.floating],
|
|
@@ -683,7 +683,7 @@ def water_droplet_activation(
|
|
|
683
683
|
Ambient temperature for each waypoint, [:math:`K`].
|
|
684
684
|
nvpm_ei_n : npt.NDArray[np.floating]
|
|
685
685
|
nvPM number emissions index, [:math:`kg^{-1}`].
|
|
686
|
-
vpm_ei_n :
|
|
686
|
+
vpm_ei_n : float
|
|
687
687
|
vPM number emissions index, [:math:`kg^{-1}`].
|
|
688
688
|
S_mw : npt.NDArray[np.floating]
|
|
689
689
|
Water saturation ratio in the aircraft plume without droplet condensation.
|
|
@@ -790,7 +790,7 @@ def entrained_ambient_droplet_number_concentration(
|
|
|
790
790
|
|
|
791
791
|
|
|
792
792
|
def emissions_index_to_number_concentration(
|
|
793
|
-
number_ei: npt.NDArray[np.floating],
|
|
793
|
+
number_ei: npt.NDArray[np.floating] | float,
|
|
794
794
|
rho_air: npt.NDArray[np.floating],
|
|
795
795
|
dilution: npt.NDArray[np.floating],
|
|
796
796
|
nu_0: float,
|
|
@@ -799,7 +799,7 @@ def emissions_index_to_number_concentration(
|
|
|
799
799
|
|
|
800
800
|
Parameters
|
|
801
801
|
----------
|
|
802
|
-
number_ei : npt.NDArray[np.floating]
|
|
802
|
+
number_ei : npt.NDArray[np.floating] | float
|
|
803
803
|
Particle number emissions index, [:math:`kg^{-1}`].
|
|
804
804
|
rho_air : npt.NDArray[np.floating]
|
|
805
805
|
Air density at each waypoint, [:math:`kg m^{-3}`].
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pycontrails
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.56.0
|
|
4
4
|
Summary: Python library for modeling aviation climate impacts
|
|
5
5
|
Author-email: "Contrails.org" <py@contrails.org>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -88,12 +88,14 @@ Requires-Dist: cartopy>=0.22; extra == "sat"
|
|
|
88
88
|
Requires-Dist: db-dtypes>=1.2; extra == "sat"
|
|
89
89
|
Requires-Dist: gcsfs>=2022.3; extra == "sat"
|
|
90
90
|
Requires-Dist: geojson>=3.1; extra == "sat"
|
|
91
|
+
Requires-Dist: geopandas>=1.1.1; extra == "sat"
|
|
91
92
|
Requires-Dist: google-cloud-bigquery>=3.23; extra == "sat"
|
|
92
93
|
Requires-Dist: google-cloud-bigquery-storage>=2.25; extra == "sat"
|
|
93
94
|
Requires-Dist: pillow>=10.3; extra == "sat"
|
|
94
95
|
Requires-Dist: pyproj>=3.5; extra == "sat"
|
|
95
96
|
Requires-Dist: rasterio>=1.3; extra == "sat"
|
|
96
97
|
Requires-Dist: scikit-image>=0.18; extra == "sat"
|
|
98
|
+
Requires-Dist: shapely>=2.0; extra == "sat"
|
|
97
99
|
Provides-Extra: open3d
|
|
98
100
|
Requires-Dist: open3d>=0.14; extra == "open3d"
|
|
99
101
|
Provides-Extra: pyproj
|
|
@@ -1,29 +1,26 @@
|
|
|
1
1
|
pycontrails/__init__.py,sha256=mKNmGUS5wW1n1PukeaOkmLwQVN24i1__mk0odjBzwEE,2107
|
|
2
|
-
pycontrails/_version.py,sha256=
|
|
2
|
+
pycontrails/_version.py,sha256=zkTAEIXvTjY5VYrpxAXxOfB87yOkpY5j-zTX6WXjfwg,748
|
|
3
3
|
pycontrails/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
4
|
pycontrails/core/__init__.py,sha256=kOAehIZBbvksSW3MuU2DfzsyeE4PaFnOTpYMeq2ZDPE,886
|
|
5
5
|
pycontrails/core/aircraft_performance.py,sha256=dasanaqfm5eP9XUDhgoKGj-eQHTWwMZ_mN6_ZdFulT0,28911
|
|
6
|
-
pycontrails/core/airports.py,sha256=
|
|
7
|
-
pycontrails/core/cache.py,sha256=
|
|
6
|
+
pycontrails/core/airports.py,sha256=muZZ9XkZHi486nbGAYUYILoz_ZH51rU6oWpJPdQYSMc,7010
|
|
7
|
+
pycontrails/core/cache.py,sha256=aQZGS_2SHRV1y_4OfD3Qfq5aCvaXk6SIwdx3aCsqX6I,29050
|
|
8
8
|
pycontrails/core/coordinates.py,sha256=J5qjGuXgbLUw_U9_qREdgOaHl0ngK6Hbbjj3uw7FwNE,5565
|
|
9
|
-
pycontrails/core/fleet.py,sha256=
|
|
10
|
-
pycontrails/core/flight.py,sha256=
|
|
9
|
+
pycontrails/core/fleet.py,sha256=acqsZ1OPnwgGjROPnboPO5JWLGHAi-h6B4Eql73h10Y,17191
|
|
10
|
+
pycontrails/core/flight.py,sha256=Av81Ikr8J4kdG4HcdCaXIu4kY3oX8EenL6QF0B-iTeU,83780
|
|
11
11
|
pycontrails/core/flightplan.py,sha256=9_o_kGQm-yG7MibCgfDCJK8mym2BYgENrizIKl1zzgM,7749
|
|
12
12
|
pycontrails/core/fuel.py,sha256=06YUDhvC8Rx6KbUXRB9qLTsJX2V7tLbzjwAfDH0R6l8,4472
|
|
13
13
|
pycontrails/core/interpolation.py,sha256=-GC3T6yh3nMtt7JCawoYeCUnDNRY9GHhxhkRhhnntxE,26437
|
|
14
14
|
pycontrails/core/met.py,sha256=NBboBTRKNt5WFSnKUdm2R_9N68EQTYii4P5A_hs79YQ,106898
|
|
15
|
-
pycontrails/core/met_var.py,sha256=
|
|
15
|
+
pycontrails/core/met_var.py,sha256=xGkhIy_uelGo6gKM9rwh6QiUA0_7D0hBgO0Xa0IFi8k,12414
|
|
16
16
|
pycontrails/core/models.py,sha256=MRDNYVr8WMTF5EJrwZ8zxPHKKMcU09apBcqycimCWwk,45236
|
|
17
17
|
pycontrails/core/polygon.py,sha256=3_vYmlQoP3x3lmgwFyqQVgl9ziAQ5e160MCm2fwFou0,18619
|
|
18
|
-
pycontrails/core/rgi_cython.cp310-win_amd64.pyd,sha256=
|
|
19
|
-
pycontrails/core/vector.py,sha256=
|
|
18
|
+
pycontrails/core/rgi_cython.cp310-win_amd64.pyd,sha256=1PwaYZ9MfM9ILAkc3LUvVKqdTUQMRktzoVWEcfAoygA,234496
|
|
19
|
+
pycontrails/core/vector.py,sha256=aF1Ni9ANZkBtoA9gfESWv9fM5fGtkmY-uQjF1Zx3Z3Q,75904
|
|
20
20
|
pycontrails/datalib/__init__.py,sha256=Q2RrnjwtFzfsmJ2tEojDCzDMkd8R0MYw4mQz3YwUsqI,381
|
|
21
21
|
pycontrails/datalib/goes.py,sha256=HTjnh147GZQfYX7R0zGVddwMuCHZQJp4cWqthGLETHY,35321
|
|
22
|
-
pycontrails/datalib/landsat.py,sha256=
|
|
23
|
-
pycontrails/datalib/sentinel.py,sha256
|
|
24
|
-
pycontrails/datalib/_leo_utils/search.py,sha256=8JzT56ps3SH1W-5rwL8BWuxLLljwxa_5fjLAuZdL_Vg,8937
|
|
25
|
-
pycontrails/datalib/_leo_utils/vis.py,sha256=0UDVcqMRqHmAORDV4Xyk-HVnTAjbOCf7KCpWm2ilTLE,1861
|
|
26
|
-
pycontrails/datalib/_leo_utils/static/bq_roi_query.sql,sha256=r_gVjpoEvCcAJP56QlXaXzgfWPZdf-kYo3D316glJLU,266
|
|
22
|
+
pycontrails/datalib/landsat.py,sha256=VUJ3BN3NXRIONWH2HMfTEZeHWUGO7ItAXVsGF0ZdWm8,21504
|
|
23
|
+
pycontrails/datalib/sentinel.py,sha256=-aV8TW9cyeX6mrfyuQE6YXDRkqhxy9XYQ84A3q2jPIQ,24182
|
|
27
24
|
pycontrails/datalib/_met_utils/metsource.py,sha256=fCObXHCKo1--v22SRayZDzWlnz0lxTMdGVNhhUyywlM,24900
|
|
28
25
|
pycontrails/datalib/ecmwf/__init__.py,sha256=9EkfWlGki8LYt7ySKf87gS8RzZjAOxK2w87_Sok3CCo,2094
|
|
29
26
|
pycontrails/datalib/ecmwf/arco_era5.py,sha256=PojAfT0N12SLcgiecZtHiN96sbRWwFx3PThrXIwSX5M,12782
|
|
@@ -39,6 +36,13 @@ pycontrails/datalib/ecmwf/static/model_level_dataframe_v20240418.csv,sha256=9u7C
|
|
|
39
36
|
pycontrails/datalib/gfs/__init__.py,sha256=DGd8twOXwRZZhHx5muc9SJT-YET1KB599kS45_x3IbY,712
|
|
40
37
|
pycontrails/datalib/gfs/gfs.py,sha256=qh_nMDaSqkGs-YgibsmDWWXGtn6HY7EUqv5tve7IK5s,23055
|
|
41
38
|
pycontrails/datalib/gfs/variables.py,sha256=gmw5cs8RAeB-s9kCbnuKFp1K2SqNbc0lNR-JqhcenZY,3239
|
|
39
|
+
pycontrails/datalib/leo_utils/__init__.py,sha256=7MgrgpWmlr71fIn6pQIEaRffuvfavlhLz-gQnfUBxIY,218
|
|
40
|
+
pycontrails/datalib/leo_utils/correction.py,sha256=cOceA702SXtmBvUsG2JJx0MKnsoGWng6469fpfMJjFQ,9830
|
|
41
|
+
pycontrails/datalib/leo_utils/landsat_metadata.py,sha256=lQ34ARIRFIhN5lU5MRhg9s_GpmZ_OkjJWs5_0hYFYok,10928
|
|
42
|
+
pycontrails/datalib/leo_utils/search.py,sha256=f4CjKbkGVZ7frc4GAb3rsKPukmEeBbaYVY_S4uCjQjQ,8936
|
|
43
|
+
pycontrails/datalib/leo_utils/sentinel_metadata.py,sha256=vVJMIubmR9V_bNG9kiTygMHo8-Ehds7spMaOzkpSgds,26254
|
|
44
|
+
pycontrails/datalib/leo_utils/vis.py,sha256=0UDVcqMRqHmAORDV4Xyk-HVnTAjbOCf7KCpWm2ilTLE,1861
|
|
45
|
+
pycontrails/datalib/leo_utils/static/bq_roi_query.sql,sha256=r_gVjpoEvCcAJP56QlXaXzgfWPZdf-kYo3D316glJLU,266
|
|
42
46
|
pycontrails/datalib/spire/__init__.py,sha256=HTeQFjMc1BJ189fALx5NDGX2IVZ0AUWC_Xu6LCsZduk,191
|
|
43
47
|
pycontrails/datalib/spire/exceptions.py,sha256=G8A68hbwPLYsXpluw7Q6TYJ1cJL-BmkKsiz93fHk-Kc,1776
|
|
44
48
|
pycontrails/datalib/spire/spire.py,sha256=u9nq4LCKi_itL6vvjlcte-wKrx9h_b62yQDkw3QOYKw,24846
|
|
@@ -48,8 +52,8 @@ pycontrails/ext/empirical_grid.py,sha256=mveQltokaGeQcxxbdMSLQ6wQ14oh3XX5dfzjWaF
|
|
|
48
52
|
pycontrails/ext/synthetic_flight.py,sha256=dEWm9vrg6SAWieh6GLAE0m1orTrApav8HHP42-4bIHg,17233
|
|
49
53
|
pycontrails/models/__init__.py,sha256=TKhrXe1Pu1-mV1gctx8cUAMrVxCCAtBkbZi9olfWq8s,34
|
|
50
54
|
pycontrails/models/accf.py,sha256=rbEn6oTqXsgDPA3Ky0y-bADHWTxGXixa8OwpHH_pXag,13991
|
|
51
|
-
pycontrails/models/dry_advection.py,sha256=
|
|
52
|
-
pycontrails/models/extended_k15.py,sha256=
|
|
55
|
+
pycontrails/models/dry_advection.py,sha256=UhpeSApTN03IRmnWhBKF-rPjy0DiUJQ8M-ngg1Ix5cc,21098
|
|
56
|
+
pycontrails/models/extended_k15.py,sha256=lOD0qLJM8LTFCe2UtZAxFBUm7FIXZeX8CdRDdTXH-l8,49305
|
|
53
57
|
pycontrails/models/issr.py,sha256=1pqijM2ecEHhmclSRodyZNnj1f-rCn2H_d44SQYo25I,7614
|
|
54
58
|
pycontrails/models/pcc.py,sha256=7k8kICqDeZ99O2n2Zpnu7EFNGjEpPka_9cu9nrmP44s,11394
|
|
55
59
|
pycontrails/models/pcr.py,sha256=YI7kd7BRdkHt970LCg0DhAOj-Bfukw0jclfvXGNo1jE,5528
|
|
@@ -106,9 +110,9 @@ pycontrails/utils/iteration.py,sha256=En2YY4NiNwCNtAVO8HL6tv9byBGKs8MKSI7R8P-gZy
|
|
|
106
110
|
pycontrails/utils/json.py,sha256=Pqashwoupuf_GfrrSfHclwug9Hg-kYQ4WNxEqay_0Rc,6083
|
|
107
111
|
pycontrails/utils/temp.py,sha256=5XXqQoEfWjz1OrhoOBZD5vkkCFeuq9LpZkyhc38gIeY,1159
|
|
108
112
|
pycontrails/utils/types.py,sha256=WJS3rnYxU4l8eEJuJVWGBRD7-zvEBlRD8TQx-fxKbic,5032
|
|
109
|
-
pycontrails-0.
|
|
110
|
-
pycontrails-0.
|
|
111
|
-
pycontrails-0.
|
|
112
|
-
pycontrails-0.
|
|
113
|
-
pycontrails-0.
|
|
114
|
-
pycontrails-0.
|
|
113
|
+
pycontrails-0.56.0.dist-info/licenses/LICENSE,sha256=HVr8JnZfTaA-12BfKUQZi5hdrB3awOwLWs5X_ga5QzA,10353
|
|
114
|
+
pycontrails-0.56.0.dist-info/licenses/NOTICE,sha256=VIhzKNYi4lQx6fpZyqiY6eMHpLuwp-_G0JQkmYYa7h0,2005
|
|
115
|
+
pycontrails-0.56.0.dist-info/METADATA,sha256=LXRlDfGkCQZOY35fv6ZQIw4WhzpfT39zuu3uSEIfocU,9308
|
|
116
|
+
pycontrails-0.56.0.dist-info/WHEEL,sha256=KUuBC6lxAbHCKilKua8R9W_TM71_-9Sg5uEP3uDWcoU,101
|
|
117
|
+
pycontrails-0.56.0.dist-info/top_level.txt,sha256=dwaYXVcMhF92QWtAYcLvL0k02vyBqwhsv92lYs2V6zQ,23
|
|
118
|
+
pycontrails-0.56.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|