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

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

Potentially problematic release.


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

@@ -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._leo_utils import search
15
- from pycontrails.datalib._leo_utils.vis import equalize, normalize
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
- return search.intersect(BQ_TABLE, flight, columns)
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 | set[str] | None
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 = {"raw", "reflectance"}, optional
214
- Whether to return raw values or rescaled reflectances for reflective bands.
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.DataArray
220
- DataArray of Sentinel-2 data.
230
+ xr.Dataset
231
+ Dataset of Sentinel-2 data.
221
232
  """
222
- if reflective not in ["raw", "reflectance"]:
223
- msg = "reflective band processing must be one of ['raw', 'radiance', 'reflectance']"
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
- ds = xr.Dataset()
227
- for band in self.bands:
228
- ds[band] = self._get(band, reflective)
229
- return ds
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 = _read_image_coordinates(granule_meta, band)
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.ndarray, pyproj.CRS, tuple[float, float, float, float]]:
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 = {"true"}
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: npt.NDArray[np.floating],
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 : npt.NDArray[np.floating]
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, vpm_ei_n = np.atleast_1d(
475
- specific_humidity, T_ambient, T_exhaust, air_pressure, G, nvpm_ei_n, vpm_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: npt.NDArray[np.floating],
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 : npt.NDArray[np.floating]
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.55.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,32 +1,32 @@
1
- pycontrails-0.55.0.dist-info/RECORD,,
2
- pycontrails-0.55.0.dist-info/WHEEL,sha256=11kMdE9gzbsaQG30fRcsAYxBLEVRsqJo098Y5iL60Xo,136
3
- pycontrails-0.55.0.dist-info/top_level.txt,sha256=Z8J1R_AiBAyCVjNw6jYLdrA68PrQqTg0t3_Yek_IZ0Q,29
4
- pycontrails-0.55.0.dist-info/METADATA,sha256=H2xDW4TtardGnAmR-wScAjXNDEk5BRx1vGaFOca92NY,9036
5
- pycontrails-0.55.0.dist-info/licenses/LICENSE,sha256=gJ-h7SFFD1mCfR6a7HILvEtodDT6Iig8bLXdgqR6ucA,10175
6
- pycontrails-0.55.0.dist-info/licenses/NOTICE,sha256=fiBPdjYibMpDzf8hqcn7TvAQ-yeK10q_Nqq24DnskYg,1962
7
- pycontrails/_version.py,sha256=MUSeiBePddUfMUmju5N5hbz2jr6ICfnUqMwuaH-AT0Y,714
1
+ pycontrails-0.56.0.dist-info/RECORD,,
2
+ pycontrails-0.56.0.dist-info/WHEEL,sha256=11kMdE9gzbsaQG30fRcsAYxBLEVRsqJo098Y5iL60Xo,136
3
+ pycontrails-0.56.0.dist-info/top_level.txt,sha256=Z8J1R_AiBAyCVjNw6jYLdrA68PrQqTg0t3_Yek_IZ0Q,29
4
+ pycontrails-0.56.0.dist-info/METADATA,sha256=V-rmySZx3DVD4x1SN4wcN9yqATXqErO7zoaXg6l2JNc,9128
5
+ pycontrails-0.56.0.dist-info/licenses/LICENSE,sha256=gJ-h7SFFD1mCfR6a7HILvEtodDT6Iig8bLXdgqR6ucA,10175
6
+ pycontrails-0.56.0.dist-info/licenses/NOTICE,sha256=fiBPdjYibMpDzf8hqcn7TvAQ-yeK10q_Nqq24DnskYg,1962
7
+ pycontrails/_version.py,sha256=2_q1TRVy1QpIW3N_C_ZBLtNup6qZoUaN3O-tBxeD494,714
8
8
  pycontrails/__init__.py,sha256=9ypSB2fKZlKghTvSrjWo6OHm5qfASwiTIvlMew3Olu4,2037
9
9
  pycontrails/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
- pycontrails/core/vector.py,sha256=X_g8lzY6plJ6oeUHigSjt9qcPv34a3m1DeK1pqocrDw,73627
10
+ pycontrails/core/vector.py,sha256=mQG1NmULztZjTK7O4waiggiR_4dA7LhTeuyQnLJRHsE,73650
11
11
  pycontrails/core/models.py,sha256=3mDTqp1V5aae9akuYwbMGIUEkESKSYTjZeyu2IiMW7s,43915
12
12
  pycontrails/core/interpolation.py,sha256=wovjj3TAf3xonVxjarclpvZLyLq6N7wZQQXsI9hT3YA,25713
13
- pycontrails/core/fleet.py,sha256=0hi_N4R93St-7iD29SE0EnadpBEl_p9lSGtDwpWvGkk,16704
14
- pycontrails/core/rgi_cython.cpython-310-darwin.so,sha256=3re4urYJmrg5WBv-BLynhlUlsKzmbZM3-26G0qfDD1U,343056
15
- pycontrails/core/flight.py,sha256=kQ78YdvjPZI6v2Bj_2Fr1MgNmrrtIN6j21l4fwcRW4E,81380
13
+ pycontrails/core/fleet.py,sha256=3uRByONZW8yHiS2gmgwOozVm6ksUVdsy6Gyf6jYjSPY,16703
14
+ pycontrails/core/rgi_cython.cpython-310-darwin.so,sha256=YqyXkw5wBU18DdWEyO--Z495pq_wHGOgjubrnHuQLzo,343056
15
+ pycontrails/core/flight.py,sha256=_a4AJjewA_lcW_qjBPga9z6JlVAev-es_M8TFVyX_1E,81590
16
16
  pycontrails/core/fuel.py,sha256=kJZ3P1lPm1L6rdPREM55XQ-VfJ_pt35cP4sO2Nnvmjs,4332
17
17
  pycontrails/core/polygon.py,sha256=g7YqWzUbOHWT65XrLqLUZLrQXYcx_x1NcJ041-Cj7UY,18070
18
- pycontrails/core/cache.py,sha256=IIyx726zN7JzNSKV0JJDksMI9OhCLdnJShmBVStRqzI,28154
18
+ pycontrails/core/cache.py,sha256=Zng2edpSKYNyrkD4j1omYMCzJ7Lm762YJnUmqoVu1Z4,28161
19
19
  pycontrails/core/__init__.py,sha256=p0O09HxdeXU0X5Z3zrHMlTfXa92YumT3fJ8wJBI5ido,856
20
20
  pycontrails/core/flightplan.py,sha256=0mvA3IO19Sap-7gwpmEIV35_mg6ChvajwhurvjZZt_U,7521
21
21
  pycontrails/core/met.py,sha256=O9W6RaEwUsg7ZERR47Q-6fYjg13BzOZtcQdw92444xg,103987
22
22
  pycontrails/core/aircraft_performance.py,sha256=Kk_Rb61jDOWPmCQHwn2jR5vMPmB8b3aq1iTWfiUMj9U,28232
23
- pycontrails/core/airports.py,sha256=ubYo-WvxKPd_dUcADx6yew9Tqh1a4VJDgX7aFqLYwB8,6775
24
- pycontrails/core/met_var.py,sha256=lAbp3cko_rzMk_u0kq-F27sUXUxUKikUvCNycwp9ILY,12020
23
+ pycontrails/core/airports.py,sha256=CzZrgJNZ7wtNv8vg9sJczMhFov7k0gmrGR4tRKCH8i8,6782
24
+ pycontrails/core/met_var.py,sha256=g69vqbxpJeXEQU8vrrcoUR1PX3zCo2-k3au1Lv2TiIw,12027
25
25
  pycontrails/core/coordinates.py,sha256=0ySsHtqTon7GMbuwmmxMbI92j3ueMteJZh4xxNm5zto,5391
26
26
  pycontrails/datalib/goes.py,sha256=_TB32tGWxumASOEZUQ_PwGWz8QMZPTJf8pG4jbPR-WY,34358
27
- pycontrails/datalib/landsat.py,sha256=r6366rEF7fOA7mT5KySCPGJplgGE5LvBw5fMqk-U1oM,19697
27
+ pycontrails/datalib/landsat.py,sha256=6ylDkAjnyX7b4ZbHn4bprO8HB8ADPFyMkwWehIs8FLg,20915
28
28
  pycontrails/datalib/__init__.py,sha256=hW9NWdFPC3y_2vHMteQ7GgQdop3917MkDaf5ZhU2RBY,369
29
- pycontrails/datalib/sentinel.py,sha256=hYSxIlQnyJHqtHWlKn73HOK_1pm-_IbGebmkHnh4UcA,17172
29
+ pycontrails/datalib/sentinel.py,sha256=ed1l1avq8lBvQinY_vNSsWRcpqxUdAPY61AGyPcLawo,23532
30
30
  pycontrails/datalib/_met_utils/metsource.py,sha256=B4Gd9gkfMMlXe-xc_xcNNZAJ0gOeRelvrBsFyk6tEs4,24151
31
31
  pycontrails/datalib/ecmwf/arco_era5.py,sha256=7HXQU5S02PzX9Ew2ZrDKSp0tDEG1eeVAvbP3decmm20,12437
32
32
  pycontrails/datalib/ecmwf/era5.py,sha256=4ULNdDlUN0kP6Tbp8D_-Bc12nAsLf0iNfZaDoj_AoZU,18952
@@ -39,15 +39,19 @@ pycontrails/datalib/ecmwf/common.py,sha256=qRMSzDQikGMi3uqvz-Y57e3biHPzSoVMfUwOu
39
39
  pycontrails/datalib/ecmwf/model_levels.py,sha256=_kgpnogaS6MlfvTX9dB5ASTHFUlZuQ_DRb-VADwEa0k,16996
40
40
  pycontrails/datalib/ecmwf/ifs.py,sha256=0swHe6tFc5Fbu9e4_jREW0H-xYHYLtxjNoE3aUUlgvc,10761
41
41
  pycontrails/datalib/ecmwf/static/model_level_dataframe_v20240418.csv,sha256=PmvGLRzn6uuCKSwiasSuVcehvvmSaqP7cnLuN6hhCQQ,9788
42
- pycontrails/datalib/_leo_utils/vis.py,sha256=-fLcm1D5cP6lThVHovV3MJSiadWyTUAvYDMvr4drMU4,1802
43
- pycontrails/datalib/_leo_utils/search.py,sha256=r87T2OV4qH1pYI2YznvsBL042f4RKxD3OA2snd3-kDI,8687
44
- pycontrails/datalib/_leo_utils/static/bq_roi_query.sql,sha256=xq6-tJyz0-bUwW0KjQymqygjH3WlQBmyBtP7Ci7SBe8,260
45
42
  pycontrails/datalib/gfs/gfs.py,sha256=3tFiR7IObHcFmhGOdb-SJ7QQJSk6tF_6qkyi-pLrIdE,22393
46
43
  pycontrails/datalib/gfs/variables.py,sha256=4ALR4zhYW8tQVlNVHrd0CK8oRNSe_2OkW3ELeaImtAI,3135
47
44
  pycontrails/datalib/gfs/__init__.py,sha256=pXNjb9cJC6ngpuCnoHnmVZ2RHzbHZ0AlsyGvgcdcl2E,684
48
45
  pycontrails/datalib/spire/spire.py,sha256=h25BVgSr7E71Ox3-y9WgqFvp-54L08yzb2Ou-iMl7wM,24242
49
46
  pycontrails/datalib/spire/__init__.py,sha256=3-My8yQItS6PL0DqXgNaltLqvN6T7nbnNnLD-sy7kt4,186
50
47
  pycontrails/datalib/spire/exceptions.py,sha256=U0V_nZTLhxJwrzldvU9PdESx8-zLddRH3FmzkJyFyrI,1714
48
+ pycontrails/datalib/leo_utils/sentinel_metadata.py,sha256=h5ieEwtmeAAtiIBdfOF8HaoE121ba0DrKb7dLvuaAeI,25506
49
+ pycontrails/datalib/leo_utils/landsat_metadata.py,sha256=B455-Yq6HTj0Se0dS4c_2F5ZjcATu2yNK1gyoIlgLMg,10628
50
+ pycontrails/datalib/leo_utils/__init__.py,sha256=-SEAc1f7zEbJHcKjgwLuhnIwte9W-ystFNLvfC4RE94,213
51
+ pycontrails/datalib/leo_utils/vis.py,sha256=-fLcm1D5cP6lThVHovV3MJSiadWyTUAvYDMvr4drMU4,1802
52
+ pycontrails/datalib/leo_utils/search.py,sha256=KbHQ2GARacDuUz3zEJuATSga-R32dQFVTqhZgndHUZI,8686
53
+ pycontrails/datalib/leo_utils/correction.py,sha256=cHf4PhHNYMqdVAFYNiTnjcVyqr1vCBMCKi0IjKB_3pw,9564
54
+ pycontrails/datalib/leo_utils/static/bq_roi_query.sql,sha256=xq6-tJyz0-bUwW0KjQymqygjH3WlQBmyBtP7Ci7SBe8,260
51
55
  pycontrails/ext/synthetic_flight.py,sha256=wROBQErfr_IhEPndC97fuWbnZQega2Z89VhzoXzZMO8,16802
52
56
  pycontrails/ext/cirium.py,sha256=DFPfRwLDwddpucAPRQhyT4bDGh0VvvoViMUd3pidam8,415
53
57
  pycontrails/ext/empirical_grid.py,sha256=FPNQA0x4nVwBXFlbs3DgIapSrXFYhoc8b8IX0M4xhBc,4363
@@ -58,14 +62,14 @@ pycontrails/utils/types.py,sha256=1AaY1x_qGlYAl08xg6PS0MPKm3OZwFBM7xLI_nHK7EY,48
58
62
  pycontrails/utils/temp.py,sha256=lGU0b_R8ze4yKlsOusHIIBaoNFBrmrB3vBjgHRlfcXk,1109
59
63
  pycontrails/utils/json.py,sha256=oTiO8xh603esfBGaGVmA5eUzR0NhAqNpQCegMMgnSbg,5896
60
64
  pycontrails/utils/dependencies.py,sha256=ATP45xYdUbIyGFzgbOe5SbokMytvB84TcexUEFnEUZE,2559
61
- pycontrails/models/extended_k15.py,sha256=2-j7ScdQnQLrZhzYJMTCUnOPBwy08p0fqZUNA3rYwSM,48058
65
+ pycontrails/models/extended_k15.py,sha256=uZ32wC5HNCJ5M9u4V4x10QoXn2hZRZy1tKcSvNJE0K4,47978
62
66
  pycontrails/models/pcc.py,sha256=0Qdl4u8PmUEpNYd398glTChkbTwsh83wYPt0Bmi8qd8,11068
63
67
  pycontrails/models/tau_cirrus.py,sha256=2Z4egt-QFprkyITRgtarA5alOTTQRQbjzgmSqE49_1g,5778
64
68
  pycontrails/models/__init__.py,sha256=dQTOLQb7RdUdUwslt5se__5y_ymbInBexQmNrmAeOdE,33
65
69
  pycontrails/models/issr.py,sha256=_qIKDgO0Owxeb0Q4WJlxcn1FJEvF3QDU-cqh2fpDsBo,7404
66
70
  pycontrails/models/sac.py,sha256=8Vx5wg4-Kb8l4GK67wp7VNVpdFM4Wyux1xKuNrjZ_IQ,15516
67
71
  pycontrails/models/accf.py,sha256=_tunWpw1sYW8ES8RvpdhNahXwaf4LwdHMEdXhv7-cCI,13566
68
- pycontrails/models/dry_advection.py,sha256=fy2Oa6FtYvjG-IrX2vMQe1QJ6pnRaRj4ZonECBdg5KA,20492
72
+ pycontrails/models/dry_advection.py,sha256=3Vf-oug6an4WRHBOMlXZMsD7B6lx-ieMr-8mkSQyP3c,20496
69
73
  pycontrails/models/pcr.py,sha256=Xde0aF8cMV9jTQ_uI2UvdHSLqotVUgPutb1Wgq7LtfY,5374
70
74
  pycontrails/models/emissions/__init__.py,sha256=CZB2zIkLUI3NGNmq2ddvRYjEtiboY6PWJjiEiXj_zII,478
71
75
  pycontrails/models/emissions/ffm2.py,sha256=mAvBHnp-p3hIn2fjKGq50eaMHi0jcb5hA5uXbJGeE9I,12068
File without changes