pycontrails 0.47.3__cp39-cp39-win_amd64.whl → 0.48.1__cp39-cp39-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/__init__.py +2 -2
- pycontrails/_version.py +2 -2
- pycontrails/core/coordinates.py +17 -10
- pycontrails/core/datalib.py +155 -113
- pycontrails/core/flight.py +45 -28
- pycontrails/core/met.py +163 -39
- pycontrails/core/met_var.py +9 -9
- pycontrails/core/models.py +27 -0
- pycontrails/core/rgi_cython.cp39-win_amd64.pyd +0 -0
- pycontrails/core/vector.py +257 -33
- pycontrails/datalib/ecmwf/common.py +14 -65
- pycontrails/datalib/ecmwf/era5.py +22 -27
- pycontrails/datalib/ecmwf/hres.py +53 -88
- pycontrails/datalib/ecmwf/ifs.py +10 -2
- pycontrails/datalib/gfs/gfs.py +68 -106
- pycontrails/models/accf.py +181 -154
- pycontrails/models/cocip/cocip.py +205 -105
- pycontrails/models/cocip/cocip_params.py +0 -4
- pycontrails/models/cocip/wake_vortex.py +9 -7
- pycontrails/models/cocipgrid/cocip_grid.py +2 -6
- pycontrails/models/issr.py +29 -31
- pycontrails/models/pcr.py +5 -12
- pycontrails/models/sac.py +24 -27
- pycontrails/models/tau_cirrus.py +22 -5
- pycontrails/utils/types.py +1 -1
- {pycontrails-0.47.3.dist-info → pycontrails-0.48.1.dist-info}/METADATA +2 -2
- {pycontrails-0.47.3.dist-info → pycontrails-0.48.1.dist-info}/RECORD +31 -31
- {pycontrails-0.47.3.dist-info → pycontrails-0.48.1.dist-info}/WHEEL +1 -1
- {pycontrails-0.47.3.dist-info → pycontrails-0.48.1.dist-info}/LICENSE +0 -0
- {pycontrails-0.47.3.dist-info → pycontrails-0.48.1.dist-info}/NOTICE +0 -0
- {pycontrails-0.47.3.dist-info → pycontrails-0.48.1.dist-info}/top_level.txt +0 -0
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import logging
|
|
6
6
|
import warnings
|
|
7
|
-
from typing import Any, NoReturn, Sequence, overload
|
|
7
|
+
from typing import Any, Literal, NoReturn, Sequence, overload
|
|
8
8
|
|
|
9
9
|
import numpy as np
|
|
10
10
|
import numpy.typing as npt
|
|
@@ -93,9 +93,6 @@ class Cocip(Model):
|
|
|
93
93
|
* - Ice water content
|
|
94
94
|
- ``specific_cloud_ice_water_content``
|
|
95
95
|
- ``ice_water_mixing_ratio``
|
|
96
|
-
* - Geopotential
|
|
97
|
-
- ``geopotential``
|
|
98
|
-
- ``geopotential_height``
|
|
99
96
|
|
|
100
97
|
.. list-table:: Variable keys for single-level radiation data
|
|
101
98
|
:header-rows: 1
|
|
@@ -153,13 +150,13 @@ class Cocip(Model):
|
|
|
153
150
|
|
|
154
151
|
**Outputs**
|
|
155
152
|
|
|
156
|
-
NaN values may appear in model output. Specifically,
|
|
153
|
+
NaN values may appear in model output. Specifically, ``np.nan`` values are used to indicate:
|
|
157
154
|
|
|
158
155
|
- Flight waypoint or contrail waypoint is not contained with the :attr:`met` domain.
|
|
159
156
|
- The variable was NOT computed during the model evaluation. For example, at flight waypoints
|
|
160
|
-
not producing any persistent contrails, "radiative" variables (
|
|
161
|
-
|
|
162
|
-
of :meth:`eval` are NaN. One exception to this rule is found on
|
|
157
|
+
not producing any persistent contrails, "radiative" variables (``rsr``, ``olr``, ``rf_sw``,
|
|
158
|
+
``rf_lw``, ``rf_net``) are not computed. Consequently, the corresponding values in the output
|
|
159
|
+
of :meth:`eval` are NaN. One exception to this rule is found on ``ef`` (energy forcing)
|
|
163
160
|
`contrail_age` predictions. For these two "cumulative" variables, waypoints not producing
|
|
164
161
|
any persistent contrails are assigned 0 values.
|
|
165
162
|
|
|
@@ -212,7 +209,6 @@ class Cocip(Model):
|
|
|
212
209
|
met_var.NorthwardWind,
|
|
213
210
|
met_var.VerticalVelocity,
|
|
214
211
|
(ecmwf.SpecificCloudIceWaterContent, gfs.CloudIceWaterMixingRatio),
|
|
215
|
-
(met_var.Geopotential, met_var.GeopotentialHeight),
|
|
216
212
|
)
|
|
217
213
|
|
|
218
214
|
#: Required single-level top of atmosphere radiation variables.
|
|
@@ -223,7 +219,7 @@ class Cocip(Model):
|
|
|
223
219
|
)
|
|
224
220
|
|
|
225
221
|
#: Minimal set of met variables needed to run the model after pre-processing.
|
|
226
|
-
#: The intention here is that ``
|
|
222
|
+
#: The intention here is that ``ciwc`` is unnecessary after
|
|
227
223
|
#: ``tau_cirrus`` has already been calculated.
|
|
228
224
|
processed_met_variables = (
|
|
229
225
|
met_var.AirTemperature,
|
|
@@ -235,28 +231,33 @@ class Cocip(Model):
|
|
|
235
231
|
)
|
|
236
232
|
|
|
237
233
|
#: Additional met variables used to support outputs
|
|
238
|
-
|
|
234
|
+
#: .. versionchanged:: 0.48.0
|
|
235
|
+
#: Moved Geopotential from :attr:`required_met_variables` to :attr:`optional_met_variables`
|
|
236
|
+
optional_met_variables = (
|
|
237
|
+
(met_var.Geopotential, met_var.GeopotentialHeight),
|
|
238
|
+
(ecmwf.CloudAreaFractionInLayer, gfs.TotalCloudCoverIsobaric),
|
|
239
|
+
)
|
|
239
240
|
|
|
240
241
|
#: Met data is not optional
|
|
241
242
|
met: MetDataset
|
|
242
243
|
met_required = True
|
|
243
244
|
|
|
244
|
-
#: Radiation data formatted as a MetDataset at a single pressure level [-1]
|
|
245
|
+
#: Radiation data formatted as a :class:`MetDataset` at a single pressure level [-1]
|
|
245
246
|
rad: MetDataset
|
|
246
247
|
|
|
247
248
|
#: Last Flight modeled in :meth:`eval`
|
|
248
249
|
source: Flight | Fleet
|
|
249
250
|
|
|
250
|
-
#: List of GeoVectorDataset contrail objects - one for each timestep
|
|
251
|
+
#: List of :class:`GeoVectorDataset` contrail objects - one for each timestep
|
|
251
252
|
contrail_list: list[GeoVectorDataset]
|
|
252
253
|
|
|
253
254
|
#: Contrail evolution output from model. Set to None when no contrails are formed.
|
|
254
255
|
contrail: pd.DataFrame | None
|
|
255
256
|
|
|
256
|
-
#: xr.Dataset
|
|
257
|
+
#: :class:`xr.Dataset` representation of contrail evolution.
|
|
257
258
|
contrail_dataset: xr.Dataset | None
|
|
258
259
|
|
|
259
|
-
#: Array of np.datetime64 time steps for contrail evolution
|
|
260
|
+
#: Array of :class:`np.datetime64` time steps for contrail evolution
|
|
260
261
|
timesteps: npt.NDArray[np.datetime64]
|
|
261
262
|
|
|
262
263
|
#: Parallel copy of flight waypoints after SAC filter applied
|
|
@@ -278,12 +279,8 @@ class Cocip(Model):
|
|
|
278
279
|
# call Model init
|
|
279
280
|
super().__init__(met, params=params, **params_kwargs)
|
|
280
281
|
|
|
281
|
-
shift_radiation_time = self.params["shift_radiation_time"]
|
|
282
282
|
compute_tau_cirrus = self.params["compute_tau_cirrus_in_model_init"]
|
|
283
|
-
met, rad = process_met_datasets(met, rad, compute_tau_cirrus
|
|
284
|
-
|
|
285
|
-
self.met = met
|
|
286
|
-
self.rad = rad
|
|
283
|
+
self.met, self.rad = process_met_datasets(met, rad, compute_tau_cirrus)
|
|
287
284
|
|
|
288
285
|
# initialize outputs to None
|
|
289
286
|
self.contrail = None
|
|
@@ -406,14 +403,13 @@ class Cocip(Model):
|
|
|
406
403
|
self.contrail_list = []
|
|
407
404
|
self._simulate_contrail_evolution()
|
|
408
405
|
|
|
409
|
-
self._cleanup_indices()
|
|
410
|
-
|
|
411
406
|
if not self.contrail_list:
|
|
412
407
|
logger.debug("No contrails formed by %s", label)
|
|
413
408
|
return self._fill_empty_flight_results(return_flight_list)
|
|
414
409
|
|
|
415
410
|
logger.debug("Complete contrail simulation for %s", label)
|
|
416
411
|
|
|
412
|
+
self._cleanup_indices()
|
|
417
413
|
self._bundle_results()
|
|
418
414
|
|
|
419
415
|
if return_flight_list:
|
|
@@ -915,7 +911,7 @@ class Cocip(Model):
|
|
|
915
911
|
self._downwash_contrail = self._create_downwash_contrail()
|
|
916
912
|
buffers = {
|
|
917
913
|
f"{coord}_buffer": self.params[f"met_{coord}_buffer"]
|
|
918
|
-
for coord in
|
|
914
|
+
for coord in ("longitude", "latitude", "level")
|
|
919
915
|
}
|
|
920
916
|
logger.debug("Downselect met for start of Cocip evolution")
|
|
921
917
|
met = self._downwash_contrail.downselect_met(self.met, **buffers, copy=False)
|
|
@@ -1101,12 +1097,20 @@ class Cocip(Model):
|
|
|
1101
1097
|
|
|
1102
1098
|
@overrides
|
|
1103
1099
|
def _cleanup_indices(self) -> None:
|
|
1104
|
-
|
|
1100
|
+
"""Cleanup interpolation artifacts."""
|
|
1101
|
+
|
|
1102
|
+
if not self.params["interpolation_use_indices"]:
|
|
1103
|
+
return
|
|
1104
|
+
|
|
1105
|
+
if hasattr(self, "contrail_list"):
|
|
1105
1106
|
for contrail in self.contrail_list:
|
|
1106
1107
|
contrail._invalidate_indices()
|
|
1107
|
-
|
|
1108
|
-
|
|
1108
|
+
|
|
1109
|
+
self.source._invalidate_indices()
|
|
1110
|
+
self._sac_flight._invalidate_indices()
|
|
1111
|
+
if hasattr(self, "_downwash_flight"):
|
|
1109
1112
|
self._downwash_flight._invalidate_indices()
|
|
1113
|
+
if hasattr(self, "_downwash_contrail"):
|
|
1110
1114
|
self._downwash_contrail._invalidate_indices()
|
|
1111
1115
|
|
|
1112
1116
|
def _bundle_results(self) -> None:
|
|
@@ -1118,7 +1122,9 @@ class Cocip(Model):
|
|
|
1118
1122
|
self.contrail = pd.concat(dfs)
|
|
1119
1123
|
|
|
1120
1124
|
# add age in hours to the contrail waypoint outputs
|
|
1121
|
-
|
|
1125
|
+
age_hours = np.empty_like(self.contrail["ef"])
|
|
1126
|
+
np.divide(self.contrail["age"], np.timedelta64(1, "h"), out=age_hours)
|
|
1127
|
+
self.contrail["age_hours"] = age_hours
|
|
1122
1128
|
|
|
1123
1129
|
if self.params["verbose_outputs"]:
|
|
1124
1130
|
# Compute dt_integration -- logic is somewhat complicated, but
|
|
@@ -1143,10 +1149,9 @@ class Cocip(Model):
|
|
|
1143
1149
|
|
|
1144
1150
|
self.contrail = seq_index.set_index("index")
|
|
1145
1151
|
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
if self.params["verbose_outputs"]:
|
|
1152
|
+
# ---
|
|
1153
|
+
# Create contrail xr.Dataset (self.contrail_dataset)
|
|
1154
|
+
# ---
|
|
1150
1155
|
if isinstance(self.source, Fleet):
|
|
1151
1156
|
self.contrail_dataset = xr.Dataset.from_dataframe(
|
|
1152
1157
|
self.contrail.set_index(["flight_id", "timestep", "waypoint"])
|
|
@@ -1253,9 +1258,11 @@ class Cocip(Model):
|
|
|
1253
1258
|
Flight or list[Flight]
|
|
1254
1259
|
Flight or list of Flight objects with empty variables.
|
|
1255
1260
|
"""
|
|
1261
|
+
self._cleanup_indices()
|
|
1256
1262
|
|
|
1257
1263
|
intersection = self.source.data.pop("_met_intersection")
|
|
1258
|
-
zeros_and_nans = np.
|
|
1264
|
+
zeros_and_nans = np.zeros(intersection.shape, dtype=np.float32)
|
|
1265
|
+
zeros_and_nans[~intersection] = np.nan
|
|
1259
1266
|
self.source["ef"] = zeros_and_nans.copy()
|
|
1260
1267
|
self.source["persistent_1"] = zeros_and_nans.copy()
|
|
1261
1268
|
self.source["cocip"] = np.sign(zeros_and_nans)
|
|
@@ -1275,8 +1282,7 @@ class Cocip(Model):
|
|
|
1275
1282
|
def process_met_datasets(
|
|
1276
1283
|
met: MetDataset,
|
|
1277
1284
|
rad: MetDataset,
|
|
1278
|
-
compute_tau_cirrus: bool |
|
|
1279
|
-
shift_radiation_time: np.timedelta64 | None = None,
|
|
1285
|
+
compute_tau_cirrus: bool | Literal["auto"] = "auto",
|
|
1280
1286
|
) -> tuple[MetDataset, MetDataset]:
|
|
1281
1287
|
"""Process and verify ERA5 data for :class:`Cocip` and :class:`CocipGrid`.
|
|
1282
1288
|
|
|
@@ -1289,18 +1295,20 @@ def process_met_datasets(
|
|
|
1289
1295
|
of the ``process_met`` parameter. The same approach is also taken
|
|
1290
1296
|
in :class:`Cocip` in version 0.27.0.
|
|
1291
1297
|
|
|
1298
|
+
.. versionchanged:: 0.48.0
|
|
1299
|
+
|
|
1300
|
+
Remove the ``shift_radiation_time`` parameter. This parameter is now
|
|
1301
|
+
inferred from the metadata on the `rad` instance.
|
|
1302
|
+
|
|
1292
1303
|
Parameters
|
|
1293
1304
|
----------
|
|
1294
1305
|
met : MetDataset
|
|
1295
1306
|
Met pressure-level data
|
|
1296
1307
|
rad : MetDataset
|
|
1297
1308
|
Rad single-level data
|
|
1298
|
-
compute_tau_cirrus : bool |
|
|
1309
|
+
compute_tau_cirrus : bool | Literal["auto"]
|
|
1299
1310
|
Whether to add ``"tau_cirrus"`` variable to pressure-level met data. If set to
|
|
1300
1311
|
``"auto"``, ``"tau_cirrus"`` will be computed iff the met data is dask-backed.
|
|
1301
|
-
shift_radiation_time : np.timedelta64 | None
|
|
1302
|
-
Shift the time dimension of radiation data to account for accumulated values.
|
|
1303
|
-
If not specified, the default value from :class:`CocipGridParams` will be used.
|
|
1304
1312
|
|
|
1305
1313
|
Returns
|
|
1306
1314
|
-------
|
|
@@ -1315,35 +1323,20 @@ def process_met_datasets(
|
|
|
1315
1323
|
"Specific humidity enhancement of the raw specific humidity values in "
|
|
1316
1324
|
"the underlying met data is deprecated."
|
|
1317
1325
|
)
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1326
|
+
|
|
1327
|
+
if compute_tau_cirrus == "auto":
|
|
1328
|
+
# If met data is dask-backed, compute tau_cirrus
|
|
1329
|
+
compute_tau_cirrus = met.data["air_temperature"].chunks is not None
|
|
1330
|
+
|
|
1323
1331
|
if "tau_cirrus" not in met.data:
|
|
1324
1332
|
met.ensure_vars(Cocip.met_variables)
|
|
1325
|
-
if
|
|
1326
|
-
compute_tau_cirrus == "auto"
|
|
1327
|
-
and met.data["specific_humidity"].chunks is not None
|
|
1328
|
-
or isinstance(compute_tau_cirrus, bool)
|
|
1329
|
-
and compute_tau_cirrus
|
|
1330
|
-
):
|
|
1333
|
+
if compute_tau_cirrus:
|
|
1331
1334
|
met = add_tau_cirrus(met)
|
|
1332
1335
|
else:
|
|
1333
1336
|
met.ensure_vars(Cocip.processed_met_variables)
|
|
1334
1337
|
|
|
1335
|
-
# Deal with rad: check shift_radiation_time
|
|
1336
1338
|
rad.ensure_vars(Cocip.rad_variables)
|
|
1337
|
-
|
|
1338
|
-
existing_shift = rad["time"].attrs["shift_radiation_time"] # this is a string
|
|
1339
|
-
if pd.Timedelta(existing_shift) != shift_radiation_time: # compare timedeltas
|
|
1340
|
-
raise ValueError(
|
|
1341
|
-
"The time coordinate in MetDataset 'rad' has already been "
|
|
1342
|
-
f"scaled by a CoCiP model with 'shift_radiation_time={existing_shift}'. "
|
|
1343
|
-
)
|
|
1344
|
-
else:
|
|
1345
|
-
shift_radiation_time = shift_radiation_time or Cocip.default_params.shift_radiation_time
|
|
1346
|
-
rad = _process_rad(rad, shift_radiation_time)
|
|
1339
|
+
rad = _process_rad(rad)
|
|
1347
1340
|
|
|
1348
1341
|
return met, rad
|
|
1349
1342
|
|
|
@@ -1366,7 +1359,7 @@ def add_tau_cirrus(met: MetDataset) -> MetDataset:
|
|
|
1366
1359
|
return met
|
|
1367
1360
|
|
|
1368
1361
|
|
|
1369
|
-
def _process_rad(rad: MetDataset
|
|
1362
|
+
def _process_rad(rad: MetDataset) -> MetDataset:
|
|
1370
1363
|
"""Process radiation specific variables for model.
|
|
1371
1364
|
|
|
1372
1365
|
These variables are used to calculate the reflected solar radiation (RSR),
|
|
@@ -1379,8 +1372,6 @@ def _process_rad(rad: MetDataset, shift_radiation_time: np.timedelta64) -> MetDa
|
|
|
1379
1372
|
----------
|
|
1380
1373
|
rad : MetDataset
|
|
1381
1374
|
Rad single-level data
|
|
1382
|
-
shift_radiation_time : np.timedelta64
|
|
1383
|
-
Shift the time dimension of radiation data to account for accumulated values.
|
|
1384
1375
|
|
|
1385
1376
|
Returns
|
|
1386
1377
|
-------
|
|
@@ -1392,18 +1383,67 @@ def _process_rad(rad: MetDataset, shift_radiation_time: np.timedelta64) -> MetDa
|
|
|
1392
1383
|
- https://www.ecmwf.int/sites/default/files/elibrary/2015/18490-radiation-quantities-ecmwf-model-and-mars.pdf
|
|
1393
1384
|
- https://confluence.ecmwf.int/pages/viewpage.action?pageId=155337784
|
|
1394
1385
|
""" # noqa: E501
|
|
1395
|
-
|
|
1396
|
-
if
|
|
1386
|
+
# If the time coordinate has already been shifted, early return
|
|
1387
|
+
if "shift_radiation_time" in rad["time"].attrs:
|
|
1388
|
+
return rad
|
|
1389
|
+
|
|
1390
|
+
provider = rad.provider_attr
|
|
1391
|
+
|
|
1392
|
+
# Only shift ECMWF data -- exit for anything else
|
|
1393
|
+
# A warning is emitted upstream if the provider is not ECMWF or NCEP
|
|
1394
|
+
if provider != "ECMWF":
|
|
1395
|
+
return rad
|
|
1396
|
+
|
|
1397
|
+
dataset = rad.dataset_attr
|
|
1398
|
+
product = rad.product_attr
|
|
1399
|
+
|
|
1400
|
+
if dataset == "HRES":
|
|
1401
|
+
try:
|
|
1402
|
+
radiation_accumulated = rad.attrs["radiation_accumulated"]
|
|
1403
|
+
except KeyError:
|
|
1404
|
+
msg = (
|
|
1405
|
+
"HRES data must have a boolean 'radiation_accumulated' attribute. "
|
|
1406
|
+
"This attribute is used to determine whether the radiation data "
|
|
1407
|
+
"has been accumulated over the time period. This is the case for "
|
|
1408
|
+
"HRES data taken from a common time of forecast with multiple "
|
|
1409
|
+
"forecast steps. If this is not the case, set the "
|
|
1410
|
+
"'radiation_accumulated' attribute to False."
|
|
1411
|
+
)
|
|
1412
|
+
raise ValueError(msg)
|
|
1413
|
+
if radiation_accumulated:
|
|
1414
|
+
# Keep the original attrs -- we need these later on
|
|
1415
|
+
old_attrs = {k: v.attrs for k, v in rad.data.items()}
|
|
1416
|
+
|
|
1417
|
+
# NOTE: Taking the diff will remove the first time step
|
|
1418
|
+
# This is typically what we want (forecast step 0 is all zeros)
|
|
1419
|
+
# But, if the data has been downselected for a particular Flight / Fleet,
|
|
1420
|
+
# we lose the first time step of the data.
|
|
1421
|
+
rad.data = rad.data.diff("time", label="upper")
|
|
1422
|
+
|
|
1423
|
+
# Add back the original attrs
|
|
1424
|
+
for k, v in rad.data.items():
|
|
1425
|
+
v.attrs = old_attrs[k]
|
|
1426
|
+
|
|
1427
|
+
shift_radiation_time = -np.timedelta64(30, "m")
|
|
1428
|
+
|
|
1429
|
+
elif dataset == "ERA5" and product == "ensemble":
|
|
1430
|
+
shift_radiation_time = -np.timedelta64(90, "m")
|
|
1431
|
+
else:
|
|
1432
|
+
shift_radiation_time = -np.timedelta64(30, "m")
|
|
1433
|
+
|
|
1434
|
+
# Do a final idiot check -- most likely, the time resolution of the data will
|
|
1435
|
+
# agree with the shift_radiation_time. If not, emit a warning. There could be
|
|
1436
|
+
# a false positive here if the data has been downsampled in time.
|
|
1437
|
+
logger.debug("Shifting rad time by %s", shift_radiation_time)
|
|
1438
|
+
rad_time_diff = np.diff(rad.data["time"])
|
|
1439
|
+
if not np.all(rad_time_diff / 2 == -shift_radiation_time):
|
|
1397
1440
|
warnings.warn(
|
|
1398
1441
|
f"Shifting radiation time dimension by unexpected interval {shift_radiation_time}. "
|
|
1399
|
-
"
|
|
1400
|
-
"
|
|
1401
|
-
"expected shift is half the time difference consecutive time steps."
|
|
1442
|
+
f"The rad data has metadata indicating it is {product} ECMWF data. "
|
|
1443
|
+
f"This dataset should have time steps of {-2 * shift_radiation_time}."
|
|
1402
1444
|
)
|
|
1403
1445
|
|
|
1404
1446
|
rad.data = rad.data.assign_coords({"time": rad.data["time"] + shift_radiation_time})
|
|
1405
|
-
msg = "Time coordinates adjusted to account for accumulation averaging"
|
|
1406
|
-
rad.data["time"].attrs["_pycontrails_modified"] = msg
|
|
1407
1447
|
rad.data["time"].attrs["shift_radiation_time"] = str(shift_radiation_time)
|
|
1408
1448
|
|
|
1409
1449
|
return rad
|
|
@@ -1439,7 +1479,9 @@ def _eval_aircraft_performance(
|
|
|
1439
1479
|
if aircraft_performance is None:
|
|
1440
1480
|
raise ValueError(
|
|
1441
1481
|
"An AircraftPerformance model parameter is required if the flight "
|
|
1442
|
-
f"does not contain the following variables: {aircraft_performance_outputs}"
|
|
1482
|
+
f"does not contain the following variables: {aircraft_performance_outputs}. "
|
|
1483
|
+
"For example, instantiate the Cocip model with "
|
|
1484
|
+
"'Cocip(..., aircraft_performance=PSFlight(...))'."
|
|
1443
1485
|
)
|
|
1444
1486
|
|
|
1445
1487
|
return aircraft_performance.eval(source=flight, copy_source=False)
|
|
@@ -1538,9 +1580,9 @@ def calc_timestep_geometry(contrail: GeoVectorDataset) -> None:
|
|
|
1538
1580
|
# Finally, at the next evolution step, the previous waypoint will accrue
|
|
1539
1581
|
# a nan value after segment_length is recalculated
|
|
1540
1582
|
|
|
1541
|
-
segment_length[~continuous] = 0
|
|
1542
|
-
sin_a[~continuous] = 0
|
|
1543
|
-
cos_a[~continuous] = 0
|
|
1583
|
+
segment_length[~continuous] = 0.0
|
|
1584
|
+
sin_a[~continuous] = 0.0
|
|
1585
|
+
cos_a[~continuous] = 0.0
|
|
1544
1586
|
|
|
1545
1587
|
np.nan_to_num(segment_length, copy=False)
|
|
1546
1588
|
np.nan_to_num(sin_a, copy=False)
|
|
@@ -1680,8 +1722,8 @@ def calc_shortwave_radiation(
|
|
|
1680
1722
|
Raises
|
|
1681
1723
|
------
|
|
1682
1724
|
ValueError
|
|
1683
|
-
If ``rad`` does not contain ``"toa_upward_shortwave_flux"`` or
|
|
1684
|
-
variable.
|
|
1725
|
+
If ``rad`` does not contain ``"toa_upward_shortwave_flux"`` or
|
|
1726
|
+
``"top_net_solar_radiation"`` variable.
|
|
1685
1727
|
|
|
1686
1728
|
Notes
|
|
1687
1729
|
-----
|
|
@@ -1692,30 +1734,36 @@ def calc_shortwave_radiation(
|
|
|
1692
1734
|
--------
|
|
1693
1735
|
:func:`geo.solar_direct_radiation`
|
|
1694
1736
|
"""
|
|
1695
|
-
|
|
1696
1737
|
if "sdr" in vector and "rsr" in vector:
|
|
1697
|
-
return
|
|
1738
|
+
return
|
|
1698
1739
|
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1740
|
+
try:
|
|
1741
|
+
sdr = vector["sdr"]
|
|
1742
|
+
except KeyError:
|
|
1743
|
+
# calculate instantaneous theoretical solar direct radiation based on geo position and time
|
|
1744
|
+
longitude = vector["longitude"]
|
|
1745
|
+
latitude = vector["latitude"]
|
|
1746
|
+
time = vector["time"]
|
|
1747
|
+
sdr = geo.solar_direct_radiation(longitude, latitude, time, threshold_cos_sza=0.01)
|
|
1748
|
+
vector["sdr"] = sdr
|
|
1704
1749
|
|
|
1705
1750
|
# GFS contains RSR (toa_upward_shortwave_flux) variable directly
|
|
1706
|
-
|
|
1707
|
-
|
|
1751
|
+
gfs_key = "toa_upward_shortwave_flux"
|
|
1752
|
+
if gfs_key in rad:
|
|
1753
|
+
interpolate_met(rad, vector, gfs_key, "rsr", **interp_kwargs)
|
|
1754
|
+
return
|
|
1755
|
+
|
|
1756
|
+
ecmwf_key = "top_net_solar_radiation"
|
|
1757
|
+
if ecmwf_key not in rad:
|
|
1758
|
+
msg = f"'rad' data must contain either '{gfs_key}' or '{ecmwf_key}' (ECMWF) variable."
|
|
1759
|
+
raise ValueError(msg)
|
|
1708
1760
|
|
|
1709
1761
|
# ECMWF contains "top_net_solar_radiation" which is SDR - RSR
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1762
|
+
tnsr = interpolate_met(rad, vector, ecmwf_key, **interp_kwargs)
|
|
1763
|
+
tnsr = _rad_accumulation_to_average_instantaneous(rad, ecmwf_key, tnsr)
|
|
1764
|
+
vector.update({ecmwf_key: tnsr})
|
|
1713
1765
|
|
|
1714
|
-
|
|
1715
|
-
raise ValueError(
|
|
1716
|
-
"'rad' data must contain either 'toa_upward_shortwave_flux' or "
|
|
1717
|
-
"'top_net_solar_radiation' (ECMWF) variable."
|
|
1718
|
-
)
|
|
1766
|
+
vector["rsr"] = np.maximum(sdr - tnsr, 0.0)
|
|
1719
1767
|
|
|
1720
1768
|
|
|
1721
1769
|
def calc_outgoing_longwave_radiation(
|
|
@@ -1747,20 +1795,22 @@ def calc_outgoing_longwave_radiation(
|
|
|
1747
1795
|
return None
|
|
1748
1796
|
|
|
1749
1797
|
# GFS contains OLR (toa_upward_longwave_flux) variable directly
|
|
1750
|
-
|
|
1751
|
-
|
|
1798
|
+
gfs_key = "toa_upward_longwave_flux"
|
|
1799
|
+
if gfs_key in rad:
|
|
1800
|
+
interpolate_met(rad, vector, gfs_key, "olr", **interp_kwargs)
|
|
1752
1801
|
return
|
|
1753
1802
|
|
|
1754
1803
|
# ECMWF contains "top_net_thermal_radiation" which is -1 * OLR
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1804
|
+
ecmwf_key = "top_net_thermal_radiation"
|
|
1805
|
+
if ecmwf_key not in rad:
|
|
1806
|
+
msg = f"'rad' data must contain either '{gfs_key}' or '{ecmwf_key}' (ECMWF) variable."
|
|
1807
|
+
raise ValueError(msg)
|
|
1759
1808
|
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1809
|
+
tntr = interpolate_met(rad, vector, ecmwf_key, **interp_kwargs)
|
|
1810
|
+
tntr = _rad_accumulation_to_average_instantaneous(rad, ecmwf_key, tntr)
|
|
1811
|
+
vector.update({ecmwf_key: tntr})
|
|
1812
|
+
|
|
1813
|
+
vector["olr"] = np.maximum(-tntr, 0.0)
|
|
1764
1814
|
|
|
1765
1815
|
|
|
1766
1816
|
def calc_radiative_properties(contrail: GeoVectorDataset, params: dict[str, Any]) -> None:
|
|
@@ -1995,8 +2045,7 @@ def calc_contrail_properties(
|
|
|
1995
2045
|
contrail.update(dn_dt_agg=dn_dt_agg)
|
|
1996
2046
|
contrail.update(dn_dt_turb=dn_dt_turb)
|
|
1997
2047
|
if radiative_heating_effects:
|
|
1998
|
-
contrail.update(heat_rate=heat_rate)
|
|
1999
|
-
contrail.update(d_heat_rate=d_heat_rate)
|
|
2048
|
+
contrail.update(heat_rate=heat_rate, d_heat_rate=d_heat_rate)
|
|
2000
2049
|
|
|
2001
2050
|
|
|
2002
2051
|
def calc_timestep_contrail_evolution(
|
|
@@ -2279,6 +2328,57 @@ def calc_timestep_contrail_evolution(
|
|
|
2279
2328
|
return final_contrail
|
|
2280
2329
|
|
|
2281
2330
|
|
|
2331
|
+
def _rad_accumulation_to_average_instantaneous(
|
|
2332
|
+
rad: MetDataset,
|
|
2333
|
+
name: str,
|
|
2334
|
+
arr: npt.NDArray[np.float_],
|
|
2335
|
+
) -> npt.NDArray[np.float_]:
|
|
2336
|
+
"""Convert from radiation accumulation to average instantaneous values.
|
|
2337
|
+
|
|
2338
|
+
.. versionadded:: 0.48.0
|
|
2339
|
+
|
|
2340
|
+
Parameters
|
|
2341
|
+
----------
|
|
2342
|
+
rad : MetDataset
|
|
2343
|
+
Radiation data
|
|
2344
|
+
name : str
|
|
2345
|
+
Variable name
|
|
2346
|
+
arr : npt.NDArray[np.float_]
|
|
2347
|
+
Array of values already interpolated from ``rad``
|
|
2348
|
+
|
|
2349
|
+
Returns
|
|
2350
|
+
-------
|
|
2351
|
+
npt.NDArray[np.float_]
|
|
2352
|
+
Array of values converted from accumulation to average instantaneous values
|
|
2353
|
+
"""
|
|
2354
|
+
mda = rad[name]
|
|
2355
|
+
try:
|
|
2356
|
+
unit = mda.attrs["units"]
|
|
2357
|
+
except KeyError as e:
|
|
2358
|
+
msg = (
|
|
2359
|
+
f"Radiation data contains '{name}' variable "
|
|
2360
|
+
"but units are not specified. Provide units in the "
|
|
2361
|
+
f"rad['{name}'].attrs passed into Cocip."
|
|
2362
|
+
)
|
|
2363
|
+
raise KeyError(msg) from e
|
|
2364
|
+
|
|
2365
|
+
# The unit is already instantaneous
|
|
2366
|
+
if unit == "W m**-2":
|
|
2367
|
+
return arr
|
|
2368
|
+
|
|
2369
|
+
if unit != "J m**-2":
|
|
2370
|
+
msg = f"Unexpected units '{unit}' for '{name}' variable. Expected 'J m**-2' or 'W m**-2'."
|
|
2371
|
+
raise ValueError(msg)
|
|
2372
|
+
|
|
2373
|
+
# Convert from J m**-2 to W m**-2
|
|
2374
|
+
if rad.dataset_attr == "ERA5" and rad.product_attr == "ensemble":
|
|
2375
|
+
n_seconds = 3.0 * 3600.0 # 3 hour interval
|
|
2376
|
+
else:
|
|
2377
|
+
n_seconds = 3600.0 # 1 hour interval
|
|
2378
|
+
|
|
2379
|
+
return arr / n_seconds
|
|
2380
|
+
|
|
2381
|
+
|
|
2282
2382
|
def _emissions_variables() -> tuple[str, ...]:
|
|
2283
2383
|
"""Return variables required for emissions calculation."""
|
|
2284
2384
|
return (
|
|
@@ -73,10 +73,6 @@ class CocipParams(ModelParams):
|
|
|
73
73
|
#: Constant below applies to ECMWF data.
|
|
74
74
|
effective_vertical_resolution: float = 2000.0
|
|
75
75
|
|
|
76
|
-
#: Shift the time coordinates of radiation parameters for accumulated values
|
|
77
|
-
#: TODO: change this to np.timedelta64(0, "m") when we start using other datasets
|
|
78
|
-
shift_radiation_time: np.timedelta64 = -np.timedelta64(30, "m")
|
|
79
|
-
|
|
80
76
|
#: Smoothing parameters for true airspeed.
|
|
81
77
|
#: Only used for Flight models.
|
|
82
78
|
#: Passed directly to :func:`scipy.signal.savgol_filter`.
|
|
@@ -54,22 +54,24 @@ def max_downward_displacement(
|
|
|
54
54
|
- :cite:`holzapfelProbabilisticTwoPhaseWake2003`
|
|
55
55
|
- :cite:`schumannContrailCirrusPrediction2012`
|
|
56
56
|
"""
|
|
57
|
-
wingspan_arr = np.broadcast_to(wingspan, true_airspeed.shape)
|
|
58
|
-
aircraft_mass_arr = np.broadcast_to(aircraft_mass, true_airspeed.shape)
|
|
59
|
-
|
|
60
57
|
rho_air = thermo.rho_d(air_temperature, air_pressure)
|
|
61
58
|
n_bv = thermo.brunt_vaisala_frequency(air_pressure, air_temperature, dT_dz)
|
|
62
|
-
t_0 = effective_time_scale(wingspan, true_airspeed,
|
|
59
|
+
t_0 = effective_time_scale(wingspan, true_airspeed, aircraft_mass, rho_air)
|
|
63
60
|
|
|
64
61
|
dz_max_strong = downward_displacement_strongly_stratified(
|
|
65
|
-
wingspan, true_airspeed,
|
|
62
|
+
wingspan, true_airspeed, aircraft_mass, rho_air, n_bv
|
|
66
63
|
)
|
|
67
64
|
|
|
68
65
|
is_weakly_stratified = n_bv * t_0 < 0.8
|
|
66
|
+
if isinstance(wingspan, np.ndarray):
|
|
67
|
+
wingspan = wingspan[is_weakly_stratified]
|
|
68
|
+
if isinstance(aircraft_mass, np.ndarray):
|
|
69
|
+
aircraft_mass = aircraft_mass[is_weakly_stratified]
|
|
70
|
+
|
|
69
71
|
dz_max_weak = downward_displacement_weakly_stratified(
|
|
70
|
-
wingspan=
|
|
72
|
+
wingspan=wingspan,
|
|
71
73
|
true_airspeed=true_airspeed[is_weakly_stratified],
|
|
72
|
-
aircraft_mass=
|
|
74
|
+
aircraft_mass=aircraft_mass,
|
|
73
75
|
rho_air=rho_air[is_weakly_stratified],
|
|
74
76
|
n_bv=n_bv[is_weakly_stratified],
|
|
75
77
|
dz_max_strong=dz_max_strong[is_weakly_stratified],
|
|
@@ -106,12 +106,8 @@ class CocipGrid(models.Model, cocip_time_handling.CocipTimeHandlingMixin):
|
|
|
106
106
|
super().__init__(met, params=params, **params_kwargs)
|
|
107
107
|
self.validate_time_params()
|
|
108
108
|
|
|
109
|
-
shift_radiation_time = self.params["shift_radiation_time"]
|
|
110
109
|
compute_tau_cirrus = self.params["compute_tau_cirrus_in_model_init"]
|
|
111
|
-
met, rad = cocip.process_met_datasets(met, rad, compute_tau_cirrus
|
|
112
|
-
|
|
113
|
-
self.met = met
|
|
114
|
-
self.rad = rad
|
|
110
|
+
self.met, self.rad = cocip.process_met_datasets(met, rad, compute_tau_cirrus)
|
|
115
111
|
|
|
116
112
|
# Convenience -- only used in `run_interpolators`
|
|
117
113
|
self.params["_interp_kwargs"] = self.interp_kwargs
|
|
@@ -309,12 +305,12 @@ class CocipGrid(models.Model, cocip_time_handling.CocipTimeHandlingMixin):
|
|
|
309
305
|
else:
|
|
310
306
|
dt_integration_str = str(dt_integration.astype("timedelta64[s]"))
|
|
311
307
|
|
|
308
|
+
self.transfer_met_source_attrs()
|
|
312
309
|
attrs: dict[str, Any] = {
|
|
313
310
|
"description": self.long_name,
|
|
314
311
|
"max_age": max_age_str,
|
|
315
312
|
"dt_integration": dt_integration_str,
|
|
316
313
|
"aircraft_type": self.get_source_param("aircraft_type"),
|
|
317
|
-
"met_source": self.met.attrs.get("met_source", "unknown"),
|
|
318
314
|
"pycontrails_version": pycontrails.__version__,
|
|
319
315
|
**self.source.attrs, # type: ignore[dict-item]
|
|
320
316
|
}
|