pycontrails 0.52.2__cp311-cp311-macosx_11_0_arm64.whl → 0.53.0__cp311-cp311-macosx_11_0_arm64.whl

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

Potentially problematic release.


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

Files changed (37) hide show
  1. pycontrails/_version.py +2 -2
  2. pycontrails/core/cache.py +1 -1
  3. pycontrails/core/flight.py +8 -5
  4. pycontrails/core/flightplan.py +1 -1
  5. pycontrails/core/interpolation.py +3 -1
  6. pycontrails/core/met.py +190 -15
  7. pycontrails/core/met_var.py +1 -1
  8. pycontrails/core/models.py +5 -5
  9. pycontrails/core/rgi_cython.cpython-311-darwin.so +0 -0
  10. pycontrails/core/vector.py +5 -5
  11. pycontrails/datalib/_leo_utils/vis.py +10 -11
  12. pycontrails/datalib/_met_utils/metsource.py +13 -11
  13. pycontrails/datalib/ecmwf/era5.py +1 -1
  14. pycontrails/datalib/ecmwf/era5_model_level.py +1 -1
  15. pycontrails/datalib/ecmwf/hres_model_level.py +3 -3
  16. pycontrails/datalib/ecmwf/variables.py +3 -3
  17. pycontrails/datalib/gfs/gfs.py +4 -3
  18. pycontrails/datalib/landsat.py +10 -9
  19. pycontrails/ext/synthetic_flight.py +1 -1
  20. pycontrails/models/accf.py +1 -1
  21. pycontrails/models/apcemm/apcemm.py +5 -5
  22. pycontrails/models/cocip/cocip.py +98 -24
  23. pycontrails/models/cocip/cocip_params.py +21 -0
  24. pycontrails/models/cocip/output_formats.py +13 -4
  25. pycontrails/models/cocip/radiative_forcing.py +3 -3
  26. pycontrails/models/cocipgrid/cocip_grid.py +4 -4
  27. pycontrails/models/ps_model/ps_model.py +4 -4
  28. pycontrails/models/sac.py +2 -2
  29. pycontrails/physics/thermo.py +1 -1
  30. pycontrails/utils/json.py +16 -18
  31. pycontrails/utils/types.py +7 -6
  32. {pycontrails-0.52.2.dist-info → pycontrails-0.53.0.dist-info}/METADATA +78 -78
  33. {pycontrails-0.52.2.dist-info → pycontrails-0.53.0.dist-info}/RECORD +37 -37
  34. {pycontrails-0.52.2.dist-info → pycontrails-0.53.0.dist-info}/WHEEL +1 -1
  35. {pycontrails-0.52.2.dist-info → pycontrails-0.53.0.dist-info}/LICENSE +0 -0
  36. {pycontrails-0.52.2.dist-info → pycontrails-0.53.0.dist-info}/NOTICE +0 -0
  37. {pycontrails-0.52.2.dist-info → pycontrails-0.53.0.dist-info}/top_level.txt +0 -0
@@ -377,7 +377,7 @@ class HRESModelLevel(ECMWFAPI):
377
377
  time = self.forecast_time.strftime("%H:%M:%S")
378
378
  steps = self.get_forecast_steps(times)
379
379
  # param 152 = log surface pressure, needed for metview level conversion
380
- grib_params = set(self.variable_ecmwfids + [152])
380
+ grib_params = set((*self.variable_ecmwfids, 152))
381
381
  return (
382
382
  f"retrieve,\n"
383
383
  f"class=od,\n"
@@ -473,8 +473,8 @@ class HRESModelLevel(ECMWFAPI):
473
473
  LOG.debug("Opening GRIB file")
474
474
  fs_ml = mv.read(target)
475
475
 
476
- # reduce memory overhead by cacheing one timestep at a time
477
- for time, step in zip(times, self.get_forecast_steps(times)):
476
+ # reduce memory overhead by caching one timestep at a time
477
+ for time, step in zip(times, self.get_forecast_steps(times), strict=True):
478
478
  fs_pl = mv.Fieldset()
479
479
  selection = dict(step=step)
480
480
  lnsp = fs_ml.select(shortName="lnsp", **selection)
@@ -118,7 +118,7 @@ RelativeHumidity = MetVariable(
118
118
  "At temperatures below -23°C it is calculated for saturation over ice. "
119
119
  "Between -23°C and 0°C this parameter is calculated by interpolating between the ice and"
120
120
  " water values using a quadratic function."
121
- "See https://www.ecmwf.int/sites/default/files/elibrary/2016/17117-part-iv-physical-processes.pdf#subsection.7.4.2" # noqa: E501
121
+ "See https://www.ecmwf.int/sites/default/files/elibrary/2016/17117-part-iv-physical-processes.pdf#subsection.7.4.2"
122
122
  ),
123
123
  )
124
124
 
@@ -166,7 +166,7 @@ TopNetSolarRadiation = MetVariable(
166
166
  "The incoming solar radiation is the amount received from the Sun. "
167
167
  "The outgoing solar radiation is the amount reflected and scattered by the Earth's"
168
168
  " atmosphere and surface"
169
- "See https://www.ecmwf.int/sites/default/files/elibrary/2015/18490-radiation-quantities-ecmwf-model-and-mars.pdf" # noqa: E501
169
+ "See https://www.ecmwf.int/sites/default/files/elibrary/2015/18490-radiation-quantities-ecmwf-model-and-mars.pdf"
170
170
  ),
171
171
  )
172
172
 
@@ -183,7 +183,7 @@ TopNetThermalRadiation = MetVariable(
183
183
  "radiation emitted to space at the top of the atmosphere is commonly known as the Outgoing"
184
184
  " Longwave Radiation (OLR). "
185
185
  "The top net thermal radiation (this parameter) is equal to the negative of OLR."
186
- "See https://www.ecmwf.int/sites/default/files/elibrary/2015/18490-radiation-quantities-ecmwf-model-and-mars.pdf" # noqa: E501
186
+ "See https://www.ecmwf.int/sites/default/files/elibrary/2015/18490-radiation-quantities-ecmwf-model-and-mars.pdf"
187
187
  ),
188
188
  )
189
189
 
@@ -13,8 +13,9 @@ import hashlib
13
13
  import logging
14
14
  import pathlib
15
15
  import warnings
16
+ from collections.abc import Callable
16
17
  from datetime import datetime
17
- from typing import TYPE_CHECKING, Any, Callable
18
+ from typing import TYPE_CHECKING, Any
18
19
 
19
20
  import numpy as np
20
21
  import pandas as pd
@@ -113,7 +114,7 @@ class GFSForecast(metsource.MetDataSource):
113
114
  - `Documentation <https://www.ncei.noaa.gov/products/weather-climate-models/global-forecast>`_
114
115
  - `Parameter sets <https://www.nco.ncep.noaa.gov/pmb/products/gfs/>`_
115
116
  - `GFS Documentation <https://www.emc.ncep.noaa.gov/emc/pages/numerical_forecast_systems/gfs/documentation.php>`_
116
- """ # noqa: E501
117
+ """
117
118
 
118
119
  __slots__ = ("client", "grid", "cachestore", "show_progress", "forecast_time")
119
120
 
@@ -495,7 +496,7 @@ class GFSForecast(metsource.MetDataSource):
495
496
  GFS dataset
496
497
  """
497
498
  # translate into netcdf from grib
498
- logger.debug(f"Translating {filepath} for timestep {str(t)} into netcdf")
499
+ logger.debug(f"Translating {filepath} for timestep {t!s} into netcdf")
499
500
 
500
501
  # get step for timestep
501
502
  step = pd.Timedelta(t - self.forecast_time) // pd.Timedelta(1, "h")
@@ -33,15 +33,6 @@ except ModuleNotFoundError as exc:
33
33
  pycontrails_optional_package="sat",
34
34
  )
35
35
 
36
- try:
37
- import rasterio
38
- except ModuleNotFoundError as exc:
39
- dependencies.raise_module_not_found_error(
40
- name="landsat module",
41
- package_name="rasterio",
42
- module_not_found_error=exc,
43
- pycontrails_optional_package="sat",
44
- )
45
36
 
46
37
  #: BigQuery table with imagery metadata
47
38
  BQ_TABLE = "bigquery-public-data.cloud_storage_geo_index.landsat_index"
@@ -313,6 +304,16 @@ def _check_band_resolution(bands: set[str]) -> None:
313
304
 
314
305
  def _read(path: str, meta: str, band: str, processing: str) -> xr.DataArray:
315
306
  """Read imagery data from Landsat files."""
307
+ try:
308
+ import rasterio
309
+ except ModuleNotFoundError as exc:
310
+ dependencies.raise_module_not_found_error(
311
+ name="landsat module",
312
+ package_name="rasterio",
313
+ module_not_found_error=exc,
314
+ pycontrails_optional_package="sat",
315
+ )
316
+
316
317
  src = rasterio.open(path)
317
318
  img = src.read(1)
318
319
  crs = pyproj.CRS.from_epsg(src.crs.to_epsg())
@@ -417,7 +417,7 @@ class SyntheticFlight:
417
417
  times_arr = np.asarray(times).T
418
418
  data = [
419
419
  {"longitude": lon, "latitude": lat, "level": level, "time": time}
420
- for lon, lat, level, time in zip(lons_arr, lats_arr, level, times_arr)
420
+ for lon, lat, level, time in zip(lons_arr, lats_arr, level, times_arr, strict=True)
421
421
  ]
422
422
  dfs = [pd.DataFrame(d).dropna() for d in data]
423
423
  dfs = [df for df in dfs if len(df) >= self.min_n_waypoints]
@@ -154,7 +154,7 @@ class ACCF(Model):
154
154
  sur_variables = (ecmwf.SurfaceSolarDownwardRadiation, ecmwf.TopNetThermalRadiation)
155
155
  default_params = ACCFParams
156
156
 
157
- short_vars = {v.short_name for v in (*met_variables, *sur_variables)}
157
+ short_vars = frozenset(v.short_name for v in (*met_variables, *sur_variables))
158
158
 
159
159
  # This variable won't get used since we are not writing the output
160
160
  # anywhere, but the library will complain if it's not defined
@@ -84,10 +84,10 @@ class APCEMMParams(models.ModelParams):
84
84
  engine_uid: str | None = None
85
85
 
86
86
  #: Aircraft performance model
87
- aircraft_performance: AircraftPerformance = PSFlight()
87
+ aircraft_performance: AircraftPerformance = dataclasses.field(default_factory=PSFlight)
88
88
 
89
89
  #: Fuel type
90
- fuel: Fuel = JetA()
90
+ fuel: Fuel = dataclasses.field(default_factory=JetA)
91
91
 
92
92
  #: List of flight waypoints to simulate in APCEMM.
93
93
  #: By default, runs a simulation for every waypoint.
@@ -371,7 +371,7 @@ class APCEMM(models.Model):
371
371
  @overload
372
372
  def eval(self, source: None = ..., **params: Any) -> NoReturn: ...
373
373
 
374
- def eval(self, source: Flight | None = None, **params: Any) -> Flight | NoReturn:
374
+ def eval(self, source: Flight | None = None, **params: Any) -> Flight:
375
375
  """Set up and run APCEMM simulations initialized at flight waypoints.
376
376
 
377
377
  Simulates the formation and evolution of contrails from a Flight
@@ -776,8 +776,8 @@ class APCEMM(models.Model):
776
776
  # Compute azimuth
777
777
  # Use forward and backward differences for first and last waypoints
778
778
  # and centered differences elsewhere
779
- ileft = [0] + list(range(self.source.size - 1))
780
- iright = list(range(1, self.source.size)) + [self.source.size - 1]
779
+ ileft = [0, *range(self.source.size - 1)]
780
+ iright = [*range(1, self.source.size), self.source.size - 1]
781
781
  lon0 = self.source["longitude"][ileft]
782
782
  lat0 = self.source["latitude"][ileft]
783
783
  lon1 = self.source["longitude"][iright]
@@ -148,7 +148,6 @@ class Cocip(Model):
148
148
 
149
149
  This implementation is regression tested against
150
150
  results from :cite:`teohAviationContrailClimate2022`.
151
- See `tests/benchmark/north-atlantic-study/validate.py`.
152
151
 
153
152
  **Outputs**
154
153
 
@@ -336,7 +335,7 @@ class Cocip(Model):
336
335
  self,
337
336
  source: Flight | Sequence[Flight] | None = None,
338
337
  **params: Any,
339
- ) -> Flight | list[Flight] | NoReturn:
338
+ ) -> Flight | list[Flight]:
340
339
  """Run CoCiP simulation on flight.
341
340
 
342
341
  Simulates the formation and evolution of contrails from a Flight
@@ -549,6 +548,8 @@ class Cocip(Model):
549
548
  verbose_outputs = self.params["verbose_outputs"]
550
549
 
551
550
  interp_kwargs = self.interp_kwargs
551
+ if self.params["preprocess_lowmem"]:
552
+ interp_kwargs["lowmem"] = True
552
553
  interpolate_met(met, self.source, "air_temperature", **interp_kwargs)
553
554
  interpolate_met(met, self.source, "specific_humidity", **interp_kwargs)
554
555
  interpolate_met(met, self.source, "eastward_wind", "u_wind", **interp_kwargs)
@@ -750,6 +751,8 @@ class Cocip(Model):
750
751
 
751
752
  # get full met grid or flight data interpolated to the pressure level `p_dz`
752
753
  interp_kwargs = self.interp_kwargs
754
+ if self.params["preprocess_lowmem"]:
755
+ interp_kwargs["lowmem"] = True
753
756
  air_temperature_lower = interpolate_met(
754
757
  met,
755
758
  self._sac_flight,
@@ -861,6 +864,8 @@ class Cocip(Model):
861
864
 
862
865
  # get met post wake vortex along initial contrail
863
866
  interp_kwargs = self.interp_kwargs
867
+ if self.params["preprocess_lowmem"]:
868
+ interp_kwargs["lowmem"] = True
864
869
  air_temperature_1 = interpolate_met(met, contrail_1, "air_temperature", **interp_kwargs)
865
870
  interpolate_met(met, contrail_1, "specific_humidity", **interp_kwargs)
866
871
 
@@ -952,11 +957,14 @@ class Cocip(Model):
952
957
  )
953
958
  logger.debug("None are filtered out!")
954
959
 
955
- def _simulate_contrail_evolution(self) -> None:
956
- """Simulate contrail evolution."""
957
- # Calculate all properties for "downwash_contrail" which is
958
- # a contrail representation of the waypoints of the downwash flight.
959
- # The downwash_contrail has already been filtered for initial persistent waypoints.
960
+ def _process_downwash_flight(self) -> tuple[MetDataset | None, MetDataset | None]:
961
+ """Create and calculate properties of contrails created by downwash vortex.
962
+
963
+ ``_downwash_contrail`` is a contrail representation of the waypoints of
964
+ ``_downwash_flight``, which has already been filtered for initial persistent waypoints.
965
+
966
+ Returns MetDatasets for subsequent use if ``preprocess_lowmem=False``.
967
+ """
960
968
  self._downwash_contrail = self._create_downwash_contrail()
961
969
  buffers = {
962
970
  f"{coord}_buffer": self.params[f"met_{coord}_buffer"]
@@ -971,6 +979,8 @@ class Cocip(Model):
971
979
  calc_timestep_geometry(self._downwash_contrail)
972
980
 
973
981
  interp_kwargs = self.interp_kwargs
982
+ if self.params["preprocess_lowmem"]:
983
+ interp_kwargs["lowmem"] = True
974
984
  calc_timestep_meteorology(self._downwash_contrail, met, self.params, **interp_kwargs)
975
985
  calc_shortwave_radiation(rad, self._downwash_contrail, **interp_kwargs)
976
986
  calc_outgoing_longwave_radiation(rad, self._downwash_contrail, **interp_kwargs)
@@ -985,6 +995,16 @@ class Cocip(Model):
985
995
  # Intersect with rad dataset
986
996
  calc_radiative_properties(self._downwash_contrail, self.params)
987
997
 
998
+ if self.params["preprocess_lowmem"]:
999
+ return None, None
1000
+ return met, rad
1001
+
1002
+ def _simulate_contrail_evolution(self) -> None:
1003
+ """Simulate contrail evolution."""
1004
+
1005
+ met, rad = self._process_downwash_flight()
1006
+ interp_kwargs = self.interp_kwargs
1007
+
988
1008
  contrail_contrail_overlapping = self.params["contrail_contrail_overlapping"]
989
1009
  if contrail_contrail_overlapping and not isinstance(self.source, Fleet):
990
1010
  warnings.warn("Contrail-Contrail Overlapping is only valid for Fleet mode.")
@@ -1022,22 +1042,7 @@ class Cocip(Model):
1022
1042
  continue
1023
1043
 
1024
1044
  # Update met, rad slices as needed
1025
- # We need to both interpolate latest_contrail, as well as the "contrail_2"
1026
- # created by calc_timestep_contrail_evolution. This "contrail_2" object
1027
- # has constant time at "time_end", hence the buffer we apply below.
1028
- # After the downwash_contrails is all used up, these updates are intended
1029
- # to happen once each hour
1030
- buffers["time_buffer"] = (
1031
- np.timedelta64(0, "ns"),
1032
- time_end - latest_contrail["time"].max(),
1033
- )
1034
- if time_end > met.indexes["time"].to_numpy()[-1]:
1035
- logger.debug("Downselect met at time_end %s within Cocip evolution", time_end)
1036
- met = latest_contrail.downselect_met(self.met, **buffers, copy=False)
1037
- met = add_tau_cirrus(met)
1038
- if time_end > rad.indexes["time"].to_numpy()[-1]:
1039
- logger.debug("Downselect rad at time_end %s within Cocip evolution", time_end)
1040
- rad = latest_contrail.downselect_met(self.rad, **buffers, copy=False)
1045
+ met, rad = self._maybe_downselect_met_rad(met, rad, latest_contrail, time_end)
1041
1046
 
1042
1047
  # Recalculate latest_contrail with new values
1043
1048
  # NOTE: We are doing a substantial amount of redundant computation here
@@ -1075,6 +1080,75 @@ class Cocip(Model):
1075
1080
 
1076
1081
  self.contrail_list.append(final_contrail)
1077
1082
 
1083
+ def _maybe_downselect_met_rad(
1084
+ self,
1085
+ met: MetDataset | None,
1086
+ rad: MetDataset | None,
1087
+ latest_contrail: GeoVectorDataset,
1088
+ time_end: np.datetime64,
1089
+ ) -> tuple[MetDataset, MetDataset]:
1090
+ """Downselect ``self.met`` and ``self.rad`` if necessary to cover ``time_end``.
1091
+
1092
+ If current ``met`` and ``rad`` slices to not include ``time_end``, new slices are selected
1093
+ from ``self.met`` and ``self.rad``. Downselection in space will cover
1094
+ - locations of current contrails (``latest_contrail``),
1095
+ - locations of additional contrails that will be loaded from ``self._downwash_flight``
1096
+ before the new slices expire,
1097
+ plus a user-defined buffer.
1098
+ """
1099
+ if met is None or time_end > met.indexes["time"].to_numpy()[-1]:
1100
+ logger.debug("Downselect met at time_end %s within Cocip evolution", time_end)
1101
+ met = self._definitely_downselect_met_or_rad(self.met, latest_contrail, time_end)
1102
+ met = add_tau_cirrus(met)
1103
+
1104
+ if rad is None or time_end > rad.indexes["time"].to_numpy()[-1]:
1105
+ logger.debug("Downselect rad at time_end %s within Cocip evolution", time_end)
1106
+ rad = self._definitely_downselect_met_or_rad(self.rad, latest_contrail, time_end)
1107
+
1108
+ return met, rad
1109
+
1110
+ def _definitely_downselect_met_or_rad(
1111
+ self, met: MetDataset, latest_contrail: GeoVectorDataset, time_end: np.datetime64
1112
+ ) -> MetDataset:
1113
+ """Perform downselection when required by :meth:`_maybe_downselect_met_rad`.
1114
+
1115
+ Downselects ``met`` (which should be one of ``self.met`` or ``self.rad``)
1116
+ to cover ``time_end``. Downselection in space covers
1117
+ - locations of current contrails (``latest_contrail``),
1118
+ - locations of additional contrails that will be loaded from ``self._downwash_flight``
1119
+ before the new slices expire,
1120
+ plus a user-defined buffer, as described in :meth:`_maybe_downselect_met_rad`.
1121
+ """
1122
+ # compute lookahead for future contrails from downwash_flight
1123
+ met_time = met.indexes["time"].to_numpy()
1124
+ mask = met_time >= time_end
1125
+ lookahead = np.min(met_time[mask]) if np.any(mask) else time_end
1126
+
1127
+ # create vector for downselection based on current + future contrails
1128
+ future_contrails = self._downwash_flight.filter(
1129
+ (self._downwash_flight["time"] >= time_end)
1130
+ & (self._downwash_flight["time"] <= lookahead),
1131
+ copy=False,
1132
+ )
1133
+ vector = GeoVectorDataset(
1134
+ {
1135
+ key: np.concatenate((latest_contrail[key], future_contrails[key]))
1136
+ for key in ("longitude", "latitude", "level", "time")
1137
+ }
1138
+ )
1139
+
1140
+ # compute time buffer to ensure downselection extends to time_end
1141
+ buffers = {
1142
+ f"{coord}_buffer": self.params[f"met_{coord}_buffer"]
1143
+ for coord in ("longitude", "latitude", "level")
1144
+ }
1145
+ buffers["time_buffer"] = (
1146
+ np.timedelta64(0, "ns"),
1147
+ max(np.timedelta64(0, "ns"), time_end - vector["time"].max()),
1148
+ )
1149
+
1150
+ return vector.downselect_met(met, **buffers, copy=False)
1151
+
1078
1152
  def _create_downwash_contrail(self) -> GeoVectorDataset:
1079
1153
  """Get Contrail representation of downwash flight."""
1080
1154
 
@@ -1442,7 +1516,7 @@ def _process_rad(rad: MetDataset) -> MetDataset:
1442
1516
  -----
1443
1517
  - https://www.ecmwf.int/sites/default/files/elibrary/2015/18490-radiation-quantities-ecmwf-model-and-mars.pdf
1444
1518
  - https://confluence.ecmwf.int/pages/viewpage.action?pageId=155337784
1445
- """ # noqa: E501
1519
+ """
1446
1520
  # If the time coordinate has already been shifted, early return
1447
1521
  if "shift_radiation_time" in rad["time"].attrs:
1448
1522
  return rad
@@ -84,6 +84,27 @@ class CocipParams(ModelParams):
84
84
  #: Humidity scaling
85
85
  humidity_scaling: HumidityScaling | None = None
86
86
 
87
+ #: Experimental. If ``True``, attempt to reduce memory consumption during
88
+ #: aircraft performance and initial contrail formation/persistent calculations
89
+ #: by calling :meth:`MetDataArray.interpolate` with ``lowmem=True``.
90
+ #:
91
+ #: **IMPORTANT**:
92
+ #:
93
+ #: * Memory optimizations used when ``proprocess_lowmem=True`` are designed for
94
+ #: meteorology backed by dask arrays with a chunk size of 1 along
95
+ #: the time dimension. This option may degrade performance if dask if not used
96
+ #: or if chunks contain more than a single time step.
97
+ #: * The impact on runtime of setting ``preprocess_lowmem=True`` depends on how
98
+ #: meteorology data is chunked. Runtime is likely to increase if meteorology
99
+ #: data is chunked in time only, but may decrease if meteorology data is also
100
+ #: chunked in longitude, latitude, and level.
101
+ #: * Changes to data access patterns with ``preprocess_lowmem=True`` alter locations
102
+ #: where interpolation is in- vs out-of-bounds. As a consequence,
103
+ #: Cocip output with ``preprocess_lowmem=True`` is only guaranteed to match output
104
+ #: with ``preprocess_lowmem=False`` when run with ``interpolation_bounds_error=True``
105
+ #: to ensure no out-of-bounds interpolation occurs.
106
+ preprocess_lowmem: bool = False
107
+
87
108
  # --------------
88
109
  # Downselect met
89
110
  # --------------
@@ -24,7 +24,6 @@ import pathlib
24
24
  import warnings
25
25
  from collections.abc import Hashable
26
26
 
27
- import matplotlib.pyplot as plt
28
27
  import numpy as np
29
28
  import numpy.typing as npt
30
29
  import pandas as pd
@@ -639,7 +638,7 @@ def regional_statistics(da_var: xr.DataArray, *, agg: str) -> pd.Series:
639
638
  -----
640
639
  - The spatial bounding box for each region is defined in Teoh et al. (2023)
641
640
  - Teoh, R., Engberg, Z., Shapiro, M., Dray, L., and Stettler, M.: A high-resolution Global
642
- Aviation emissions Inventory based on ADS-B (GAIA) for 20192021, EGUsphere [preprint],
641
+ Aviation emissions Inventory based on ADS-B (GAIA) for 2019-2021, EGUsphere [preprint],
643
642
  https://doi.org/10.5194/egusphere-2023-724, 2023.
644
643
  """
645
644
  if (agg == "mean") and (len(da_var.time) > 1):
@@ -716,7 +715,7 @@ def _regional_data_arrays(da_global: xr.DataArray) -> dict[str, xr.DataArray]:
716
715
  -----
717
716
  - The spatial bounding box for each region is defined in Teoh et al. (2023)
718
717
  - Teoh, R., Engberg, Z., Shapiro, M., Dray, L., and Stettler, M.: A high-resolution Global
719
- Aviation emissions Inventory based on ADS-B (GAIA) for 20192021, EGUsphere [preprint],
718
+ Aviation emissions Inventory based on ADS-B (GAIA) for 2019-2021, EGUsphere [preprint],
720
719
  https://doi.org/10.5194/egusphere-2023-724, 2023.
721
720
  """
722
721
  return {
@@ -2141,7 +2140,17 @@ def compare_cocip_with_goes(
2141
2140
  name="compare_cocip_with_goes function",
2142
2141
  package_name="cartopy",
2143
2142
  module_not_found_error=e,
2144
- pycontrails_optional_package="goes",
2143
+ pycontrails_optional_package="sat",
2144
+ )
2145
+
2146
+ try:
2147
+ import matplotlib.pyplot as plt
2148
+ except ModuleNotFoundError as e:
2149
+ dependencies.raise_module_not_found_error(
2150
+ name="compare_cocip_with_goes function",
2151
+ package_name="matplotlib",
2152
+ module_not_found_error=e,
2153
+ pycontrails_optional_package="vis",
2145
2154
  )
2146
2155
 
2147
2156
  # Round `time` to nearest GOES image time slice
@@ -10,6 +10,7 @@ References
10
10
  from __future__ import annotations
11
11
 
12
12
  import dataclasses
13
+ import itertools
13
14
 
14
15
  import numpy as np
15
16
  import numpy.typing as npt
@@ -941,7 +942,7 @@ def contrail_contrail_overlap_radiative_effects(
941
942
  References
942
943
  ----------
943
944
  - Schumann et al. (2021) Air traffic and contrail changes over Europe during COVID-19:
944
- A model study, Atmos. Chem. Phys., 21, 74297450, https://doi.org/10.5194/ACP-21-7429-2021.
945
+ A model study, Atmos. Chem. Phys., 21, 7429-7450, https://doi.org/10.5194/ACP-21-7429-2021.
945
946
  - Teoh et al. (2023) Global aviation contrail climate effects from 2019 to 2021.
946
947
 
947
948
  Notes
@@ -999,8 +1000,7 @@ def contrail_contrail_overlap_radiative_effects(
999
1000
  # Account for contrail overlapping starting from bottom to top layers
1000
1001
  altitude_layers = np.arange(min_altitude_m, max_altitude_m + 1.0, dz_overlap_m)
1001
1002
 
1002
- # TODO: replace zip with itertools.pairwise after we drop python3.9 support
1003
- for alt_layer0, alt_layer1 in zip(altitude_layers, altitude_layers[1:]):
1003
+ for alt_layer0, alt_layer1 in itertools.pairwise(altitude_layers):
1004
1004
  is_in_layer = (altitude >= alt_layer0) & (altitude < alt_layer1)
1005
1005
 
1006
1006
  # Get contrail waypoints at current altitude layer
@@ -143,7 +143,7 @@ class CocipGrid(models.Model):
143
143
 
144
144
  def eval(
145
145
  self, source: GeoVectorDataset | MetDataset | None = None, **params: Any
146
- ) -> GeoVectorDataset | MetDataset | NoReturn:
146
+ ) -> GeoVectorDataset | MetDataset:
147
147
  """Run CoCiP simulation on a 4d coordinate grid or arbitrary set of 4d points.
148
148
 
149
149
  If the :attr:`params` ``verbose_outputs_evolution`` is True, the model holds
@@ -446,9 +446,9 @@ class CocipGrid(models.Model):
446
446
  if ap_model := self.params["aircraft_performance"]:
447
447
  attrs["ap_model"] = type(ap_model).__name__
448
448
 
449
- if isinstance(azimuth, (np.floating, np.integer)):
449
+ if isinstance(azimuth, np.floating | np.integer):
450
450
  attrs["azimuth"] = azimuth.item()
451
- elif isinstance(azimuth, (float, int)):
451
+ elif isinstance(azimuth, float | int):
452
452
  attrs["azimuth"] = azimuth
453
453
 
454
454
  if isinstance(self.source, MetDataset):
@@ -897,7 +897,7 @@ def _setdefault_from_params(key: str, vector: GeoVectorDataset, params: dict[str
897
897
  if scalar is None:
898
898
  return
899
899
 
900
- if not isinstance(scalar, (int, float)):
900
+ if not isinstance(scalar, int | float):
901
901
  msg = (
902
902
  f"Parameter {key} must be a scalar. For non-scalar values, directly "
903
903
  "set the data on the 'source'."
@@ -312,7 +312,7 @@ class PSFlight(AircraftPerformance):
312
312
  atyp_param.wing_surface_area,
313
313
  q_fuel,
314
314
  )
315
- elif isinstance(fuel_flow, (int, float)):
315
+ elif isinstance(fuel_flow, int | float):
316
316
  fuel_flow = np.full_like(true_airspeed, fuel_flow)
317
317
 
318
318
  # Flight phase
@@ -339,11 +339,11 @@ class PSFlight(AircraftPerformance):
339
339
 
340
340
  # XXX: Explicitly broadcast scalar inputs as needed to keep a consistent
341
341
  # output spec.
342
- if isinstance(aircraft_mass, (int, float)):
342
+ if isinstance(aircraft_mass, int | float):
343
343
  aircraft_mass = np.full_like(true_airspeed, aircraft_mass)
344
- if isinstance(engine_efficiency, (int, float)):
344
+ if isinstance(engine_efficiency, int | float):
345
345
  engine_efficiency = np.full_like(true_airspeed, engine_efficiency)
346
- if isinstance(thrust, (int, float)):
346
+ if isinstance(thrust, int | float):
347
347
  thrust = np.full_like(true_airspeed, thrust)
348
348
 
349
349
  return AircraftPerformanceData(
pycontrails/models/sac.py CHANGED
@@ -150,7 +150,7 @@ class SAC(Model):
150
150
  if scale_humidity:
151
151
  for k, v in humidity_scaling.description.items():
152
152
  self.source.attrs[f"humidity_scaling_{k}"] = v
153
- if isinstance(engine_efficiency, (int, float)):
153
+ if isinstance(engine_efficiency, int | float):
154
154
  self.source.attrs["engine_efficiency"] = engine_efficiency
155
155
 
156
156
  return self.source
@@ -236,7 +236,7 @@ def T_sat_liquid(G: ArrayLike) -> ArrayLike:
236
236
  # This comment is pasted several places in `pycontrails` -- they should all be
237
237
  # addressed at the same time.
238
238
  log_ = np.log(G - 0.053)
239
- return -46.46 - constants.absolute_zero + 9.43 * log_ + 0.72 * log_**2 # type: ignore[return-value] # noqa: E501
239
+ return -46.46 - constants.absolute_zero + 9.43 * log_ + 0.72 * log_**2 # type: ignore[return-value]
240
240
 
241
241
 
242
242
  def _e_sat_liquid_prime(T: ArrayScalarLike) -> ArrayScalarLike:
@@ -163,7 +163,7 @@ def e_sat_liquid(T: ArrayScalarLike) -> ArrayScalarLike:
163
163
  # 6.1121 * np.exp((18.678 * (T - 273.15) / 234.5) * (T - 273.15) / (257.14 + (T - 273.15)))
164
164
 
165
165
  # Magnus Tetens (Murray, 1967)
166
- # 6.1078 * np.exp(17.269388 * (T - 273.16) / (T 35.86))
166
+ # 6.1078 * np.exp(17.269388 * (T - 273.16) / (T - 35.86))
167
167
 
168
168
  # Guide to Meteorological Instruments and Methods of Observation (CIMO Guide) (WMO, 2008)
169
169
  # 6.112 * np.exp(17.62 * (T - 273.15) / (243.12 + T - 273.15))
pycontrails/utils/json.py CHANGED
@@ -48,23 +48,21 @@ class NumpyEncoder(json.JSONEncoder):
48
48
  """
49
49
  if isinstance(
50
50
  obj,
51
- (
52
- np.int_,
53
- np.intc,
54
- np.intp,
55
- np.int8,
56
- np.int16,
57
- np.int32,
58
- np.int64,
59
- np.uint8,
60
- np.uint16,
61
- np.uint32,
62
- np.uint64,
63
- ),
51
+ np.int_
52
+ | np.intc
53
+ | np.intp
54
+ | np.int8
55
+ | np.int16
56
+ | np.int32
57
+ | np.int64
58
+ | np.uint8
59
+ | np.uint16
60
+ | np.uint32
61
+ | np.uint64,
64
62
  ):
65
63
  return int(obj)
66
64
 
67
- if isinstance(obj, (np.float16, np.float32, np.float64)):
65
+ if isinstance(obj, np.float16 | np.float32 | np.float64):
68
66
  return float(obj)
69
67
 
70
68
  # TODO: this is not easily reversible - np.timedelta64(str(np.timedelta64(1, "h"))) raises
@@ -74,10 +72,10 @@ class NumpyEncoder(json.JSONEncoder):
74
72
  if isinstance(obj, (np.datetime64)):
75
73
  return str(obj)
76
74
 
77
- if isinstance(obj, (np.complex64, np.complex128)):
75
+ if isinstance(obj, np.complex64 | np.complex128):
78
76
  return {"real": obj.real, "imag": obj.imag}
79
77
 
80
- if isinstance(obj, (np.ndarray,)):
78
+ if isinstance(obj, np.ndarray):
81
79
  return obj.tolist()
82
80
 
83
81
  if isinstance(obj, (np.bool_)):
@@ -86,7 +84,7 @@ class NumpyEncoder(json.JSONEncoder):
86
84
  if isinstance(obj, (np.void)):
87
85
  return None
88
86
 
89
- if isinstance(obj, (pd.Series, pd.Index)):
87
+ if isinstance(obj, pd.Series | pd.Index):
90
88
  return obj.to_numpy().tolist()
91
89
 
92
90
  try:
@@ -146,7 +144,7 @@ def dataframe_to_geojson_points(
146
144
  )
147
145
 
148
146
  # downselect dataframe
149
- cols = ["longitude", "latitude", "altitude", "time"] + properties
147
+ cols = ["longitude", "latitude", "altitude", "time", *properties]
150
148
  df = df[cols]
151
149
 
152
150
  # filter out coords with nan values, or filter just on "filter_nan" labels
@@ -3,8 +3,9 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import functools
6
+ from collections.abc import Callable
6
7
  from datetime import datetime
7
- from typing import Any, Callable, TypeVar, Union
8
+ from typing import Any, TypeVar
8
9
 
9
10
  import numpy as np
10
11
  import numpy.typing as npt
@@ -12,11 +13,11 @@ import pandas as pd
12
13
  import xarray as xr
13
14
 
14
15
  #: Array like (np.ndarray, xr.DataArray)
15
- ArrayLike = TypeVar("ArrayLike", np.ndarray, xr.DataArray, Union[xr.DataArray, np.ndarray])
16
+ ArrayLike = TypeVar("ArrayLike", np.ndarray, xr.DataArray, xr.DataArray | np.ndarray)
16
17
 
17
18
  #: Array or Float (np.ndarray, float)
18
19
  ArrayOrFloat = TypeVar(
19
- "ArrayOrFloat", npt.NDArray[np.float64], float, Union[float, npt.NDArray[np.float64]]
20
+ "ArrayOrFloat", npt.NDArray[np.float64], float, float | npt.NDArray[np.float64]
20
21
  )
21
22
 
22
23
  #: Array like input (np.ndarray, xr.DataArray, np.float64, float)
@@ -26,8 +27,8 @@ ArrayScalarLike = TypeVar(
26
27
  xr.DataArray,
27
28
  np.float64,
28
29
  float,
29
- Union[np.ndarray, float],
30
- Union[xr.DataArray, np.ndarray],
30
+ np.ndarray | float,
31
+ xr.DataArray | np.ndarray,
31
32
  )
32
33
 
33
34
  #: Datetime like input (datetime, pd.Timestamp, np.datetime64)
@@ -71,7 +72,7 @@ def support_arraylike(
71
72
  return ret
72
73
 
73
74
  # Keep python native numeric types native
74
- if isinstance(arr, (float, int, np.float64)):
75
+ if isinstance(arr, float | int | np.float64):
75
76
  return ret.item()
76
77
 
77
78
  # Recreate pd.Series