pycontrails 0.52.2__cp312-cp312-win_amd64.whl → 0.53.0__cp312-cp312-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 +2 -2
- pycontrails/core/cache.py +1 -1
- pycontrails/core/flight.py +8 -5
- pycontrails/core/flightplan.py +1 -1
- pycontrails/core/interpolation.py +3 -1
- pycontrails/core/met.py +190 -15
- pycontrails/core/met_var.py +1 -1
- pycontrails/core/models.py +5 -5
- pycontrails/core/rgi_cython.cp312-win_amd64.pyd +0 -0
- pycontrails/core/vector.py +5 -5
- pycontrails/datalib/_leo_utils/vis.py +10 -11
- pycontrails/datalib/_met_utils/metsource.py +13 -11
- pycontrails/datalib/ecmwf/era5.py +1 -1
- pycontrails/datalib/ecmwf/era5_model_level.py +1 -1
- pycontrails/datalib/ecmwf/hres_model_level.py +3 -3
- pycontrails/datalib/ecmwf/variables.py +3 -3
- pycontrails/datalib/gfs/gfs.py +4 -3
- pycontrails/datalib/landsat.py +10 -9
- pycontrails/ext/synthetic_flight.py +1 -1
- pycontrails/models/accf.py +1 -1
- pycontrails/models/apcemm/apcemm.py +5 -5
- pycontrails/models/cocip/cocip.py +98 -24
- pycontrails/models/cocip/cocip_params.py +21 -0
- pycontrails/models/cocip/output_formats.py +13 -4
- pycontrails/models/cocip/radiative_forcing.py +3 -3
- pycontrails/models/cocipgrid/cocip_grid.py +4 -4
- pycontrails/models/ps_model/ps_model.py +4 -4
- pycontrails/models/sac.py +2 -2
- pycontrails/physics/thermo.py +1 -1
- pycontrails/utils/json.py +16 -18
- pycontrails/utils/types.py +7 -6
- {pycontrails-0.52.2.dist-info → pycontrails-0.53.0.dist-info}/METADATA +78 -78
- {pycontrails-0.52.2.dist-info → pycontrails-0.53.0.dist-info}/RECORD +37 -37
- {pycontrails-0.52.2.dist-info → pycontrails-0.53.0.dist-info}/WHEEL +1 -1
- {pycontrails-0.52.2.dist-info → pycontrails-0.53.0.dist-info}/LICENSE +0 -0
- {pycontrails-0.52.2.dist-info → pycontrails-0.53.0.dist-info}/NOTICE +0 -0
- {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
|
|
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
|
|
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"
|
|
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"
|
|
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"
|
|
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
|
|
pycontrails/datalib/gfs/gfs.py
CHANGED
|
@@ -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
|
|
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
|
-
"""
|
|
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 {
|
|
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")
|
pycontrails/datalib/landsat.py
CHANGED
|
@@ -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]
|
pycontrails/models/accf.py
CHANGED
|
@@ -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 =
|
|
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
|
|
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
|
|
780
|
-
iright =
|
|
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]
|
|
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
|
|
956
|
-
"""
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
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
|
-
|
|
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
|
-
"""
|
|
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 2019
|
|
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 2019
|
|
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="
|
|
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, 7429
|
|
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
|
-
|
|
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
|
|
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,
|
|
449
|
+
if isinstance(azimuth, np.floating | np.integer):
|
|
450
450
|
attrs["azimuth"] = azimuth.item()
|
|
451
|
-
elif isinstance(azimuth,
|
|
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,
|
|
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,
|
|
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,
|
|
342
|
+
if isinstance(aircraft_mass, int | float):
|
|
343
343
|
aircraft_mass = np.full_like(true_airspeed, aircraft_mass)
|
|
344
|
-
if isinstance(engine_efficiency,
|
|
344
|
+
if isinstance(engine_efficiency, int | float):
|
|
345
345
|
engine_efficiency = np.full_like(true_airspeed, engine_efficiency)
|
|
346
|
-
if isinstance(thrust,
|
|
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,
|
|
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]
|
|
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:
|
pycontrails/physics/thermo.py
CHANGED
|
@@ -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
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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,
|
|
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,
|
|
75
|
+
if isinstance(obj, np.complex64 | np.complex128):
|
|
78
76
|
return {"real": obj.real, "imag": obj.imag}
|
|
79
77
|
|
|
80
|
-
if isinstance(obj,
|
|
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,
|
|
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"
|
|
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
|
pycontrails/utils/types.py
CHANGED
|
@@ -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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
30
|
-
|
|
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,
|
|
75
|
+
if isinstance(arr, float | int | np.float64):
|
|
75
76
|
return ret.item()
|
|
76
77
|
|
|
77
78
|
# Recreate pd.Series
|