pycontrails 0.58.0__cp314-cp314-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.
- pycontrails/__init__.py +70 -0
- pycontrails/_version.py +34 -0
- pycontrails/core/__init__.py +30 -0
- pycontrails/core/aircraft_performance.py +679 -0
- pycontrails/core/airports.py +228 -0
- pycontrails/core/cache.py +889 -0
- pycontrails/core/coordinates.py +174 -0
- pycontrails/core/fleet.py +483 -0
- pycontrails/core/flight.py +2185 -0
- pycontrails/core/flightplan.py +228 -0
- pycontrails/core/fuel.py +140 -0
- pycontrails/core/interpolation.py +702 -0
- pycontrails/core/met.py +2931 -0
- pycontrails/core/met_var.py +387 -0
- pycontrails/core/models.py +1321 -0
- pycontrails/core/polygon.py +549 -0
- pycontrails/core/rgi_cython.cpython-314-darwin.so +0 -0
- pycontrails/core/vector.py +2249 -0
- pycontrails/datalib/__init__.py +12 -0
- pycontrails/datalib/_met_utils/metsource.py +746 -0
- pycontrails/datalib/ecmwf/__init__.py +73 -0
- pycontrails/datalib/ecmwf/arco_era5.py +345 -0
- pycontrails/datalib/ecmwf/common.py +114 -0
- pycontrails/datalib/ecmwf/era5.py +554 -0
- pycontrails/datalib/ecmwf/era5_model_level.py +490 -0
- pycontrails/datalib/ecmwf/hres.py +804 -0
- pycontrails/datalib/ecmwf/hres_model_level.py +466 -0
- pycontrails/datalib/ecmwf/ifs.py +287 -0
- pycontrails/datalib/ecmwf/model_levels.py +435 -0
- pycontrails/datalib/ecmwf/static/model_level_dataframe_v20240418.csv +139 -0
- pycontrails/datalib/ecmwf/variables.py +268 -0
- pycontrails/datalib/geo_utils.py +261 -0
- pycontrails/datalib/gfs/__init__.py +28 -0
- pycontrails/datalib/gfs/gfs.py +656 -0
- pycontrails/datalib/gfs/variables.py +104 -0
- pycontrails/datalib/goes.py +757 -0
- pycontrails/datalib/himawari/__init__.py +27 -0
- pycontrails/datalib/himawari/header_struct.py +266 -0
- pycontrails/datalib/himawari/himawari.py +667 -0
- pycontrails/datalib/landsat.py +589 -0
- pycontrails/datalib/leo_utils/__init__.py +5 -0
- pycontrails/datalib/leo_utils/correction.py +266 -0
- pycontrails/datalib/leo_utils/landsat_metadata.py +300 -0
- pycontrails/datalib/leo_utils/search.py +250 -0
- pycontrails/datalib/leo_utils/sentinel_metadata.py +748 -0
- pycontrails/datalib/leo_utils/static/bq_roi_query.sql +6 -0
- pycontrails/datalib/leo_utils/vis.py +59 -0
- pycontrails/datalib/sentinel.py +650 -0
- pycontrails/datalib/spire/__init__.py +5 -0
- pycontrails/datalib/spire/exceptions.py +62 -0
- pycontrails/datalib/spire/spire.py +604 -0
- pycontrails/ext/bada.py +42 -0
- pycontrails/ext/cirium.py +14 -0
- pycontrails/ext/empirical_grid.py +140 -0
- pycontrails/ext/synthetic_flight.py +431 -0
- pycontrails/models/__init__.py +1 -0
- pycontrails/models/accf.py +425 -0
- pycontrails/models/apcemm/__init__.py +8 -0
- pycontrails/models/apcemm/apcemm.py +983 -0
- pycontrails/models/apcemm/inputs.py +226 -0
- pycontrails/models/apcemm/static/apcemm_yaml_template.yaml +183 -0
- pycontrails/models/apcemm/utils.py +437 -0
- pycontrails/models/cocip/__init__.py +29 -0
- pycontrails/models/cocip/cocip.py +2742 -0
- pycontrails/models/cocip/cocip_params.py +305 -0
- pycontrails/models/cocip/cocip_uncertainty.py +291 -0
- pycontrails/models/cocip/contrail_properties.py +1530 -0
- pycontrails/models/cocip/output_formats.py +2270 -0
- pycontrails/models/cocip/radiative_forcing.py +1260 -0
- pycontrails/models/cocip/radiative_heating.py +520 -0
- pycontrails/models/cocip/unterstrasser_wake_vortex.py +508 -0
- pycontrails/models/cocip/wake_vortex.py +396 -0
- pycontrails/models/cocip/wind_shear.py +120 -0
- pycontrails/models/cocipgrid/__init__.py +9 -0
- pycontrails/models/cocipgrid/cocip_grid.py +2552 -0
- pycontrails/models/cocipgrid/cocip_grid_params.py +138 -0
- pycontrails/models/dry_advection.py +602 -0
- pycontrails/models/emissions/__init__.py +21 -0
- pycontrails/models/emissions/black_carbon.py +599 -0
- pycontrails/models/emissions/emissions.py +1353 -0
- pycontrails/models/emissions/ffm2.py +336 -0
- pycontrails/models/emissions/static/default-engine-uids.csv +239 -0
- pycontrails/models/emissions/static/edb-gaseous-v29b-engines.csv +596 -0
- pycontrails/models/emissions/static/edb-nvpm-v29b-engines.csv +215 -0
- pycontrails/models/extended_k15.py +1327 -0
- pycontrails/models/humidity_scaling/__init__.py +37 -0
- pycontrails/models/humidity_scaling/humidity_scaling.py +1075 -0
- pycontrails/models/humidity_scaling/quantiles/era5-model-level-quantiles.pq +0 -0
- pycontrails/models/humidity_scaling/quantiles/era5-pressure-level-quantiles.pq +0 -0
- pycontrails/models/issr.py +210 -0
- pycontrails/models/pcc.py +326 -0
- pycontrails/models/pcr.py +154 -0
- pycontrails/models/ps_model/__init__.py +18 -0
- pycontrails/models/ps_model/ps_aircraft_params.py +381 -0
- pycontrails/models/ps_model/ps_grid.py +701 -0
- pycontrails/models/ps_model/ps_model.py +1000 -0
- pycontrails/models/ps_model/ps_operational_limits.py +525 -0
- pycontrails/models/ps_model/static/ps-aircraft-params-20250328.csv +69 -0
- pycontrails/models/ps_model/static/ps-synonym-list-20250328.csv +104 -0
- pycontrails/models/sac.py +442 -0
- pycontrails/models/tau_cirrus.py +183 -0
- pycontrails/physics/__init__.py +1 -0
- pycontrails/physics/constants.py +117 -0
- pycontrails/physics/geo.py +1138 -0
- pycontrails/physics/jet.py +968 -0
- pycontrails/physics/static/iata-cargo-load-factors-20250221.csv +74 -0
- pycontrails/physics/static/iata-passenger-load-factors-20250221.csv +74 -0
- pycontrails/physics/thermo.py +551 -0
- pycontrails/physics/units.py +472 -0
- pycontrails/py.typed +0 -0
- pycontrails/utils/__init__.py +1 -0
- pycontrails/utils/dependencies.py +66 -0
- pycontrails/utils/iteration.py +13 -0
- pycontrails/utils/json.py +187 -0
- pycontrails/utils/temp.py +50 -0
- pycontrails/utils/types.py +163 -0
- pycontrails-0.58.0.dist-info/METADATA +180 -0
- pycontrails-0.58.0.dist-info/RECORD +122 -0
- pycontrails-0.58.0.dist-info/WHEEL +6 -0
- pycontrails-0.58.0.dist-info/licenses/LICENSE +178 -0
- pycontrails-0.58.0.dist-info/licenses/NOTICE +43 -0
- pycontrails-0.58.0.dist-info/top_level.txt +3 -0
|
@@ -0,0 +1,2742 @@
|
|
|
1
|
+
"""Top level CoCiP classes and methods."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import sys
|
|
7
|
+
import warnings
|
|
8
|
+
from collections.abc import Sequence
|
|
9
|
+
from typing import Any, Literal, NoReturn, overload
|
|
10
|
+
|
|
11
|
+
if sys.version_info >= (3, 12):
|
|
12
|
+
from typing import override
|
|
13
|
+
else:
|
|
14
|
+
from typing_extensions import override
|
|
15
|
+
|
|
16
|
+
import numpy as np
|
|
17
|
+
import numpy.typing as npt
|
|
18
|
+
import pandas as pd
|
|
19
|
+
import xarray as xr
|
|
20
|
+
|
|
21
|
+
from pycontrails.core import met_var, models
|
|
22
|
+
from pycontrails.core.aircraft_performance import AircraftPerformance
|
|
23
|
+
from pycontrails.core.fleet import Fleet
|
|
24
|
+
from pycontrails.core.flight import Flight
|
|
25
|
+
from pycontrails.core.met import MetDataset
|
|
26
|
+
from pycontrails.core.met_var import MetVariable
|
|
27
|
+
from pycontrails.core.models import Model, interpolate_met
|
|
28
|
+
from pycontrails.core.vector import GeoVectorDataset, VectorDataDict
|
|
29
|
+
from pycontrails.datalib import ecmwf, gfs
|
|
30
|
+
from pycontrails.models import extended_k15, sac, tau_cirrus
|
|
31
|
+
from pycontrails.models.cocip import (
|
|
32
|
+
contrail_properties,
|
|
33
|
+
radiative_forcing,
|
|
34
|
+
radiative_heating,
|
|
35
|
+
unterstrasser_wake_vortex,
|
|
36
|
+
wake_vortex,
|
|
37
|
+
wind_shear,
|
|
38
|
+
)
|
|
39
|
+
from pycontrails.models.cocip.cocip_params import CocipFlightParams
|
|
40
|
+
from pycontrails.models.emissions.emissions import Emissions
|
|
41
|
+
from pycontrails.physics import constants, geo, thermo, units
|
|
42
|
+
|
|
43
|
+
logger = logging.getLogger(__name__)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class Cocip(Model):
|
|
47
|
+
r"""Contrail Cirrus Prediction Model (CoCiP).
|
|
48
|
+
|
|
49
|
+
Published by Ulrich Schumann *et. al.*
|
|
50
|
+
(`DLR Institute of Atmospheric Physics <https://www.dlr.de/pa/en/>`_)
|
|
51
|
+
in :cite:`schumannContrailCirrusPrediction2012`, :cite:`schumannParametricRadiativeForcing2012`.
|
|
52
|
+
|
|
53
|
+
Parameters
|
|
54
|
+
----------
|
|
55
|
+
met : MetDataset
|
|
56
|
+
Pressure level dataset containing :attr:`met_variables` variables.
|
|
57
|
+
See *Notes* for variable names by data source.
|
|
58
|
+
rad : MetDataset
|
|
59
|
+
Single level dataset containing top of atmosphere radiation fluxes.
|
|
60
|
+
See *Notes* for variable names by data source.
|
|
61
|
+
params : dict[str, Any] | None, optional
|
|
62
|
+
Override Cocip model parameters with dictionary.
|
|
63
|
+
See :class:`CocipFlightParams` for model parameters.
|
|
64
|
+
**params_kwargs : Any
|
|
65
|
+
Override Cocip model parameters with keyword arguments.
|
|
66
|
+
See :class:`CocipFlightParams` for model parameters.
|
|
67
|
+
|
|
68
|
+
Notes
|
|
69
|
+
-----
|
|
70
|
+
**Inputs**
|
|
71
|
+
|
|
72
|
+
The required meteorology variables depend on the data source. :class:`Cocip`
|
|
73
|
+
supports data-source-specific variables from ECMWF models (HRES, ERA5) and the NCEP GFS, plus
|
|
74
|
+
a generic set of model-agnostic variables.
|
|
75
|
+
|
|
76
|
+
See :attr:`met_variables` and :attr:`rad_variables` for the list of required variables
|
|
77
|
+
to the ``met`` and ``rad`` parameters, respectively.
|
|
78
|
+
When an item in one of these arrays is a :class:`tuple`, variable keys depend on data source.
|
|
79
|
+
|
|
80
|
+
A warning will be raised if meteorology data is from a source not currently supported by
|
|
81
|
+
a pycontrails datalib. In this case it is the responsibility of the user to ensure that
|
|
82
|
+
meteorology data is formatted correctly. The warning can be suppressed with a context manager:
|
|
83
|
+
|
|
84
|
+
.. code-block:: python
|
|
85
|
+
:emphasize-lines: 2,3
|
|
86
|
+
|
|
87
|
+
import warnings
|
|
88
|
+
with warnings.catch_warnings():
|
|
89
|
+
warnings.simplefilter("ignore", category=UserWarning, message="Unknown provider")
|
|
90
|
+
cocip = Cocip(met, rad, ...)
|
|
91
|
+
|
|
92
|
+
The current list of required variables (labelled by ``"standard_name"``):
|
|
93
|
+
|
|
94
|
+
.. list-table:: Variable keys for pressure level data
|
|
95
|
+
:header-rows: 1
|
|
96
|
+
|
|
97
|
+
* - Parameter
|
|
98
|
+
- ECMWF
|
|
99
|
+
- GFS
|
|
100
|
+
- Generic
|
|
101
|
+
* - Air Temperature
|
|
102
|
+
- ``air_temperature``
|
|
103
|
+
- ``air_temperature``
|
|
104
|
+
- ``air_temperature``
|
|
105
|
+
* - Specific Humidity
|
|
106
|
+
- ``specific_humidity``
|
|
107
|
+
- ``specific_humidity``
|
|
108
|
+
- ``specific_humidity``
|
|
109
|
+
* - Eastward wind
|
|
110
|
+
- ``eastward_wind``
|
|
111
|
+
- ``eastward_wind``
|
|
112
|
+
- ``eastward_wind``
|
|
113
|
+
* - Northward wind
|
|
114
|
+
- ``northward_wind``
|
|
115
|
+
- ``northward_wind``
|
|
116
|
+
- ``northward_wind``
|
|
117
|
+
* - Vertical velocity
|
|
118
|
+
- ``lagrangian_tendency_of_air_pressure``
|
|
119
|
+
- ``lagrangian_tendency_of_air_pressure``
|
|
120
|
+
- ``lagrangian_tendency_of_air_pressure``
|
|
121
|
+
* - Ice water content
|
|
122
|
+
- ``specific_cloud_ice_water_content``
|
|
123
|
+
- ``ice_water_mixing_ratio``
|
|
124
|
+
- ``mass_fraction_of_cloud_ice_in_air``
|
|
125
|
+
|
|
126
|
+
.. list-table:: Variable keys for single-level radiation data
|
|
127
|
+
:header-rows: 1
|
|
128
|
+
|
|
129
|
+
* - Parameter
|
|
130
|
+
- ECMWF
|
|
131
|
+
- GFS
|
|
132
|
+
- Generic
|
|
133
|
+
* - Top solar radiation
|
|
134
|
+
- ``top_net_solar_radiation``
|
|
135
|
+
- ``toa_upward_shortwave_flux``
|
|
136
|
+
- ``toa_net_downward_shortwave_flux``
|
|
137
|
+
* - Top thermal radiation
|
|
138
|
+
- ``top_net_thermal_radiation``
|
|
139
|
+
- ``toa_upward_longwave_flux``
|
|
140
|
+
- ``toa_outgoing_longwave_flux``
|
|
141
|
+
|
|
142
|
+
**Modifications**
|
|
143
|
+
|
|
144
|
+
This implementation differs from original CoCiP (Fortran) implementation in a few places:
|
|
145
|
+
|
|
146
|
+
- This model uses aircraft performance and emissions models to calculate nvPM, fuel flow,
|
|
147
|
+
and overall propulsion efficiency, if not already provided.
|
|
148
|
+
- As described in :cite:`teohAviationContrailClimate2022`, this implementation sets
|
|
149
|
+
the initial ice particle activation rate to be a function of
|
|
150
|
+
the difference between the ambient temperature and the critical SAC threshold temperature.
|
|
151
|
+
See :func:`pycontrails.models.sac.T_critical_sac`.
|
|
152
|
+
- Isobaric heat capacity calculation.
|
|
153
|
+
The original model uses a constant value of 1004 :math:`J \ kg^{-1} \ K^{-1}`,
|
|
154
|
+
whereas this model calculates isobaric heat capacity as a function of specific humidity.
|
|
155
|
+
See :func:`pycontrails.physics.thermo.c_pm`.
|
|
156
|
+
- Solar direct radiation.
|
|
157
|
+
The original algorithm uses ECMWF radiation variable `tisr` (top incident solar radiation)
|
|
158
|
+
as solar direct radiation value.
|
|
159
|
+
This implementation calculates the theoretical solar direct radiation at
|
|
160
|
+
any arbitrary point in the atmosphere.
|
|
161
|
+
See :func:`pycontrails.physics.geo.solar_direct_radiation`.
|
|
162
|
+
- Segment angle.
|
|
163
|
+
The segment angle calculations for flights and contrail segments have been updated
|
|
164
|
+
to use more precise spherical geometry instead of a triangular approximation.
|
|
165
|
+
As the triangle approaches zero, the two calculations agree.
|
|
166
|
+
See :func:`pycontrails.physics.geo.segment_angle`.
|
|
167
|
+
- Integration.
|
|
168
|
+
This implementation consistently uses left-Riemann sums
|
|
169
|
+
in the time integration of contrail segments.
|
|
170
|
+
- Segment length ratio.
|
|
171
|
+
Instead of taking a geometric mean between contrail segments before/after advection,
|
|
172
|
+
a simple ratio is computed.
|
|
173
|
+
See :func:`contrail_properties.segment_length_ratio`.
|
|
174
|
+
- Segment energy flux.
|
|
175
|
+
This implementation does not average spatially contiguous contrail segments when calculating
|
|
176
|
+
the mean energy flux for the segment of interest.
|
|
177
|
+
See :func:`contrail_properties.mean_energy_flux_per_m`.
|
|
178
|
+
|
|
179
|
+
This implementation is regression tested against
|
|
180
|
+
results from :cite:`teohAviationContrailClimate2022`.
|
|
181
|
+
|
|
182
|
+
**Outputs**
|
|
183
|
+
|
|
184
|
+
NaN values may appear in model output. Specifically, ``np.nan`` values are used to indicate:
|
|
185
|
+
|
|
186
|
+
- Flight waypoint or contrail waypoint is not contained with the :attr:`met` domain.
|
|
187
|
+
- The variable was NOT computed during the model evaluation. For example, at flight waypoints
|
|
188
|
+
not producing any persistent contrails, "radiative" variables (``rsr``, ``olr``, ``rf_sw``,
|
|
189
|
+
``rf_lw``, ``rf_net``) are not computed. Consequently, the corresponding values in the output
|
|
190
|
+
of :meth:`eval` are NaN. One exception to this rule is found on ``ef`` (energy forcing)
|
|
191
|
+
`contrail_age` predictions. For these two "cumulative" variables, waypoints not producing
|
|
192
|
+
any persistent contrails are assigned 0 values.
|
|
193
|
+
|
|
194
|
+
References
|
|
195
|
+
----------
|
|
196
|
+
- :cite:`schumannDeterminationContrailsSatellite1990`
|
|
197
|
+
- :cite:`schumannContrailCirrusPrediction2010`
|
|
198
|
+
- :cite:`voigtInsituObservationsYoung2010`
|
|
199
|
+
- :cite:`schumannPotentialReduceClimate2011`
|
|
200
|
+
- :cite:`schumannContrailCirrusPrediction2012`
|
|
201
|
+
- :cite:`schumannParametricRadiativeForcing2012`
|
|
202
|
+
- :cite:`schumannContrailsVisibleAviation2012`
|
|
203
|
+
- :cite:`schumannEffectiveRadiusIce2011`
|
|
204
|
+
- :cite:`schumannDehydrationEffectsContrails2015`
|
|
205
|
+
- :cite:`teohMitigatingClimateForcing2020`
|
|
206
|
+
- :cite:`schumannAviationContrailCirrus2021`
|
|
207
|
+
- :cite:`schumannAirTrafficContrail2021`
|
|
208
|
+
- :cite:`teohAviationContrailClimate2022`
|
|
209
|
+
|
|
210
|
+
See Also
|
|
211
|
+
--------
|
|
212
|
+
:class:`CocipFlightParams`
|
|
213
|
+
:mod:`wake_vortex`
|
|
214
|
+
:mod:`contrail_properties`
|
|
215
|
+
:mod:`radiative_forcing`
|
|
216
|
+
:mod:`humidity_scaling`
|
|
217
|
+
:class:`Emissions`
|
|
218
|
+
:mod:`sac`
|
|
219
|
+
:mod:`tau_cirrus`
|
|
220
|
+
"""
|
|
221
|
+
|
|
222
|
+
__slots__ = (
|
|
223
|
+
"_downwash_contrail",
|
|
224
|
+
"_downwash_flight",
|
|
225
|
+
"_sac_flight",
|
|
226
|
+
"contrail",
|
|
227
|
+
"contrail_dataset",
|
|
228
|
+
"contrail_list",
|
|
229
|
+
"rad",
|
|
230
|
+
"timesteps",
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
name = "cocip"
|
|
234
|
+
long_name = "Contrail Cirrus Prediction Model"
|
|
235
|
+
default_params = CocipFlightParams
|
|
236
|
+
met_variables = (
|
|
237
|
+
met_var.AirTemperature,
|
|
238
|
+
met_var.SpecificHumidity,
|
|
239
|
+
met_var.EastwardWind,
|
|
240
|
+
met_var.NorthwardWind,
|
|
241
|
+
met_var.VerticalVelocity,
|
|
242
|
+
(
|
|
243
|
+
met_var.MassFractionOfCloudIceInAir,
|
|
244
|
+
ecmwf.SpecificCloudIceWaterContent,
|
|
245
|
+
gfs.CloudIceWaterMixingRatio,
|
|
246
|
+
),
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
#: Required single-level top of atmosphere radiation variables.
|
|
250
|
+
#: Variable keys depend on data source (e.g. ECMWF, GFS).
|
|
251
|
+
rad_variables = (
|
|
252
|
+
(
|
|
253
|
+
met_var.TOANetDownwardShortwaveFlux,
|
|
254
|
+
ecmwf.TopNetSolarRadiation,
|
|
255
|
+
gfs.TOAUpwardShortwaveRadiation,
|
|
256
|
+
),
|
|
257
|
+
(
|
|
258
|
+
met_var.TOAOutgoingLongwaveFlux,
|
|
259
|
+
ecmwf.TopNetThermalRadiation,
|
|
260
|
+
gfs.TOAUpwardLongwaveRadiation,
|
|
261
|
+
),
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
#: Minimal set of met variables needed to run the model after pre-processing.
|
|
265
|
+
#: The intention here is that ``ciwc`` is unnecessary after
|
|
266
|
+
#: ``tau_cirrus`` has already been calculated.
|
|
267
|
+
processed_met_variables = (
|
|
268
|
+
met_var.AirTemperature,
|
|
269
|
+
met_var.SpecificHumidity,
|
|
270
|
+
met_var.EastwardWind,
|
|
271
|
+
met_var.NorthwardWind,
|
|
272
|
+
met_var.VerticalVelocity,
|
|
273
|
+
tau_cirrus.TauCirrus,
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
#: Additional met variables used to support outputs
|
|
277
|
+
#:
|
|
278
|
+
#: .. versionchanged:: 0.48.0
|
|
279
|
+
#: Moved Geopotential from :attr:`met_variables` to :attr:`optional_met_variables`
|
|
280
|
+
optional_met_variables = (
|
|
281
|
+
(met_var.Geopotential, met_var.GeopotentialHeight),
|
|
282
|
+
(
|
|
283
|
+
met_var.CloudAreaFractionInAtmosphereLayer,
|
|
284
|
+
ecmwf.CloudAreaFractionInLayer,
|
|
285
|
+
gfs.TotalCloudCoverIsobaric,
|
|
286
|
+
),
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
#: Met data is not optional
|
|
290
|
+
met: MetDataset
|
|
291
|
+
met_required = True
|
|
292
|
+
|
|
293
|
+
#: Radiation data formatted as a :class:`MetDataset` at a single pressure level [-1]
|
|
294
|
+
rad: MetDataset
|
|
295
|
+
|
|
296
|
+
#: Last Flight modeled in :meth:`eval`
|
|
297
|
+
source: Flight | Fleet
|
|
298
|
+
|
|
299
|
+
#: List of :class:`GeoVectorDataset` contrail objects - one for each timestep
|
|
300
|
+
contrail_list: list[GeoVectorDataset]
|
|
301
|
+
|
|
302
|
+
#: Contrail evolution output from model.
|
|
303
|
+
#:
|
|
304
|
+
#: Set to None when no contrails are formed.
|
|
305
|
+
#: Otherwise, this is a :class:`pandas.DataFrame` describing the evolution of the contrail.
|
|
306
|
+
#: Columns include:
|
|
307
|
+
#:
|
|
308
|
+
#: - ``waypoint``: The index of the waypoint in the original flight creating
|
|
309
|
+
#: the contrail. This can be used to join the contrail DataFrame to the :attr:`source`.
|
|
310
|
+
#: - ``formation_time``: Time of contrail formation. Agrees with the ``time`` column
|
|
311
|
+
#: in :attr:`source`.
|
|
312
|
+
#: - ``continuous``: Boolean indicating whether the contrail is continuous or not.
|
|
313
|
+
#: - ``persistent``: Boolean indicating whether the contrail is persistent or not.
|
|
314
|
+
#: A contrail segment is considered continuous if both the current and the next
|
|
315
|
+
#: contrail waypoint at the same time step persist.
|
|
316
|
+
#: - ``segment_length``: Length of the contrail segment, [:math:`m`].
|
|
317
|
+
#: - ``sin_a``, ``cos_a``: Sine and cosine of the segment angle.
|
|
318
|
+
#: - ``width``, ``depth``: Contrail width and depth, [:math:`m`].
|
|
319
|
+
#: - ``sigma_yz``: The ``yz`` component of the covariance matrix, [:math:`m^{2}`].
|
|
320
|
+
#: See :func:`contrail_properties.plume_temporal_evolution`.
|
|
321
|
+
#: - ``q_sat``: Saturation specific humidity over ice, [:math:`kg \ kg^{-1}`].
|
|
322
|
+
#: - ``n_ice_per_m``: Number of ice particles per distance, [:math:`m^{-1}`].
|
|
323
|
+
#: - ``iwc``: Ice water content, [:math:`kg_{ice} kg_{air}^{-1}`].
|
|
324
|
+
#: - ``tau_contrail``: Optical depth of the contrail. See
|
|
325
|
+
#: :func:`contrail_properties.contrail_optical_depth`.
|
|
326
|
+
#: - ``rf_sw``, ``rf_lw``, ``rf_net``: Shortwave, longwave, and net instantaneous
|
|
327
|
+
#: radiative forcing, [:math:`W \ m^{-2}`] at the contrail waypoint.
|
|
328
|
+
#: - ``ef``: Energy forcing, [:math:`J`] at the contrail waypoint. See
|
|
329
|
+
#: :func:`contrail_properties.energy_forcing`.
|
|
330
|
+
contrail: pd.DataFrame | None
|
|
331
|
+
|
|
332
|
+
#: :class:`xr.Dataset` representation of contrail evolution.
|
|
333
|
+
contrail_dataset: xr.Dataset | None
|
|
334
|
+
|
|
335
|
+
#: Array of :class:`numpy.datetime64` time steps for contrail evolution
|
|
336
|
+
timesteps: npt.NDArray[np.datetime64]
|
|
337
|
+
|
|
338
|
+
#: Parallel copy of flight waypoints after SAC filter applied
|
|
339
|
+
_sac_flight: Flight
|
|
340
|
+
|
|
341
|
+
#: Parallel copy of flight waypoints after wake vortex downwash applied
|
|
342
|
+
_downwash_flight: Flight
|
|
343
|
+
|
|
344
|
+
#: GeoVectorDataset representation of :attr:`downwash_flight`
|
|
345
|
+
_downwash_contrail: GeoVectorDataset
|
|
346
|
+
|
|
347
|
+
def __init__(
|
|
348
|
+
self,
|
|
349
|
+
met: MetDataset,
|
|
350
|
+
rad: MetDataset,
|
|
351
|
+
params: dict[str, Any] | None = None,
|
|
352
|
+
**params_kwargs: Any,
|
|
353
|
+
) -> None:
|
|
354
|
+
# call Model init
|
|
355
|
+
super().__init__(met, params=params, **params_kwargs)
|
|
356
|
+
|
|
357
|
+
compute_tau_cirrus = self.params["compute_tau_cirrus_in_model_init"]
|
|
358
|
+
self.met, self.rad = process_met_datasets(met, rad, compute_tau_cirrus)
|
|
359
|
+
|
|
360
|
+
# initialize outputs to None
|
|
361
|
+
self.contrail = None
|
|
362
|
+
self.contrail_dataset = None
|
|
363
|
+
|
|
364
|
+
# ----------
|
|
365
|
+
# Public API
|
|
366
|
+
# ----------
|
|
367
|
+
|
|
368
|
+
@overload
|
|
369
|
+
def eval(self, source: Fleet, **params: Any) -> Fleet: ...
|
|
370
|
+
|
|
371
|
+
@overload
|
|
372
|
+
def eval(self, source: Flight, **params: Any) -> Flight: ...
|
|
373
|
+
|
|
374
|
+
@overload
|
|
375
|
+
def eval(self, source: Sequence[Flight], **params: Any) -> list[Flight]: ...
|
|
376
|
+
|
|
377
|
+
@overload
|
|
378
|
+
def eval(self, source: None = ..., **params: Any) -> NoReturn: ...
|
|
379
|
+
|
|
380
|
+
def eval(
|
|
381
|
+
self,
|
|
382
|
+
source: Flight | Sequence[Flight] | None = None,
|
|
383
|
+
**params: Any,
|
|
384
|
+
) -> Flight | list[Flight]:
|
|
385
|
+
"""Run CoCiP simulation on flight.
|
|
386
|
+
|
|
387
|
+
Simulates the formation and evolution of contrails from a Flight
|
|
388
|
+
using the contrail cirrus prediction model (CoCiP) from Schumann (2012)
|
|
389
|
+
:cite:`schumannContrailCirrusPrediction2012`.
|
|
390
|
+
|
|
391
|
+
.. versionchanged:: 0.25.11
|
|
392
|
+
|
|
393
|
+
Previously, any waypoint not surviving the wake vortex downwash phase of CoCiP
|
|
394
|
+
was assigned a nan-value in the ``ef`` array within the model
|
|
395
|
+
output. This is no longer the case. Instead, energy forcing is set to 0.0
|
|
396
|
+
for all waypoints which fail to produce persistent contrails. In particular,
|
|
397
|
+
nan values in the ``ef`` array are only used to indicate an
|
|
398
|
+
out-of-met-domain waypoint. The same convention is now used for output variables
|
|
399
|
+
``contrail_age`` and ``cocip`` as well.
|
|
400
|
+
|
|
401
|
+
.. versionchanged::0.26.0
|
|
402
|
+
|
|
403
|
+
:attr:`met` and :attr:`rad` down-selection is now handled automatically.
|
|
404
|
+
|
|
405
|
+
Parameters
|
|
406
|
+
----------
|
|
407
|
+
source : Flight | Sequence[Flight] | None
|
|
408
|
+
Input Flight(s) to model.
|
|
409
|
+
**params : Any
|
|
410
|
+
Overwrite model parameters before eval.
|
|
411
|
+
|
|
412
|
+
Returns
|
|
413
|
+
-------
|
|
414
|
+
Flight | list[Flight]
|
|
415
|
+
Flight(s) with updated Contrail data. The model parameter "verbose_outputs"
|
|
416
|
+
determines the variables on the return flight object.
|
|
417
|
+
|
|
418
|
+
References
|
|
419
|
+
----------
|
|
420
|
+
- :cite:`schumannContrailCirrusPrediction2012`
|
|
421
|
+
"""
|
|
422
|
+
|
|
423
|
+
self.update_params(params)
|
|
424
|
+
self.set_source(source)
|
|
425
|
+
self.source = self.require_source_type(Flight)
|
|
426
|
+
return_flight_list = isinstance(self.source, Fleet) and isinstance(source, Sequence)
|
|
427
|
+
|
|
428
|
+
self._set_timesteps()
|
|
429
|
+
|
|
430
|
+
# Downselect met for CoCiP initialization
|
|
431
|
+
# We only need to buffer in the negative vertical direction,
|
|
432
|
+
# which is the positive direction for level
|
|
433
|
+
logger.debug("Downselect met for Cocip initialization")
|
|
434
|
+
level_buffer = 0, self.params["met_level_buffer"][1]
|
|
435
|
+
met = self.source.downselect_met(self.met, level_buffer=level_buffer)
|
|
436
|
+
met = add_tau_cirrus(met)
|
|
437
|
+
|
|
438
|
+
# Prepare flight for model
|
|
439
|
+
self._process_flight(met)
|
|
440
|
+
|
|
441
|
+
# Save humidity scaling type to output attrs
|
|
442
|
+
# NOTE: Do this after _process_flight because that method automatically
|
|
443
|
+
# broadcasts all numeric source params.
|
|
444
|
+
humidity_scaling = self.params["humidity_scaling"]
|
|
445
|
+
if humidity_scaling is not None:
|
|
446
|
+
for k, v in humidity_scaling.description.items():
|
|
447
|
+
self.source.attrs[f"humidity_scaling_{k}"] = v
|
|
448
|
+
|
|
449
|
+
if isinstance(self.source, Fleet):
|
|
450
|
+
label = f"fleet of {self.source.n_flights} flights"
|
|
451
|
+
else:
|
|
452
|
+
label = f"flight {self.source['flight_id'][0]}"
|
|
453
|
+
|
|
454
|
+
logger.debug("Finding initial linear contrails with SAC")
|
|
455
|
+
self._find_initial_contrail_regions()
|
|
456
|
+
|
|
457
|
+
if not self._sac_flight:
|
|
458
|
+
logger.debug("No linear contrails formed by %s", label)
|
|
459
|
+
return self._fill_empty_flight_results(return_flight_list)
|
|
460
|
+
|
|
461
|
+
self._simulate_wake_vortex_downwash(met)
|
|
462
|
+
|
|
463
|
+
self._find_initial_persistent_contrails(met)
|
|
464
|
+
|
|
465
|
+
if not self._downwash_flight:
|
|
466
|
+
logger.debug("No persistent contrails formed by flight %s", label)
|
|
467
|
+
return self._fill_empty_flight_results(return_flight_list)
|
|
468
|
+
|
|
469
|
+
self.contrail_list = []
|
|
470
|
+
self._simulate_contrail_evolution()
|
|
471
|
+
|
|
472
|
+
if not self.contrail_list:
|
|
473
|
+
logger.debug("No contrails formed by %s", label)
|
|
474
|
+
return self._fill_empty_flight_results(return_flight_list)
|
|
475
|
+
|
|
476
|
+
logger.debug("Complete contrail simulation for %s", label)
|
|
477
|
+
|
|
478
|
+
self._cleanup_indices()
|
|
479
|
+
self._bundle_results()
|
|
480
|
+
|
|
481
|
+
if return_flight_list:
|
|
482
|
+
return self.source.to_flight_list() # type: ignore[attr-defined]
|
|
483
|
+
|
|
484
|
+
return self.source
|
|
485
|
+
|
|
486
|
+
@classmethod
|
|
487
|
+
def generic_rad_variables(cls) -> tuple[MetVariable, ...]:
|
|
488
|
+
"""Return a model-agnostic list of required radiation variables.
|
|
489
|
+
|
|
490
|
+
Returns
|
|
491
|
+
-------
|
|
492
|
+
tuple[MetVariable, ...]
|
|
493
|
+
List of model-agnostic variants of required variables
|
|
494
|
+
"""
|
|
495
|
+
available = set(met_var.MET_VARIABLES)
|
|
496
|
+
return tuple(models._find_match(required, available) for required in cls.rad_variables)
|
|
497
|
+
|
|
498
|
+
@classmethod
|
|
499
|
+
def ecmwf_rad_variables(cls) -> tuple[MetVariable, ...]:
|
|
500
|
+
"""Return an ECMWF-specific list of required radiation variables.
|
|
501
|
+
|
|
502
|
+
Returns
|
|
503
|
+
-------
|
|
504
|
+
tuple[MetVariable, ...]
|
|
505
|
+
List of ECMWF-specific variants of required variables
|
|
506
|
+
"""
|
|
507
|
+
available = set(ecmwf.ECMWF_VARIABLES)
|
|
508
|
+
return tuple(models._find_match(required, available) for required in cls.rad_variables)
|
|
509
|
+
|
|
510
|
+
@classmethod
|
|
511
|
+
def gfs_rad_variables(cls) -> tuple[MetVariable, ...]:
|
|
512
|
+
"""Return a GFS-specific list of required radiation variables.
|
|
513
|
+
|
|
514
|
+
Returns
|
|
515
|
+
-------
|
|
516
|
+
tuple[MetVariable, ...]
|
|
517
|
+
List of GFS-specific variants of required variables
|
|
518
|
+
"""
|
|
519
|
+
available = set(gfs.GFS_VARIABLES)
|
|
520
|
+
return tuple(models._find_match(required, available) for required in cls.rad_variables)
|
|
521
|
+
|
|
522
|
+
def _set_timesteps(self) -> None:
|
|
523
|
+
"""Set the :attr:`timesteps` based on the ``source`` time range.
|
|
524
|
+
|
|
525
|
+
This method is called in :meth:`eval` before the flight is processed.
|
|
526
|
+
"""
|
|
527
|
+
if isinstance(self.source, Fleet):
|
|
528
|
+
# time not sorted in Fleet instance
|
|
529
|
+
tmin = self.source["time"].min()
|
|
530
|
+
tmax = self.source["time"].max()
|
|
531
|
+
else:
|
|
532
|
+
tmin = self.source["time"][0]
|
|
533
|
+
tmax = self.source["time"][-1]
|
|
534
|
+
|
|
535
|
+
tmin = pd.to_datetime(tmin)
|
|
536
|
+
tmax = pd.to_datetime(tmax)
|
|
537
|
+
dt = pd.to_timedelta(self.params["dt_integration"])
|
|
538
|
+
|
|
539
|
+
t_start = tmin.ceil(dt)
|
|
540
|
+
t_end = tmax.floor(dt) + self.params["max_age"] + dt
|
|
541
|
+
self.timesteps = np.arange(t_start, t_end, dt)
|
|
542
|
+
|
|
543
|
+
# -------------
|
|
544
|
+
# Model Methods
|
|
545
|
+
# -------------
|
|
546
|
+
|
|
547
|
+
def _process_flight(self, met: MetDataset) -> None:
|
|
548
|
+
"""Prepare :attr:`self.source` for use in model eval.
|
|
549
|
+
|
|
550
|
+
Missing flight values should be prefilled before calling this method.
|
|
551
|
+
This method modifies :attr:`self.source`.
|
|
552
|
+
|
|
553
|
+
.. versionchanged:: 0.35.2
|
|
554
|
+
|
|
555
|
+
No longer broadcast all numeric source params. Instead, numeric
|
|
556
|
+
source params can be accessed with :meth:`Flight.get_data_or_attr`.
|
|
557
|
+
|
|
558
|
+
Parameters
|
|
559
|
+
----------
|
|
560
|
+
met : MetDataset
|
|
561
|
+
Meteorology data
|
|
562
|
+
|
|
563
|
+
Raises
|
|
564
|
+
------
|
|
565
|
+
ValueError
|
|
566
|
+
If non-sequential waypoints are found in ``self.source["waypoint"]``
|
|
567
|
+
If there is no intersection between met domain and :attr:`source`.
|
|
568
|
+
|
|
569
|
+
See Also
|
|
570
|
+
--------
|
|
571
|
+
:method:`Flight.resample_and_fill`
|
|
572
|
+
"""
|
|
573
|
+
logger.debug("Pre-processing flight parameters")
|
|
574
|
+
|
|
575
|
+
# STEP 1: Check for core columns
|
|
576
|
+
# Attach level and altitude to avoid some redundancy
|
|
577
|
+
self.source.setdefault("altitude", self.source.altitude)
|
|
578
|
+
self.source.setdefault("level", self.source.level)
|
|
579
|
+
self.source.setdefault("air_pressure", self.source.air_pressure)
|
|
580
|
+
|
|
581
|
+
core_columns = ("longitude", "latitude", "altitude", "time")
|
|
582
|
+
for col in core_columns:
|
|
583
|
+
if np.isnan(self.source[col]).any():
|
|
584
|
+
raise ValueError(
|
|
585
|
+
f"Parameter `flight` must not contain NaN values in {col} field."
|
|
586
|
+
"Call method `resample_and_fill` to clean up flight trajectory."
|
|
587
|
+
)
|
|
588
|
+
|
|
589
|
+
# STEP 2: Check for waypoints
|
|
590
|
+
# Note: Fleet instances always have a waypoint column
|
|
591
|
+
if "waypoint" not in self.source:
|
|
592
|
+
# Give flight a range index
|
|
593
|
+
self.source["waypoint"] = np.arange(self.source.size)
|
|
594
|
+
elif not isinstance(self.source, Fleet) and not np.all(
|
|
595
|
+
np.diff(self.source["waypoint"]) == 1
|
|
596
|
+
):
|
|
597
|
+
# If self.source is a usual Flight (not the subclass Fleet)
|
|
598
|
+
# and has "waypoint" data, we want to give a warning if the waypoint
|
|
599
|
+
# data has an usual index. CoCiP uses the waypoint for its continuity
|
|
600
|
+
# convention, so the waypoint data is critical.
|
|
601
|
+
msg = (
|
|
602
|
+
"Found non-sequential waypoints in flight key 'waypoint'. "
|
|
603
|
+
"The CoCiP algorithm requires flight data key 'waypoint' "
|
|
604
|
+
"to contain sequential waypoints if defined."
|
|
605
|
+
)
|
|
606
|
+
raise ValueError(msg)
|
|
607
|
+
|
|
608
|
+
# STEP 3: Test met domain for some overlap
|
|
609
|
+
# We attach the intersection to the source.
|
|
610
|
+
# This is used in the function _fill_empty_flight_results
|
|
611
|
+
intersection = self.source.coords_intersect_met(met)
|
|
612
|
+
self.source["_met_intersection"] = intersection
|
|
613
|
+
logger.debug(
|
|
614
|
+
"Fraction of flight waypoints intersecting met domain: %s / %s",
|
|
615
|
+
intersection.sum(),
|
|
616
|
+
self.source.size,
|
|
617
|
+
)
|
|
618
|
+
if not intersection.any():
|
|
619
|
+
msg = (
|
|
620
|
+
"No intersection between flight waypoints and met domain. "
|
|
621
|
+
"Rerun Cocip with met overlapping flight."
|
|
622
|
+
)
|
|
623
|
+
raise ValueError(msg)
|
|
624
|
+
|
|
625
|
+
# STEP 4: Begin met interpolation
|
|
626
|
+
# Unfortunately we use both "u_wind" and "eastward_wind" to refer to the
|
|
627
|
+
# same variable, so the logic gets a bit more complex.
|
|
628
|
+
humidity_scaling = self.params["humidity_scaling"]
|
|
629
|
+
scale_humidity = humidity_scaling is not None and "specific_humidity" not in self.source
|
|
630
|
+
verbose_outputs = self.params["verbose_outputs"]
|
|
631
|
+
|
|
632
|
+
interp_kwargs = self.interp_kwargs
|
|
633
|
+
if self.params["preprocess_lowmem"]:
|
|
634
|
+
interp_kwargs["lowmem"] = True
|
|
635
|
+
interpolate_met(met, self.source, "air_temperature", **interp_kwargs)
|
|
636
|
+
interpolate_met(met, self.source, "specific_humidity", **interp_kwargs)
|
|
637
|
+
interpolate_met(met, self.source, "eastward_wind", "u_wind", **interp_kwargs)
|
|
638
|
+
interpolate_met(met, self.source, "northward_wind", "v_wind", **interp_kwargs)
|
|
639
|
+
|
|
640
|
+
if scale_humidity:
|
|
641
|
+
humidity_scaling.eval(self.source, copy_source=False)
|
|
642
|
+
|
|
643
|
+
# if humidity_scaling isn't defined, add rhi to source for verbose_outputs
|
|
644
|
+
elif verbose_outputs:
|
|
645
|
+
self.source["rhi"] = thermo.rhi(
|
|
646
|
+
self.source["specific_humidity"],
|
|
647
|
+
self.source["air_temperature"],
|
|
648
|
+
self.source.air_pressure,
|
|
649
|
+
)
|
|
650
|
+
|
|
651
|
+
# Cache extra met properties for post-analysis
|
|
652
|
+
if verbose_outputs:
|
|
653
|
+
interpolate_met(met, self.source, "tau_cirrus", **interp_kwargs)
|
|
654
|
+
|
|
655
|
+
# handle ECMWF/GFS/generic ciwc variables
|
|
656
|
+
if (key := "specific_cloud_ice_water_content") in met: # noqa: SIM114
|
|
657
|
+
interpolate_met(met, self.source, key, **interp_kwargs)
|
|
658
|
+
elif (key := "ice_water_mixing_ratio") in met: # noqa: SIM114
|
|
659
|
+
interpolate_met(met, self.source, key, **interp_kwargs)
|
|
660
|
+
elif (key := "mass_fraction_of_cloud_ice_in_air") in met:
|
|
661
|
+
interpolate_met(met, self.source, key, **interp_kwargs)
|
|
662
|
+
|
|
663
|
+
self.source["rho_air"] = thermo.rho_d(
|
|
664
|
+
self.source["air_temperature"], self.source.air_pressure
|
|
665
|
+
)
|
|
666
|
+
self.source["sdr"] = geo.solar_direct_radiation(
|
|
667
|
+
self.source["longitude"], self.source["latitude"], self.source["time"]
|
|
668
|
+
)
|
|
669
|
+
|
|
670
|
+
# STEP 5: Calculate segment-specific properties if they are not already attached
|
|
671
|
+
if "true_airspeed" not in self.source:
|
|
672
|
+
self.source["true_airspeed"] = self.source.segment_true_airspeed(
|
|
673
|
+
u_wind=self.source["u_wind"],
|
|
674
|
+
v_wind=self.source["v_wind"],
|
|
675
|
+
smooth=self.params["smooth_true_airspeed"],
|
|
676
|
+
window_length=self.params["smooth_true_airspeed_window_length"],
|
|
677
|
+
)
|
|
678
|
+
if "segment_length" not in self.source:
|
|
679
|
+
self.source["segment_length"] = self.source.segment_length()
|
|
680
|
+
|
|
681
|
+
max_ = self.source.max_distance_gap
|
|
682
|
+
lim_ = self.params["max_seg_length_m"]
|
|
683
|
+
if max_ > 0.9 * lim_:
|
|
684
|
+
warnings.warn(
|
|
685
|
+
"Flight trajectory has segment lengths close to or exceeding the "
|
|
686
|
+
"'max_seg_length_m' parameter. Evolved contrail segments may reach "
|
|
687
|
+
"their end of life artificially early. Either resample the flight "
|
|
688
|
+
"with the 'resample_and_fill' method (recommended), or use a larger "
|
|
689
|
+
"'max_seg_length_m' parameter. Current values: "
|
|
690
|
+
f"max_seg_length_m={lim_}, max_seg_length_on_trajectory={max_}"
|
|
691
|
+
)
|
|
692
|
+
|
|
693
|
+
# if either angle is not provided, re-calculate both
|
|
694
|
+
# this is the one case where we will overwrite a flight data key
|
|
695
|
+
if "sin_a" not in self.source or "cos_a" not in self.source:
|
|
696
|
+
self.source["sin_a"], self.source["cos_a"] = self.source.segment_angle()
|
|
697
|
+
|
|
698
|
+
# STEP 6: Calculate emissions if requested, or if keys don't exist in Flight
|
|
699
|
+
if self.params["process_emissions"]:
|
|
700
|
+
self._process_emissions()
|
|
701
|
+
|
|
702
|
+
# STEP 7: Ensure that flight has the required variables defined as attrs or columns
|
|
703
|
+
self.source.ensure_vars(_emissions_variables())
|
|
704
|
+
|
|
705
|
+
def _process_emissions(self) -> None:
|
|
706
|
+
"""Process flight emissions.
|
|
707
|
+
|
|
708
|
+
See :class:`Emissions`.
|
|
709
|
+
|
|
710
|
+
We should consider supporting OpenAP (https://github.com/TUDelft-CNS-ATM/openap)
|
|
711
|
+
and alternate performance models in the future.
|
|
712
|
+
"""
|
|
713
|
+
logger.debug("Processing flight emissions")
|
|
714
|
+
|
|
715
|
+
# Call aircraft performance and Emissions models as needed
|
|
716
|
+
# NOTE: None of these sub-models actually do any met interpolation -- self.source already
|
|
717
|
+
# has all of the required met variables attached. Therefore, we don't need to worry about
|
|
718
|
+
# being consistent with passing in Cocip's interp_kwargs and humidity_scaling into
|
|
719
|
+
# the sub-models.
|
|
720
|
+
emissions = Emissions()
|
|
721
|
+
ap_model = self.params["aircraft_performance"]
|
|
722
|
+
|
|
723
|
+
# Run against a list of flights (Fleet)
|
|
724
|
+
if isinstance(self.source, Fleet):
|
|
725
|
+
# Rip the Fleet apart, run BADA on each, then reassemble
|
|
726
|
+
logger.debug("Separately running aircraft performance on each flight in fleet")
|
|
727
|
+
fls = self.source.to_flight_list(copy=False)
|
|
728
|
+
fls = [_eval_aircraft_performance(ap_model, fl) for fl in fls]
|
|
729
|
+
|
|
730
|
+
# In Fleet-mode, always call emissions
|
|
731
|
+
logger.debug("Separately running emissions on each flight in fleet")
|
|
732
|
+
fls = [_eval_emissions(emissions, fl) for fl in fls]
|
|
733
|
+
|
|
734
|
+
# Broadcast numeric AP and emissions variables back to Fleet.data
|
|
735
|
+
for fl in fls:
|
|
736
|
+
fl.broadcast_attrs(_emissions_variables(), raise_error=False)
|
|
737
|
+
|
|
738
|
+
# Convert back to fleet
|
|
739
|
+
attrs = self.source.attrs
|
|
740
|
+
attrs.pop("fl_attrs", None)
|
|
741
|
+
attrs.pop("data_keys", None)
|
|
742
|
+
self.source = Fleet.from_seq(fls, broadcast_numeric=False, attrs=attrs)
|
|
743
|
+
|
|
744
|
+
# Single flight
|
|
745
|
+
else:
|
|
746
|
+
self.source = _eval_aircraft_performance(ap_model, self.source)
|
|
747
|
+
self.source = _eval_emissions(emissions, self.source)
|
|
748
|
+
|
|
749
|
+
# Scale nvPM with parameter (fleet / flight)
|
|
750
|
+
factor = self.params["nvpm_ei_n_enhancement_factor"]
|
|
751
|
+
try:
|
|
752
|
+
self.source["nvpm_ei_n"] *= factor
|
|
753
|
+
except KeyError:
|
|
754
|
+
self.source.attrs.update({"nvpm_ei_n": self.source.attrs["nvpm_ei_n"] * factor})
|
|
755
|
+
|
|
756
|
+
def _find_initial_contrail_regions(self) -> None:
|
|
757
|
+
"""Apply Schmidt-Appleman criteria to determine regions of persistent contrail formation.
|
|
758
|
+
|
|
759
|
+
This method:
|
|
760
|
+
- Modifies :attr:`flight` in place by assigning additional columns arising from SAC
|
|
761
|
+
- Creates :attr:`_sac_flight` needed for :meth:`_simulate_wave_vortex_downwash`.
|
|
762
|
+
"""
|
|
763
|
+
# calculate the SAC for each waypoint
|
|
764
|
+
sac_model = sac.SAC(
|
|
765
|
+
met=None, # self.source is already interpolated against met, so we don't need it
|
|
766
|
+
copy_source=False,
|
|
767
|
+
)
|
|
768
|
+
self.source = sac_model.eval(source=self.source)
|
|
769
|
+
|
|
770
|
+
# Estimate SAC threshold temperature along initial contrail
|
|
771
|
+
# This variable is not involved in calculating the SAC,
|
|
772
|
+
# but it is required by `contrail_properties.ice_particle_number`.
|
|
773
|
+
# The three variables used below are all computed in sac_model.eval
|
|
774
|
+
self.source["T_critical_sac"] = sac.T_critical_sac(
|
|
775
|
+
self.source["T_sat_liquid"],
|
|
776
|
+
self.source["rh"],
|
|
777
|
+
self.source["G"],
|
|
778
|
+
)
|
|
779
|
+
|
|
780
|
+
# create a new Flight only at points where "sac" == 1
|
|
781
|
+
filt = self.source["sac"] == 1.0
|
|
782
|
+
if self.params["filter_sac"]:
|
|
783
|
+
self._sac_flight = self.source.filter(filt)
|
|
784
|
+
logger.debug(
|
|
785
|
+
"Fraction of waypoints satisfying the SAC: %s / %s",
|
|
786
|
+
len(self._sac_flight),
|
|
787
|
+
len(self.source),
|
|
788
|
+
)
|
|
789
|
+
return
|
|
790
|
+
|
|
791
|
+
warnings.warn("Manually overriding SAC filter")
|
|
792
|
+
logger.info("Manually overriding SAC filter")
|
|
793
|
+
self._sac_flight = self.source.copy()
|
|
794
|
+
logger.debug(
|
|
795
|
+
"Fraction of waypoints satisfying the SAC: %s / %s",
|
|
796
|
+
np.sum(filt),
|
|
797
|
+
self._sac_flight.size,
|
|
798
|
+
)
|
|
799
|
+
logger.debug("None are filtered out!")
|
|
800
|
+
|
|
801
|
+
def _simulate_wake_vortex_downwash(self, met: MetDataset) -> None:
|
|
802
|
+
"""Apply wake vortex model to calculate initial contrail geometry.
|
|
803
|
+
|
|
804
|
+
The calculation uses a parametric wake vortex transport and decay model to simulate the
|
|
805
|
+
wake vortex phase and obtain the initial contrail width, depth and downward displacement.
|
|
806
|
+
|
|
807
|
+
This method assigns additional columns "width" and "depth" to :attr:`_sac_flight`
|
|
808
|
+
|
|
809
|
+
Parameters
|
|
810
|
+
----------
|
|
811
|
+
met : MetDataset
|
|
812
|
+
Meteorology data
|
|
813
|
+
|
|
814
|
+
References
|
|
815
|
+
----------
|
|
816
|
+
- :cite:`holzapfelProbabilisticTwoPhaseWake2003`
|
|
817
|
+
"""
|
|
818
|
+
air_temperature = self._sac_flight["air_temperature"]
|
|
819
|
+
u_wind = self._sac_flight["u_wind"]
|
|
820
|
+
v_wind = self._sac_flight["v_wind"]
|
|
821
|
+
air_pressure = self._sac_flight.air_pressure
|
|
822
|
+
|
|
823
|
+
# flight parameters
|
|
824
|
+
aircraft_mass = self._sac_flight.get_data_or_attr("aircraft_mass")
|
|
825
|
+
true_airspeed = self._sac_flight["true_airspeed"]
|
|
826
|
+
|
|
827
|
+
# In Fleet-mode, wingspan resides on `data`, and in Flight-mode,
|
|
828
|
+
# wingspan resides on `attrs`.
|
|
829
|
+
wingspan = self._sac_flight.get_data_or_attr("wingspan")
|
|
830
|
+
|
|
831
|
+
# get the pressure level `dz_m` lower than element pressure
|
|
832
|
+
dz_m = self.params["dz_m"]
|
|
833
|
+
air_pressure_lower = thermo.pressure_dz(air_temperature, air_pressure, dz_m)
|
|
834
|
+
level_lower = air_pressure_lower / 100.0
|
|
835
|
+
|
|
836
|
+
# get full met grid or flight data interpolated to the pressure level `p_dz`
|
|
837
|
+
interp_kwargs = self.interp_kwargs
|
|
838
|
+
if self.params["preprocess_lowmem"]:
|
|
839
|
+
interp_kwargs["lowmem"] = True
|
|
840
|
+
air_temperature_lower = interpolate_met(
|
|
841
|
+
met,
|
|
842
|
+
self._sac_flight,
|
|
843
|
+
"air_temperature",
|
|
844
|
+
"air_temperature_lower",
|
|
845
|
+
level=level_lower,
|
|
846
|
+
**interp_kwargs,
|
|
847
|
+
)
|
|
848
|
+
u_wind_lower = interpolate_met(
|
|
849
|
+
met,
|
|
850
|
+
self._sac_flight,
|
|
851
|
+
"eastward_wind",
|
|
852
|
+
"u_wind_lower",
|
|
853
|
+
level=level_lower,
|
|
854
|
+
**interp_kwargs,
|
|
855
|
+
)
|
|
856
|
+
v_wind_lower = interpolate_met(
|
|
857
|
+
met,
|
|
858
|
+
self._sac_flight,
|
|
859
|
+
"northward_wind",
|
|
860
|
+
"v_wind_lower",
|
|
861
|
+
level=level_lower,
|
|
862
|
+
**interp_kwargs,
|
|
863
|
+
)
|
|
864
|
+
|
|
865
|
+
# Temperature gradient
|
|
866
|
+
dT_dz = self._sac_flight["dT_dz"] = thermo.T_potential_gradient(
|
|
867
|
+
air_temperature,
|
|
868
|
+
air_pressure,
|
|
869
|
+
air_temperature_lower,
|
|
870
|
+
air_pressure_lower,
|
|
871
|
+
dz_m,
|
|
872
|
+
)
|
|
873
|
+
|
|
874
|
+
# wind shear
|
|
875
|
+
ds_dz = self._sac_flight["ds_dz"] = wind_shear.wind_shear(
|
|
876
|
+
u_wind, u_wind_lower, v_wind, v_wind_lower, dz_m
|
|
877
|
+
)
|
|
878
|
+
|
|
879
|
+
# Initial contrail width, depth and downward displacement
|
|
880
|
+
dz_max = self._sac_flight["dz_max"] = wake_vortex.max_downward_displacement(
|
|
881
|
+
wingspan,
|
|
882
|
+
true_airspeed,
|
|
883
|
+
aircraft_mass,
|
|
884
|
+
air_temperature,
|
|
885
|
+
dT_dz,
|
|
886
|
+
ds_dz,
|
|
887
|
+
air_pressure,
|
|
888
|
+
effective_vertical_resolution=self.params["effective_vertical_resolution"],
|
|
889
|
+
wind_shear_enhancement_exponent=self.params["wind_shear_enhancement_exponent"],
|
|
890
|
+
)
|
|
891
|
+
|
|
892
|
+
# derive downwash values and save to data model
|
|
893
|
+
self._sac_flight["width"] = wake_vortex.initial_contrail_width(wingspan, dz_max)
|
|
894
|
+
initial_wake_vortex_depth = self.params["initial_wake_vortex_depth"]
|
|
895
|
+
self._sac_flight["depth"] = wake_vortex.initial_contrail_depth(
|
|
896
|
+
dz_max, initial_wake_vortex_depth
|
|
897
|
+
)
|
|
898
|
+
# Initially, sigma_yz is set to 0
|
|
899
|
+
# See bottom left paragraph p. 552 Schumann 2012 beginning with:
|
|
900
|
+
# >>> "The contrail model starts from initial values ..."
|
|
901
|
+
self._sac_flight["sigma_yz"] = np.zeros_like(dz_max)
|
|
902
|
+
|
|
903
|
+
def _find_initial_persistent_contrails(self, met: MetDataset) -> None:
|
|
904
|
+
"""Calculate the initial contrail properties after the wake vortex phase.
|
|
905
|
+
|
|
906
|
+
Determine points with initial persistent contrails.
|
|
907
|
+
|
|
908
|
+
This method first calculates ice water content (``iwc``) and number of ice particles per
|
|
909
|
+
distance (``n_ice_per_m_1``).
|
|
910
|
+
|
|
911
|
+
It then tests each contrail waypoint for initial persistence and calculates
|
|
912
|
+
parameters for the first iteration of the time integration.
|
|
913
|
+
|
|
914
|
+
Note that the subscript "_1" represents the conditions after the wake vortex phase.
|
|
915
|
+
|
|
916
|
+
Parameters
|
|
917
|
+
----------
|
|
918
|
+
met : MetDataset
|
|
919
|
+
Meteorology data
|
|
920
|
+
"""
|
|
921
|
+
|
|
922
|
+
# met parameters along Flight path
|
|
923
|
+
air_pressure = self._sac_flight.air_pressure
|
|
924
|
+
air_temperature = self._sac_flight["air_temperature"]
|
|
925
|
+
specific_humidity = self._sac_flight["specific_humidity"]
|
|
926
|
+
T_critical_sac = self._sac_flight["T_critical_sac"]
|
|
927
|
+
|
|
928
|
+
# Flight performance parameters
|
|
929
|
+
fuel_flow = self._sac_flight.get_data_or_attr("fuel_flow")
|
|
930
|
+
true_airspeed = self._sac_flight["true_airspeed"]
|
|
931
|
+
fuel_dist = fuel_flow / true_airspeed
|
|
932
|
+
|
|
933
|
+
nvpm_ei_n = self._sac_flight.get_data_or_attr("nvpm_ei_n")
|
|
934
|
+
ei_h2o = self._sac_flight.fuel.ei_h2o
|
|
935
|
+
|
|
936
|
+
# get initial contrail parameters from wake vortex simulation
|
|
937
|
+
width = self._sac_flight["width"]
|
|
938
|
+
depth = self._sac_flight["depth"]
|
|
939
|
+
|
|
940
|
+
# initial contrail altitude set to 0.5 * depth
|
|
941
|
+
contrail_1 = GeoVectorDataset(
|
|
942
|
+
longitude=self._sac_flight["longitude"],
|
|
943
|
+
latitude=self._sac_flight["latitude"],
|
|
944
|
+
altitude=self._sac_flight.altitude - 0.5 * depth,
|
|
945
|
+
time=self._sac_flight["time"],
|
|
946
|
+
copy=False,
|
|
947
|
+
)
|
|
948
|
+
|
|
949
|
+
# get met post wake vortex along initial contrail
|
|
950
|
+
interp_kwargs = self.interp_kwargs
|
|
951
|
+
if self.params["preprocess_lowmem"]:
|
|
952
|
+
interp_kwargs["lowmem"] = True
|
|
953
|
+
air_temperature_1 = interpolate_met(met, contrail_1, "air_temperature", **interp_kwargs)
|
|
954
|
+
interpolate_met(met, contrail_1, "specific_humidity", **interp_kwargs)
|
|
955
|
+
|
|
956
|
+
humidity_scaling = self.params["humidity_scaling"]
|
|
957
|
+
if humidity_scaling is not None:
|
|
958
|
+
humidity_scaling.eval(contrail_1, copy_source=False)
|
|
959
|
+
else:
|
|
960
|
+
contrail_1["air_pressure"] = contrail_1.air_pressure
|
|
961
|
+
contrail_1["rhi"] = thermo.rhi(
|
|
962
|
+
contrail_1["specific_humidity"], air_temperature_1, contrail_1["air_pressure"]
|
|
963
|
+
)
|
|
964
|
+
|
|
965
|
+
air_pressure_1 = contrail_1["air_pressure"]
|
|
966
|
+
specific_humidity_1 = contrail_1["specific_humidity"]
|
|
967
|
+
rhi_1 = contrail_1["rhi"]
|
|
968
|
+
level_1 = contrail_1.level
|
|
969
|
+
altitude_1 = contrail_1.altitude
|
|
970
|
+
|
|
971
|
+
# calculate thermo properties post wake vortex
|
|
972
|
+
q_sat_1 = thermo.q_sat_ice(air_temperature_1, air_pressure_1)
|
|
973
|
+
rho_air_1 = thermo.rho_d(air_temperature_1, air_pressure_1)
|
|
974
|
+
|
|
975
|
+
# Initialize initial contrail properties
|
|
976
|
+
iwc = contrail_properties.initial_iwc(
|
|
977
|
+
air_temperature, specific_humidity, air_pressure, fuel_dist, width, depth, ei_h2o
|
|
978
|
+
)
|
|
979
|
+
iwc_ad = contrail_properties.iwc_adiabatic_heating(
|
|
980
|
+
air_temperature, air_pressure, air_pressure_1
|
|
981
|
+
)
|
|
982
|
+
iwc_1 = contrail_properties.iwc_post_wake_vortex(iwc, iwc_ad)
|
|
983
|
+
|
|
984
|
+
if self.params["vpm_activation"]:
|
|
985
|
+
# We can add a Cocip parameter for T_exhaust, vpm_ei_n, and particles
|
|
986
|
+
aei = extended_k15.droplet_apparent_emission_index(
|
|
987
|
+
specific_humidity=specific_humidity,
|
|
988
|
+
T_ambient=air_temperature,
|
|
989
|
+
T_exhaust=self.source.attrs.get("T_exhaust", extended_k15.DEFAULT_EXHAUST_T),
|
|
990
|
+
air_pressure=air_pressure,
|
|
991
|
+
nvpm_ei_n=nvpm_ei_n,
|
|
992
|
+
vpm_ei_n=self.source.attrs.get("vpm_ei_n", extended_k15.DEFAULT_VPM_EI_N),
|
|
993
|
+
G=self._sac_flight["G"],
|
|
994
|
+
)
|
|
995
|
+
min_aei = None # don't clip
|
|
996
|
+
|
|
997
|
+
else:
|
|
998
|
+
f_activation = contrail_properties.ice_particle_activation_rate(
|
|
999
|
+
air_temperature, T_critical_sac
|
|
1000
|
+
)
|
|
1001
|
+
aei = nvpm_ei_n * f_activation
|
|
1002
|
+
min_aei = self.params["min_ice_particle_number_nvpm_ei_n"]
|
|
1003
|
+
|
|
1004
|
+
n_ice_per_m_0 = contrail_properties.initial_ice_particle_number(
|
|
1005
|
+
aei=aei, fuel_dist=fuel_dist, min_aei=min_aei
|
|
1006
|
+
)
|
|
1007
|
+
|
|
1008
|
+
if self.params["unterstrasser_ice_survival_fraction"]:
|
|
1009
|
+
wingspan = self._sac_flight.get_data_or_attr("wingspan")
|
|
1010
|
+
rhi_0 = thermo.rhi(specific_humidity, air_temperature, air_pressure)
|
|
1011
|
+
f_surv = unterstrasser_wake_vortex.ice_particle_number_survival_fraction(
|
|
1012
|
+
air_temperature,
|
|
1013
|
+
rhi_0,
|
|
1014
|
+
ei_h2o,
|
|
1015
|
+
wingspan,
|
|
1016
|
+
true_airspeed,
|
|
1017
|
+
fuel_flow,
|
|
1018
|
+
nvpm_ei_n,
|
|
1019
|
+
0.5 * depth, # Taking the mid-point of the contrail plume
|
|
1020
|
+
)
|
|
1021
|
+
else:
|
|
1022
|
+
f_surv = contrail_properties.ice_particle_survival_fraction(iwc, iwc_1)
|
|
1023
|
+
|
|
1024
|
+
n_ice_per_m_1 = n_ice_per_m_0 * f_surv
|
|
1025
|
+
|
|
1026
|
+
# Check for persistent initial_contrails
|
|
1027
|
+
persistent_1 = contrail_properties.initial_persistent(iwc_1, rhi_1)
|
|
1028
|
+
|
|
1029
|
+
self._sac_flight["altitude_1"] = altitude_1
|
|
1030
|
+
self._sac_flight["level_1"] = level_1
|
|
1031
|
+
self._sac_flight["air_temperature_1"] = air_temperature_1
|
|
1032
|
+
self._sac_flight["specific_humidity_1"] = specific_humidity_1
|
|
1033
|
+
self._sac_flight["q_sat_1"] = q_sat_1
|
|
1034
|
+
self._sac_flight["air_pressure_1"] = air_pressure_1
|
|
1035
|
+
self._sac_flight["rho_air_1"] = rho_air_1
|
|
1036
|
+
self._sac_flight["rhi_1"] = rhi_1
|
|
1037
|
+
self._sac_flight["iwc_1"] = iwc_1
|
|
1038
|
+
self._sac_flight["f_surv"] = f_surv
|
|
1039
|
+
self._sac_flight["n_ice_per_m_0"] = n_ice_per_m_0
|
|
1040
|
+
self._sac_flight["n_ice_per_m_1"] = n_ice_per_m_1
|
|
1041
|
+
self._sac_flight["persistent_1"] = persistent_1
|
|
1042
|
+
|
|
1043
|
+
# Create new Flight only at persistent points
|
|
1044
|
+
if self.params["filter_initially_persistent"]:
|
|
1045
|
+
self._downwash_flight = self._sac_flight.filter(persistent_1.astype(bool))
|
|
1046
|
+
logger.debug(
|
|
1047
|
+
"Fraction of waypoints with initially persistent contrails: %s / %s",
|
|
1048
|
+
len(self._downwash_flight),
|
|
1049
|
+
len(self._sac_flight),
|
|
1050
|
+
)
|
|
1051
|
+
|
|
1052
|
+
else:
|
|
1053
|
+
warnings.warn("Manually overriding initially persistent filter")
|
|
1054
|
+
logger.info("Manually overriding initially persistent filter")
|
|
1055
|
+
self._downwash_flight = self._sac_flight.copy()
|
|
1056
|
+
logger.debug(
|
|
1057
|
+
"Fraction of waypoints with initially persistent contrails: %s / %s",
|
|
1058
|
+
persistent_1.sum(),
|
|
1059
|
+
persistent_1.size,
|
|
1060
|
+
)
|
|
1061
|
+
logger.debug("None are filtered out!")
|
|
1062
|
+
|
|
1063
|
+
def _process_downwash_flight(self) -> tuple[MetDataset | None, MetDataset | None]:
|
|
1064
|
+
"""Create and calculate properties of contrails created by downwash vortex.
|
|
1065
|
+
|
|
1066
|
+
``_downwash_contrail`` is a contrail representation of the waypoints of
|
|
1067
|
+
``_downwash_flight``, which has already been filtered for initial persistent waypoints.
|
|
1068
|
+
|
|
1069
|
+
Returns MetDatasets for subsequent use if ``preprocess_lowmem=False``.
|
|
1070
|
+
"""
|
|
1071
|
+
self._downwash_contrail = self._create_downwash_contrail()
|
|
1072
|
+
buffers = {
|
|
1073
|
+
f"{coord}_buffer": self.params[f"met_{coord}_buffer"]
|
|
1074
|
+
for coord in ("longitude", "latitude", "level")
|
|
1075
|
+
}
|
|
1076
|
+
logger.debug("Downselect met for start of Cocip evolution")
|
|
1077
|
+
met = self._downwash_contrail.downselect_met(self.met, **buffers)
|
|
1078
|
+
met = add_tau_cirrus(met)
|
|
1079
|
+
rad = self._downwash_contrail.downselect_met(self.rad, **buffers)
|
|
1080
|
+
|
|
1081
|
+
calc_continuous(self._downwash_contrail)
|
|
1082
|
+
calc_timestep_geometry(self._downwash_contrail)
|
|
1083
|
+
|
|
1084
|
+
interp_kwargs = self.interp_kwargs
|
|
1085
|
+
if self.params["preprocess_lowmem"]:
|
|
1086
|
+
interp_kwargs["lowmem"] = True
|
|
1087
|
+
calc_timestep_meteorology(self._downwash_contrail, met, self.params, **interp_kwargs)
|
|
1088
|
+
calc_shortwave_radiation(rad, self._downwash_contrail, **interp_kwargs)
|
|
1089
|
+
calc_outgoing_longwave_radiation(rad, self._downwash_contrail, **interp_kwargs)
|
|
1090
|
+
calc_contrail_properties(
|
|
1091
|
+
self._downwash_contrail,
|
|
1092
|
+
self.params["effective_vertical_resolution"],
|
|
1093
|
+
self.params["wind_shear_enhancement_exponent"],
|
|
1094
|
+
self.params["sedimentation_impact_factor"],
|
|
1095
|
+
self.params["radiative_heating_effects"],
|
|
1096
|
+
)
|
|
1097
|
+
|
|
1098
|
+
# Intersect with rad dataset
|
|
1099
|
+
calc_radiative_properties(self._downwash_contrail, self.params)
|
|
1100
|
+
|
|
1101
|
+
if self.params["preprocess_lowmem"]:
|
|
1102
|
+
return None, None
|
|
1103
|
+
return met, rad
|
|
1104
|
+
|
|
1105
|
+
def _simulate_contrail_evolution(self) -> None:
|
|
1106
|
+
"""Simulate contrail evolution."""
|
|
1107
|
+
|
|
1108
|
+
met, rad = self._process_downwash_flight()
|
|
1109
|
+
interp_kwargs = self.interp_kwargs
|
|
1110
|
+
|
|
1111
|
+
contrail_contrail_overlapping = self.params["contrail_contrail_overlapping"]
|
|
1112
|
+
if contrail_contrail_overlapping and not isinstance(self.source, Fleet):
|
|
1113
|
+
warnings.warn("Contrail-Contrail Overlapping is only valid for Fleet mode.")
|
|
1114
|
+
|
|
1115
|
+
# Complete iteration at time_idx - 2
|
|
1116
|
+
for time_idx, time_end in enumerate(self.timesteps[:-1]):
|
|
1117
|
+
logger.debug("Start time integration step %s ending at time %s", time_idx, time_end)
|
|
1118
|
+
|
|
1119
|
+
# get the last evolution step of contrail waypoints, if it exists
|
|
1120
|
+
latest_contrail = self.contrail_list[-1] if self.contrail_list else GeoVectorDataset()
|
|
1121
|
+
|
|
1122
|
+
# load new contrail segments from downwash_flight
|
|
1123
|
+
contrail_2_segments = self._get_contrail_2_segments(time_idx)
|
|
1124
|
+
if contrail_2_segments:
|
|
1125
|
+
logger.debug(
|
|
1126
|
+
"Discover %s new contrail waypoints formed by downwash_flight waypoints.",
|
|
1127
|
+
contrail_2_segments.size,
|
|
1128
|
+
)
|
|
1129
|
+
logger.debug("Previously persistent contrail size: %s", latest_contrail.size)
|
|
1130
|
+
|
|
1131
|
+
# Append new waypoints to latest contrail, or set as first contrail waypoints
|
|
1132
|
+
latest_contrail = latest_contrail + contrail_2_segments
|
|
1133
|
+
|
|
1134
|
+
# CRITICAL: When running in "Fleet" mode, the latest_contrail is no longer
|
|
1135
|
+
# sorted by flight_id. Fixing that below.
|
|
1136
|
+
if isinstance(self.source, Fleet):
|
|
1137
|
+
latest_contrail = latest_contrail.sort(["flight_id", "time"])
|
|
1138
|
+
|
|
1139
|
+
# Check for an empty contrail
|
|
1140
|
+
if not latest_contrail:
|
|
1141
|
+
logger.debug("Empty latest_contrail at timestep %s", time_end)
|
|
1142
|
+
if np.all(time_end > self._downwash_contrail["time"]):
|
|
1143
|
+
logger.debug("No remaining downwash_contrail waypoints. Break.")
|
|
1144
|
+
break
|
|
1145
|
+
continue
|
|
1146
|
+
|
|
1147
|
+
# Update met, rad slices as needed
|
|
1148
|
+
met, rad = self._maybe_downselect_met_rad(met, rad, latest_contrail, time_end)
|
|
1149
|
+
|
|
1150
|
+
# Recalculate latest_contrail with new values
|
|
1151
|
+
# NOTE: We are doing a substantial amount of redundant computation here
|
|
1152
|
+
# At waypoints for which the continuity hasn't changed, there is nothing
|
|
1153
|
+
# new going on, and so we are overwriting variables in latest_contrail
|
|
1154
|
+
# with the same values
|
|
1155
|
+
# NOTE: Both latest_contrail and contrail_2_segments contain all
|
|
1156
|
+
# 54 variables. The only difference is the change in continuity.
|
|
1157
|
+
# The change in continuity impacts:
|
|
1158
|
+
# - sin_a, cos_a, segment_length (in calc_timestep_geometry)
|
|
1159
|
+
# - dsn_dz (in calc_timestep_meteorology, then enhanced in contrail_properties)
|
|
1160
|
+
# And there is huge room to optimize this
|
|
1161
|
+
calc_continuous(latest_contrail)
|
|
1162
|
+
calc_timestep_geometry(latest_contrail)
|
|
1163
|
+
calc_timestep_meteorology(latest_contrail, met, self.params, **interp_kwargs)
|
|
1164
|
+
calc_contrail_properties(
|
|
1165
|
+
latest_contrail,
|
|
1166
|
+
self.params["effective_vertical_resolution"],
|
|
1167
|
+
self.params["wind_shear_enhancement_exponent"],
|
|
1168
|
+
self.params["sedimentation_impact_factor"],
|
|
1169
|
+
self.params["radiative_heating_effects"],
|
|
1170
|
+
)
|
|
1171
|
+
|
|
1172
|
+
final_contrail = calc_timestep_contrail_evolution(
|
|
1173
|
+
met=met,
|
|
1174
|
+
rad=rad,
|
|
1175
|
+
contrail_1=latest_contrail,
|
|
1176
|
+
time_2=time_end,
|
|
1177
|
+
params=self.params,
|
|
1178
|
+
**interp_kwargs,
|
|
1179
|
+
)
|
|
1180
|
+
|
|
1181
|
+
if contrail_contrail_overlapping:
|
|
1182
|
+
final_contrail = _contrail_contrail_overlapping(final_contrail, self.params)
|
|
1183
|
+
|
|
1184
|
+
self.contrail_list.append(final_contrail)
|
|
1185
|
+
|
|
1186
|
+
def _maybe_downselect_met_rad(
|
|
1187
|
+
self,
|
|
1188
|
+
met: MetDataset | None,
|
|
1189
|
+
rad: MetDataset | None,
|
|
1190
|
+
latest_contrail: GeoVectorDataset,
|
|
1191
|
+
time_end: np.datetime64,
|
|
1192
|
+
) -> tuple[MetDataset, MetDataset]:
|
|
1193
|
+
"""Downselect ``self.met`` and ``self.rad`` if necessary to cover ``time_end``.
|
|
1194
|
+
|
|
1195
|
+
If current ``met`` and ``rad`` slices to not include ``time_end``, new slices are selected
|
|
1196
|
+
from ``self.met`` and ``self.rad``. Downselection in space will cover
|
|
1197
|
+
- locations of current contrails (``latest_contrail``),
|
|
1198
|
+
- locations of additional contrails that will be loaded from ``self._downwash_flight``
|
|
1199
|
+
before the new slices expire,
|
|
1200
|
+
plus a user-defined buffer.
|
|
1201
|
+
"""
|
|
1202
|
+
if met is None or time_end > met.indexes["time"].to_numpy()[-1]:
|
|
1203
|
+
logger.debug("Downselect met at time_end %s within Cocip evolution", time_end)
|
|
1204
|
+
met = self._definitely_downselect_met_or_rad(self.met, latest_contrail, time_end)
|
|
1205
|
+
met = add_tau_cirrus(met)
|
|
1206
|
+
|
|
1207
|
+
if rad is None or time_end > rad.indexes["time"].to_numpy()[-1]:
|
|
1208
|
+
logger.debug("Downselect rad at time_end %s within Cocip evolution", time_end)
|
|
1209
|
+
rad = self._definitely_downselect_met_or_rad(self.rad, latest_contrail, time_end)
|
|
1210
|
+
|
|
1211
|
+
return met, rad
|
|
1212
|
+
|
|
1213
|
+
def _definitely_downselect_met_or_rad(
|
|
1214
|
+
self, met: MetDataset, latest_contrail: GeoVectorDataset, time_end: np.datetime64
|
|
1215
|
+
) -> MetDataset:
|
|
1216
|
+
"""Perform downselection when required by :meth:`_maybe_downselect_met_rad`.
|
|
1217
|
+
|
|
1218
|
+
Downselects ``met`` (which should be one of ``self.met`` or ``self.rad``)
|
|
1219
|
+
to cover ``time_end``. Downselection in space covers
|
|
1220
|
+
- locations of current contrails (``latest_contrail``),
|
|
1221
|
+
- locations of additional contrails that will be loaded from ``self._downwash_flight``
|
|
1222
|
+
before the new slices expire,
|
|
1223
|
+
plus a user-defined buffer, as described in :meth:`_maybe_downselect_met_rad`.
|
|
1224
|
+
"""
|
|
1225
|
+
# compute lookahead for future contrails from downwash_flight
|
|
1226
|
+
met_time = met.indexes["time"].to_numpy()
|
|
1227
|
+
mask = met_time >= time_end
|
|
1228
|
+
lookahead = np.min(met_time[mask]) if np.any(mask) else time_end
|
|
1229
|
+
|
|
1230
|
+
# create vector for downselection based on current + future contrails
|
|
1231
|
+
future_contrails = self._downwash_flight.filter(
|
|
1232
|
+
(self._downwash_flight["time"] >= time_end)
|
|
1233
|
+
& (self._downwash_flight["time"] <= lookahead),
|
|
1234
|
+
copy=False,
|
|
1235
|
+
)
|
|
1236
|
+
vector = GeoVectorDataset._from_fastpath(
|
|
1237
|
+
{
|
|
1238
|
+
key: np.concatenate((latest_contrail[key], future_contrails[key]))
|
|
1239
|
+
for key in ("longitude", "latitude", "level", "time")
|
|
1240
|
+
},
|
|
1241
|
+
)
|
|
1242
|
+
|
|
1243
|
+
# compute time buffer to ensure downselection extends to time_end
|
|
1244
|
+
buffers = {
|
|
1245
|
+
f"{coord}_buffer": self.params[f"met_{coord}_buffer"]
|
|
1246
|
+
for coord in ("longitude", "latitude", "level")
|
|
1247
|
+
}
|
|
1248
|
+
buffers["time_buffer"] = (
|
|
1249
|
+
np.timedelta64(0, "ns"),
|
|
1250
|
+
max(np.timedelta64(0, "ns"), time_end - vector["time"].max()),
|
|
1251
|
+
)
|
|
1252
|
+
|
|
1253
|
+
return vector.downselect_met(met, **buffers)
|
|
1254
|
+
|
|
1255
|
+
def _create_downwash_contrail(self) -> GeoVectorDataset:
|
|
1256
|
+
"""Get Contrail representation of downwash flight."""
|
|
1257
|
+
|
|
1258
|
+
downwash_contrail_data = {
|
|
1259
|
+
"waypoint": self._downwash_flight["waypoint"],
|
|
1260
|
+
"flight_id": self._downwash_flight["flight_id"],
|
|
1261
|
+
"time": self._downwash_flight["time"],
|
|
1262
|
+
"longitude": self._downwash_flight["longitude"],
|
|
1263
|
+
"latitude": self._downwash_flight["latitude"],
|
|
1264
|
+
# intentionally specify altitude and level to avoid pressure level calculations
|
|
1265
|
+
"altitude": self._downwash_flight["altitude_1"],
|
|
1266
|
+
"level": self._downwash_flight["level_1"],
|
|
1267
|
+
"air_pressure": self._downwash_flight["air_pressure_1"],
|
|
1268
|
+
"width": self._downwash_flight["width"],
|
|
1269
|
+
"depth": self._downwash_flight["depth"],
|
|
1270
|
+
"sigma_yz": self._downwash_flight["sigma_yz"],
|
|
1271
|
+
"air_temperature": self._downwash_flight["air_temperature_1"],
|
|
1272
|
+
"specific_humidity": self._downwash_flight["specific_humidity_1"],
|
|
1273
|
+
"q_sat": self._downwash_flight["q_sat_1"],
|
|
1274
|
+
"rho_air": self._downwash_flight["rho_air_1"],
|
|
1275
|
+
"rhi": self._downwash_flight["rhi_1"],
|
|
1276
|
+
"iwc": self._downwash_flight["iwc_1"],
|
|
1277
|
+
"n_ice_per_m": self._downwash_flight["n_ice_per_m_1"],
|
|
1278
|
+
"persistent": self._downwash_flight["persistent_1"],
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
contrail = GeoVectorDataset._from_fastpath(downwash_contrail_data).copy()
|
|
1282
|
+
contrail["formation_time"] = contrail["time"].copy()
|
|
1283
|
+
contrail["age"] = contrail["formation_time"] - contrail["time"]
|
|
1284
|
+
|
|
1285
|
+
# Heating rate, differential heating rate and
|
|
1286
|
+
# cumulative heat energy absorbed by the contrail
|
|
1287
|
+
if self.params["radiative_heating_effects"]:
|
|
1288
|
+
contrail["heat_rate"] = np.zeros_like(contrail["n_ice_per_m"])
|
|
1289
|
+
contrail["d_heat_rate"] = np.zeros_like(contrail["n_ice_per_m"])
|
|
1290
|
+
|
|
1291
|
+
# Increase in temperature averaged over the contrail plume
|
|
1292
|
+
contrail["cumul_heat"] = np.zeros_like(contrail["n_ice_per_m"])
|
|
1293
|
+
|
|
1294
|
+
# Temperature difference between the upper and lower half of the contrail plume
|
|
1295
|
+
contrail["cumul_differential_heat"] = np.zeros_like(contrail["n_ice_per_m"])
|
|
1296
|
+
|
|
1297
|
+
# Initially set energy forcing to 0 because the contrail just formed (age = 0)
|
|
1298
|
+
contrail["ef"] = np.zeros_like(contrail["n_ice_per_m"])
|
|
1299
|
+
if self.params["compute_atr20"]:
|
|
1300
|
+
contrail["global_yearly_mean_rf"] = np.zeros_like(contrail["n_ice_per_m"])
|
|
1301
|
+
contrail["atr20"] = np.zeros_like(contrail["n_ice_per_m"])
|
|
1302
|
+
|
|
1303
|
+
if not self.params["filter_sac"]:
|
|
1304
|
+
contrail["sac"] = self._downwash_flight["sac"]
|
|
1305
|
+
if not self.params["filter_initially_persistent"]:
|
|
1306
|
+
contrail["initially_persistent"] = self._downwash_flight["persistent_1"]
|
|
1307
|
+
if self.params["persistent_buffer"] is not None:
|
|
1308
|
+
contrail["end_of_life"] = np.full(contrail.size, np.datetime64("NaT", "ns"))
|
|
1309
|
+
|
|
1310
|
+
return contrail
|
|
1311
|
+
|
|
1312
|
+
def _get_contrail_2_segments(self, time_idx: int) -> GeoVectorDataset:
|
|
1313
|
+
"""Get batch of newly formed contrail segments."""
|
|
1314
|
+
time = self._downwash_contrail["time"]
|
|
1315
|
+
t_cur = self.timesteps[time_idx]
|
|
1316
|
+
if time_idx == 0:
|
|
1317
|
+
filt = time < t_cur
|
|
1318
|
+
else:
|
|
1319
|
+
t_prev = self.timesteps[time_idx - 1]
|
|
1320
|
+
filt = (time < t_cur) & (time >= t_prev)
|
|
1321
|
+
|
|
1322
|
+
return self._downwash_contrail.filter(filt)
|
|
1323
|
+
|
|
1324
|
+
@override
|
|
1325
|
+
def _cleanup_indices(self) -> None:
|
|
1326
|
+
"""Cleanup interpolation artifacts."""
|
|
1327
|
+
|
|
1328
|
+
if not self.params["interpolation_use_indices"]:
|
|
1329
|
+
return
|
|
1330
|
+
|
|
1331
|
+
if hasattr(self, "contrail_list"):
|
|
1332
|
+
for contrail in self.contrail_list:
|
|
1333
|
+
contrail._invalidate_indices()
|
|
1334
|
+
|
|
1335
|
+
self.source._invalidate_indices()
|
|
1336
|
+
self._sac_flight._invalidate_indices()
|
|
1337
|
+
if hasattr(self, "_downwash_flight"):
|
|
1338
|
+
self._downwash_flight._invalidate_indices()
|
|
1339
|
+
if hasattr(self, "_downwash_contrail"):
|
|
1340
|
+
self._downwash_contrail._invalidate_indices()
|
|
1341
|
+
|
|
1342
|
+
def _bundle_results(self) -> None:
|
|
1343
|
+
# ---
|
|
1344
|
+
# Create contrail dataframe (self.contrail)
|
|
1345
|
+
# ---
|
|
1346
|
+
self.contrail = GeoVectorDataset.sum(self.contrail_list).dataframe
|
|
1347
|
+
self.contrail["timestep"] = np.concatenate(
|
|
1348
|
+
[np.full(c.size, i) for i, c in enumerate(self.contrail_list)]
|
|
1349
|
+
)
|
|
1350
|
+
|
|
1351
|
+
# add age in hours to the contrail waypoint outputs
|
|
1352
|
+
age_hours = np.empty_like(self.contrail["ef"])
|
|
1353
|
+
np.divide(self.contrail["age"], np.timedelta64(1, "h"), out=age_hours)
|
|
1354
|
+
self.contrail["age_hours"] = age_hours
|
|
1355
|
+
|
|
1356
|
+
verbose_outputs = self.params["verbose_outputs"]
|
|
1357
|
+
if verbose_outputs:
|
|
1358
|
+
# Compute dt_integration -- logic is somewhat complicated, but
|
|
1359
|
+
# we're simply addressing that the first dt_integration
|
|
1360
|
+
# is different from the rest
|
|
1361
|
+
|
|
1362
|
+
# We call reset_index to introduces an `index` RangeIndex column,
|
|
1363
|
+
# Which we use in the `groupby` to identify the
|
|
1364
|
+
# index of the first evolution step at each waypoint.
|
|
1365
|
+
tmp = self.contrail.reset_index()
|
|
1366
|
+
cols = ["formation_time", "time", "index"]
|
|
1367
|
+
first_form_time = tmp.groupby("waypoint")[cols].first()
|
|
1368
|
+
first_dt = first_form_time["time"] - first_form_time["formation_time"]
|
|
1369
|
+
first_dt = first_dt.set_axis(first_form_time["index"])
|
|
1370
|
+
|
|
1371
|
+
self.contrail = tmp.set_index("index")
|
|
1372
|
+
self.contrail["dt_integration"] = first_dt
|
|
1373
|
+
self.contrail.fillna({"dt_integration": self.params["dt_integration"]}, inplace=True)
|
|
1374
|
+
|
|
1375
|
+
# ---
|
|
1376
|
+
# Create contrail xr.Dataset (self.contrail_dataset)
|
|
1377
|
+
# ---
|
|
1378
|
+
if isinstance(self.source, Fleet):
|
|
1379
|
+
keys = ["flight_id", "timestep", "waypoint"]
|
|
1380
|
+
else:
|
|
1381
|
+
keys = ["timestep", "waypoint"]
|
|
1382
|
+
self.contrail_dataset = xr.Dataset.from_dataframe(self.contrail.set_index(keys))
|
|
1383
|
+
|
|
1384
|
+
# ---
|
|
1385
|
+
# Create output Flight / Fleet (self.source)
|
|
1386
|
+
# ---
|
|
1387
|
+
|
|
1388
|
+
col_idx = ["flight_id", "waypoint"] if isinstance(self.source, Fleet) else ["waypoint"]
|
|
1389
|
+
del self.source["_met_intersection"]
|
|
1390
|
+
|
|
1391
|
+
# Attach intermediate calculations from `sac_flight` and `downwash_flight` to flight
|
|
1392
|
+
sac_cols = [
|
|
1393
|
+
"width",
|
|
1394
|
+
"depth",
|
|
1395
|
+
"rhi_1",
|
|
1396
|
+
"air_temperature_1",
|
|
1397
|
+
"specific_humidity_1",
|
|
1398
|
+
"altitude_1",
|
|
1399
|
+
"persistent_1",
|
|
1400
|
+
]
|
|
1401
|
+
|
|
1402
|
+
# add additional columns
|
|
1403
|
+
if verbose_outputs:
|
|
1404
|
+
sac_cols += ["dT_dz", "ds_dz", "dz_max"]
|
|
1405
|
+
|
|
1406
|
+
downwash_cols = [
|
|
1407
|
+
"rho_air_1",
|
|
1408
|
+
"iwc_1",
|
|
1409
|
+
"f_surv",
|
|
1410
|
+
"n_ice_per_m_0",
|
|
1411
|
+
"n_ice_per_m_1",
|
|
1412
|
+
]
|
|
1413
|
+
df = pd.concat(
|
|
1414
|
+
[
|
|
1415
|
+
self.source.dataframe.set_index(col_idx),
|
|
1416
|
+
self._sac_flight.dataframe.set_index(col_idx)[sac_cols],
|
|
1417
|
+
self._downwash_flight.dataframe.set_index(col_idx)[downwash_cols],
|
|
1418
|
+
],
|
|
1419
|
+
axis=1,
|
|
1420
|
+
)
|
|
1421
|
+
|
|
1422
|
+
# Aggregate contrail data back to flight
|
|
1423
|
+
grouped = self.contrail.groupby(col_idx)
|
|
1424
|
+
|
|
1425
|
+
# Perform all aggregations
|
|
1426
|
+
agg_dict = {"ef": ["sum"], "age": ["max"]}
|
|
1427
|
+
if self.params["compute_atr20"]:
|
|
1428
|
+
agg_dict["global_yearly_mean_rf"] = ["sum"]
|
|
1429
|
+
agg_dict["atr20"] = ["sum"]
|
|
1430
|
+
|
|
1431
|
+
rad_keys = ["sdr", "rsr", "olr", "rf_sw", "rf_lw", "rf_net"]
|
|
1432
|
+
for key in rad_keys:
|
|
1433
|
+
if verbose_outputs:
|
|
1434
|
+
agg_dict[key] = ["mean", "min", "max"]
|
|
1435
|
+
else:
|
|
1436
|
+
agg_dict[key] = ["mean"]
|
|
1437
|
+
|
|
1438
|
+
aggregated = grouped.agg(agg_dict)
|
|
1439
|
+
aggregated.columns = [f"{k1}_{k2}" for k1, k2 in aggregated.columns]
|
|
1440
|
+
aggregated = aggregated.rename(columns={"ef_sum": "ef", "age_max": "contrail_age"})
|
|
1441
|
+
if self.params["compute_atr20"]:
|
|
1442
|
+
aggregated = aggregated.rename(
|
|
1443
|
+
columns={"global_yearly_mean_rf_sum": "global_yearly_mean_rf", "atr20_sum": "atr20"}
|
|
1444
|
+
)
|
|
1445
|
+
|
|
1446
|
+
# Join the two
|
|
1447
|
+
df = df.join(aggregated)
|
|
1448
|
+
|
|
1449
|
+
# Fill missing values for ef and contrail_age per conventions
|
|
1450
|
+
# Mean, max, and min radiative values are *not* filled with 0
|
|
1451
|
+
df.fillna({"ef": 0.0, "contrail_age": np.timedelta64(0, "ns")}, inplace=True)
|
|
1452
|
+
|
|
1453
|
+
# cocip flag for each waypoint
|
|
1454
|
+
# -1 if negative EF, 0 if no EF, 1 if positive EF,
|
|
1455
|
+
# or NaN for outside of domain of flight waypoints that don't persist
|
|
1456
|
+
df["cocip"] = np.sign(df["ef"])
|
|
1457
|
+
logger.debug("Total number of waypoints with nonzero EF: %s", df["cocip"].ne(0.0).sum())
|
|
1458
|
+
|
|
1459
|
+
# reset the index
|
|
1460
|
+
df = df.reset_index()
|
|
1461
|
+
|
|
1462
|
+
# Reassign to source
|
|
1463
|
+
self.source.data = VectorDataDict({k: v.to_numpy() for k, v in df.items()})
|
|
1464
|
+
|
|
1465
|
+
def _fill_empty_flight_results(self, return_list_flight: bool) -> Flight | list[Flight]:
|
|
1466
|
+
"""Fill empty results into flight / fleet and return.
|
|
1467
|
+
|
|
1468
|
+
This method attaches an all nan array to each of the variables:
|
|
1469
|
+
- sdr
|
|
1470
|
+
- rsr
|
|
1471
|
+
- olr
|
|
1472
|
+
- rf_sw
|
|
1473
|
+
- rf_lw
|
|
1474
|
+
- rf_net
|
|
1475
|
+
|
|
1476
|
+
This method also attaches zeros (for trajectory points contained within met grid)
|
|
1477
|
+
or nans (for trajectory points outside of the met grid) to the following variables.
|
|
1478
|
+
- ef
|
|
1479
|
+
- cocip
|
|
1480
|
+
- contrail_age
|
|
1481
|
+
- persistent_1
|
|
1482
|
+
|
|
1483
|
+
Parameters
|
|
1484
|
+
----------
|
|
1485
|
+
return_list_flight : bool
|
|
1486
|
+
If True, a list of :class:`Flight` is returned. In this case, :attr:`source`
|
|
1487
|
+
is assumed to be a :class:`Fleet`.
|
|
1488
|
+
|
|
1489
|
+
Returns
|
|
1490
|
+
-------
|
|
1491
|
+
Flight | list[Flight]
|
|
1492
|
+
Flight or list of Flight objects with empty variables.
|
|
1493
|
+
"""
|
|
1494
|
+
self._cleanup_indices()
|
|
1495
|
+
|
|
1496
|
+
intersection = self.source.data.pop("_met_intersection")
|
|
1497
|
+
zeros_and_nans = np.zeros(intersection.shape, dtype=np.float32)
|
|
1498
|
+
zeros_and_nans[~intersection] = np.nan
|
|
1499
|
+
self.source["ef"] = zeros_and_nans.copy()
|
|
1500
|
+
self.source["persistent_1"] = zeros_and_nans.copy()
|
|
1501
|
+
self.source["cocip"] = np.sign(zeros_and_nans)
|
|
1502
|
+
self.source["contrail_age"] = zeros_and_nans.astype("timedelta64[ns]")
|
|
1503
|
+
|
|
1504
|
+
if return_list_flight:
|
|
1505
|
+
return self.source.to_flight_list() # type: ignore[attr-defined]
|
|
1506
|
+
|
|
1507
|
+
return self.source
|
|
1508
|
+
|
|
1509
|
+
|
|
1510
|
+
# ----------------------------------------
|
|
1511
|
+
# Functions used in Cocip and CocipGrid
|
|
1512
|
+
# ----------------------------------------
|
|
1513
|
+
|
|
1514
|
+
|
|
1515
|
+
def process_met_datasets(
|
|
1516
|
+
met: MetDataset,
|
|
1517
|
+
rad: MetDataset,
|
|
1518
|
+
compute_tau_cirrus: bool | Literal["auto"] = "auto",
|
|
1519
|
+
) -> tuple[MetDataset, MetDataset]:
|
|
1520
|
+
"""Process and verify ERA5 data for :class:`Cocip` and :class:`CocipGrid`.
|
|
1521
|
+
|
|
1522
|
+
The implementation uses :class:`Cocip` for the source of truth in determining
|
|
1523
|
+
which met variables are required.
|
|
1524
|
+
|
|
1525
|
+
.. versionchanged:: 0.25.0
|
|
1526
|
+
|
|
1527
|
+
This method is called in the :class:`CocipGrid` constructor regardless
|
|
1528
|
+
of the ``process_met`` parameter. The same approach is also taken
|
|
1529
|
+
in :class:`Cocip` in version 0.27.0.
|
|
1530
|
+
|
|
1531
|
+
.. versionchanged:: 0.48.0
|
|
1532
|
+
|
|
1533
|
+
Remove the ``shift_radiation_time`` parameter. This parameter is now
|
|
1534
|
+
inferred from the metadata on the `rad` instance.
|
|
1535
|
+
|
|
1536
|
+
Parameters
|
|
1537
|
+
----------
|
|
1538
|
+
met : MetDataset
|
|
1539
|
+
Met pressure-level data
|
|
1540
|
+
rad : MetDataset
|
|
1541
|
+
Rad single-level data
|
|
1542
|
+
compute_tau_cirrus : bool | Literal["auto"]
|
|
1543
|
+
Whether to add ``"tau_cirrus"`` variable to pressure-level met data. If set to
|
|
1544
|
+
``"auto"``, ``"tau_cirrus"`` will be computed iff the met data is dask-backed.
|
|
1545
|
+
|
|
1546
|
+
Returns
|
|
1547
|
+
-------
|
|
1548
|
+
met : MetDataset
|
|
1549
|
+
Met data, possibly with "tau_cirrus" variable attached.
|
|
1550
|
+
rad : MetDataset
|
|
1551
|
+
Rad data with time shifted to account for accumulated values.
|
|
1552
|
+
|
|
1553
|
+
Raises
|
|
1554
|
+
------
|
|
1555
|
+
If a previous version of pycontrails has already scaled the gridded humidity
|
|
1556
|
+
data.
|
|
1557
|
+
"""
|
|
1558
|
+
# Check for remnants of previous scaling.
|
|
1559
|
+
if "_pycontrails_modified" in met["specific_humidity"].attrs:
|
|
1560
|
+
raise ValueError(
|
|
1561
|
+
"Specific humidity enhancement of the raw specific humidity values in "
|
|
1562
|
+
"the underlying met data is deprecated."
|
|
1563
|
+
)
|
|
1564
|
+
|
|
1565
|
+
if compute_tau_cirrus == "auto":
|
|
1566
|
+
# If met data is dask-backed, compute tau_cirrus
|
|
1567
|
+
compute_tau_cirrus = met.data["air_temperature"].chunks is not None
|
|
1568
|
+
|
|
1569
|
+
if "tau_cirrus" not in met.data:
|
|
1570
|
+
met.ensure_vars(Cocip.met_variables)
|
|
1571
|
+
if compute_tau_cirrus:
|
|
1572
|
+
met = add_tau_cirrus(met)
|
|
1573
|
+
else:
|
|
1574
|
+
met.ensure_vars(Cocip.processed_met_variables)
|
|
1575
|
+
|
|
1576
|
+
rad.ensure_vars(Cocip.rad_variables)
|
|
1577
|
+
rad = _process_rad(rad)
|
|
1578
|
+
|
|
1579
|
+
return met, rad
|
|
1580
|
+
|
|
1581
|
+
|
|
1582
|
+
def add_tau_cirrus(met: MetDataset) -> MetDataset:
|
|
1583
|
+
"""Add "tau_cirrus" variable to weather data if it does not exist already.
|
|
1584
|
+
|
|
1585
|
+
Parameters
|
|
1586
|
+
----------
|
|
1587
|
+
met : MetDataset
|
|
1588
|
+
Met pressure-level data.
|
|
1589
|
+
|
|
1590
|
+
Returns
|
|
1591
|
+
-------
|
|
1592
|
+
met : MetDataset
|
|
1593
|
+
Met data with "tau_cirrus" variable attached.
|
|
1594
|
+
"""
|
|
1595
|
+
if "tau_cirrus" not in met.data:
|
|
1596
|
+
met.data["tau_cirrus"] = tau_cirrus.tau_cirrus(met)
|
|
1597
|
+
return met
|
|
1598
|
+
|
|
1599
|
+
|
|
1600
|
+
def _process_rad(rad: MetDataset) -> MetDataset:
|
|
1601
|
+
"""Process radiation specific variables for model.
|
|
1602
|
+
|
|
1603
|
+
These variables are used to calculate the reflected solar radiation (RSR),
|
|
1604
|
+
and outgoing longwave radiation (OLR).
|
|
1605
|
+
|
|
1606
|
+
The time stamp is adjusted by ``shift_radiation_time`` to account for
|
|
1607
|
+
accumulated values being averaged over the time period.
|
|
1608
|
+
|
|
1609
|
+
Parameters
|
|
1610
|
+
----------
|
|
1611
|
+
rad : MetDataset
|
|
1612
|
+
Rad single-level data
|
|
1613
|
+
|
|
1614
|
+
Returns
|
|
1615
|
+
-------
|
|
1616
|
+
MetDataset
|
|
1617
|
+
Rad data with time shifted.
|
|
1618
|
+
|
|
1619
|
+
Raises
|
|
1620
|
+
------
|
|
1621
|
+
ValueError
|
|
1622
|
+
If a "radiation_accumulated" field is not found on ``rad.attrs``.
|
|
1623
|
+
|
|
1624
|
+
Notes
|
|
1625
|
+
-----
|
|
1626
|
+
- https://www.ecmwf.int/sites/default/files/elibrary/2015/18490-radiation-quantities-ecmwf-model-and-mars.pdf
|
|
1627
|
+
- https://confluence.ecmwf.int/pages/viewpage.action?pageId=155337784
|
|
1628
|
+
"""
|
|
1629
|
+
# If the time coordinate has already been shifted, early return
|
|
1630
|
+
if "shift_radiation_time" in rad["time"].attrs:
|
|
1631
|
+
return rad
|
|
1632
|
+
|
|
1633
|
+
provider = rad.provider_attr
|
|
1634
|
+
|
|
1635
|
+
# Only shift ECMWF data -- exit for anything else
|
|
1636
|
+
# A warning is emitted upstream if the provider is not ECMWF or NCEP
|
|
1637
|
+
if provider != "ECMWF":
|
|
1638
|
+
return rad
|
|
1639
|
+
|
|
1640
|
+
dataset = rad.dataset_attr
|
|
1641
|
+
product = rad.product_attr
|
|
1642
|
+
|
|
1643
|
+
if dataset == "HRES":
|
|
1644
|
+
try:
|
|
1645
|
+
radiation_accumulated = rad.attrs["radiation_accumulated"]
|
|
1646
|
+
except KeyError as exc:
|
|
1647
|
+
msg = (
|
|
1648
|
+
"HRES data must have a boolean 'radiation_accumulated' attribute. "
|
|
1649
|
+
"This attribute is used to determine whether the radiation data "
|
|
1650
|
+
"has been accumulated over the time period. This is the case for "
|
|
1651
|
+
"HRES data taken from a common time of forecast with multiple "
|
|
1652
|
+
"forecast steps. If this is not the case, set the "
|
|
1653
|
+
"'radiation_accumulated' attribute to False."
|
|
1654
|
+
)
|
|
1655
|
+
raise ValueError(msg) from exc
|
|
1656
|
+
if radiation_accumulated:
|
|
1657
|
+
# Don't assume that radiation data is uniformly spaced in time
|
|
1658
|
+
# Instead, infer the appropriate time shift
|
|
1659
|
+
time_diff = rad.data["time"].diff("time", label="upper")
|
|
1660
|
+
time_shift = -time_diff / 2
|
|
1661
|
+
|
|
1662
|
+
# Keep the original attrs -- we need these later on
|
|
1663
|
+
old_attrs = {k: v.attrs for k, v in rad.data.items()}
|
|
1664
|
+
|
|
1665
|
+
# Also need to keep dataset-level attrs, which are lost
|
|
1666
|
+
# when dividing a Dataset by a DataArray
|
|
1667
|
+
old_rad_attrs = rad.data.attrs
|
|
1668
|
+
|
|
1669
|
+
# NOTE: Taking the diff will remove the first time step
|
|
1670
|
+
# This is typically what we want (forecast step 0 is all zeros)
|
|
1671
|
+
# But, if the data has been downselected for a particular Flight / Fleet,
|
|
1672
|
+
# we lose the first time step of the data.
|
|
1673
|
+
#
|
|
1674
|
+
# Other portions of the code convert HRES accumulated fluxes (J/m2)
|
|
1675
|
+
# to averaged fluxes (W/m2) assuming that accumulations are over
|
|
1676
|
+
# one hour. For those conversions to work correctly, must normalize
|
|
1677
|
+
# accumulations by number of hours between steps
|
|
1678
|
+
time_diff_h = time_diff / np.timedelta64(1, "h")
|
|
1679
|
+
rad.data = rad.data.diff("time", label="upper") / time_diff_h
|
|
1680
|
+
rad.data.attrs = old_rad_attrs
|
|
1681
|
+
|
|
1682
|
+
# Add back the original attrs
|
|
1683
|
+
for k, v in rad.data.items():
|
|
1684
|
+
v.attrs = old_attrs[k]
|
|
1685
|
+
|
|
1686
|
+
# Short-circuit to avoid idiot check
|
|
1687
|
+
rad.data = rad.data.assign_coords({"time": rad.data["time"] + time_shift})
|
|
1688
|
+
if np.unique(time_shift).size == 1:
|
|
1689
|
+
rad.data["time"].attrs["shift_radiation_time"] = str(time_shift.values[0])
|
|
1690
|
+
else:
|
|
1691
|
+
rad.data["time"].attrs["shift_radiation_time"] = "variable"
|
|
1692
|
+
return rad
|
|
1693
|
+
|
|
1694
|
+
shift_radiation_time = -np.timedelta64(30, "m")
|
|
1695
|
+
|
|
1696
|
+
elif dataset == "ERA5" and product == "ensemble":
|
|
1697
|
+
shift_radiation_time = -np.timedelta64(90, "m")
|
|
1698
|
+
else:
|
|
1699
|
+
shift_radiation_time = -np.timedelta64(30, "m")
|
|
1700
|
+
|
|
1701
|
+
# Do a final idiot check -- most likely, the time resolution of the data will
|
|
1702
|
+
# agree with the shift_radiation_time. If not, emit a warning. There could be
|
|
1703
|
+
# a false positive here if the data has been downsampled in time.
|
|
1704
|
+
logger.debug("Shifting rad time by %s", shift_radiation_time)
|
|
1705
|
+
rad_time_diff = np.diff(rad.data["time"])
|
|
1706
|
+
if not np.all(rad_time_diff / 2 == -shift_radiation_time):
|
|
1707
|
+
warnings.warn(
|
|
1708
|
+
f"Shifting radiation time dimension by unexpected interval {shift_radiation_time}. "
|
|
1709
|
+
f"The rad data has metadata indicating it is {product} ECMWF data. "
|
|
1710
|
+
f"This dataset should have time steps of {-2 * shift_radiation_time}."
|
|
1711
|
+
)
|
|
1712
|
+
|
|
1713
|
+
rad.data = rad.data.assign_coords({"time": rad.data["time"] + shift_radiation_time})
|
|
1714
|
+
rad.data["time"].attrs["shift_radiation_time"] = str(shift_radiation_time)
|
|
1715
|
+
|
|
1716
|
+
return rad
|
|
1717
|
+
|
|
1718
|
+
|
|
1719
|
+
def _eval_aircraft_performance(
|
|
1720
|
+
aircraft_performance: AircraftPerformance | None, flight: Flight
|
|
1721
|
+
) -> Flight:
|
|
1722
|
+
"""Evaluate the :class:`AircraftPerformance` model.
|
|
1723
|
+
|
|
1724
|
+
Parameters
|
|
1725
|
+
----------
|
|
1726
|
+
aircraft_performance : AircraftPerformance | None
|
|
1727
|
+
Input aircraft performance model
|
|
1728
|
+
flight : Flight
|
|
1729
|
+
Flight to evaluate
|
|
1730
|
+
|
|
1731
|
+
Returns
|
|
1732
|
+
-------
|
|
1733
|
+
Flight
|
|
1734
|
+
Output from aircraft performance model
|
|
1735
|
+
|
|
1736
|
+
Raises
|
|
1737
|
+
------
|
|
1738
|
+
ValueError
|
|
1739
|
+
If ``aircraft_performance`` is None
|
|
1740
|
+
"""
|
|
1741
|
+
|
|
1742
|
+
ap_vars = {"wingspan", "engine_efficiency", "fuel_flow", "aircraft_mass"}
|
|
1743
|
+
missing = ap_vars.difference(flight).difference(flight.attrs)
|
|
1744
|
+
if not missing:
|
|
1745
|
+
return flight
|
|
1746
|
+
|
|
1747
|
+
if aircraft_performance is None:
|
|
1748
|
+
msg = (
|
|
1749
|
+
f"An AircraftPerformance model parameter is required if the flight does not contain "
|
|
1750
|
+
f"the following variables: {ap_vars}. This flight is missing: {missing}. "
|
|
1751
|
+
"Instantiate the Cocip model with an AircraftPerformance model. "
|
|
1752
|
+
"For example, 'Cocip(..., aircraft_performance=PSFlight(...))'."
|
|
1753
|
+
)
|
|
1754
|
+
raise ValueError(msg)
|
|
1755
|
+
|
|
1756
|
+
return aircraft_performance.eval(source=flight, copy_source=False)
|
|
1757
|
+
|
|
1758
|
+
|
|
1759
|
+
def _eval_emissions(emissions: Emissions, flight: Flight) -> Flight:
|
|
1760
|
+
"""Evaluate the :class:`Emissions` model.
|
|
1761
|
+
|
|
1762
|
+
Parameters
|
|
1763
|
+
----------
|
|
1764
|
+
emissions : Emissions
|
|
1765
|
+
Emissions model
|
|
1766
|
+
flight : Flight
|
|
1767
|
+
Flight to evaluate
|
|
1768
|
+
|
|
1769
|
+
Returns
|
|
1770
|
+
-------
|
|
1771
|
+
Flight
|
|
1772
|
+
Output from emissions model
|
|
1773
|
+
"""
|
|
1774
|
+
|
|
1775
|
+
emissions_outputs = "nvpm_ei_n"
|
|
1776
|
+
if flight.ensure_vars(emissions_outputs, False):
|
|
1777
|
+
return flight
|
|
1778
|
+
return emissions.eval(source=flight, copy_source=False)
|
|
1779
|
+
|
|
1780
|
+
|
|
1781
|
+
def calc_continuous(contrail: GeoVectorDataset) -> None:
|
|
1782
|
+
"""Calculate the continuous segments of this timestep.
|
|
1783
|
+
|
|
1784
|
+
Mutates parameter ``contrail`` in place by setting or updating the
|
|
1785
|
+
"continuous" variable.
|
|
1786
|
+
|
|
1787
|
+
Parameters
|
|
1788
|
+
----------
|
|
1789
|
+
contrail : GeoVectorDataset
|
|
1790
|
+
GeoVectorDataset instance onto which "continuous" is set.
|
|
1791
|
+
|
|
1792
|
+
Raises
|
|
1793
|
+
------
|
|
1794
|
+
ValueError
|
|
1795
|
+
If ``contrail`` is empty.
|
|
1796
|
+
"""
|
|
1797
|
+
if not contrail:
|
|
1798
|
+
raise ValueError("Cannot calculate continuous on an empty contrail")
|
|
1799
|
+
same_flight = contrail["flight_id"][:-1] == contrail["flight_id"][1:]
|
|
1800
|
+
consecutive_waypoint = np.diff(contrail["waypoint"]) == 1
|
|
1801
|
+
continuous = np.empty(contrail.size, dtype=bool)
|
|
1802
|
+
continuous[:-1] = same_flight & consecutive_waypoint
|
|
1803
|
+
continuous[-1] = False # This fails if contrail is empty
|
|
1804
|
+
contrail.update(continuous=continuous) # overwrite continuous
|
|
1805
|
+
|
|
1806
|
+
|
|
1807
|
+
def calc_timestep_geometry(contrail: GeoVectorDataset) -> None:
|
|
1808
|
+
"""Calculate contrail segment-specific properties.
|
|
1809
|
+
|
|
1810
|
+
Mutates parameter ``contrail`` in place by setting or updating the variables
|
|
1811
|
+
- "sin_a"
|
|
1812
|
+
- "cos_a"
|
|
1813
|
+
- "segment_length"
|
|
1814
|
+
|
|
1815
|
+
Any nan values in these variables are set to 0. This ensures any segment-based property
|
|
1816
|
+
derived from the segment geometry does not contribute to contrail energy forcing.
|
|
1817
|
+
|
|
1818
|
+
See Also
|
|
1819
|
+
--------
|
|
1820
|
+
- :func:`wind_shear.wind_shear_normal` to see how "sin_a" and "cos_a"
|
|
1821
|
+
are used to compute wind shear terms.
|
|
1822
|
+
- :func:`calc_timestep_contrail_evolution` to see how "segment_length" is used.
|
|
1823
|
+
|
|
1824
|
+
Parameters
|
|
1825
|
+
----------
|
|
1826
|
+
contrail : GeoVectorDataset
|
|
1827
|
+
GeoVectorDataset instance onto which variables are set.
|
|
1828
|
+
"""
|
|
1829
|
+
# get contrail waypoints
|
|
1830
|
+
longitude = contrail["longitude"]
|
|
1831
|
+
latitude = contrail["latitude"]
|
|
1832
|
+
altitude = contrail.altitude
|
|
1833
|
+
continuous = contrail["continuous"]
|
|
1834
|
+
|
|
1835
|
+
# calculate segment properties
|
|
1836
|
+
segment_length = geo.segment_length(longitude, latitude, altitude)
|
|
1837
|
+
sin_a, cos_a = geo.segment_angle(longitude, latitude)
|
|
1838
|
+
|
|
1839
|
+
# NOTE: Set all nan and discontinuous values to 0. With our current implementation, this
|
|
1840
|
+
# ensures degenerate or discontinuous segments do not contribute to contrail impact.
|
|
1841
|
+
# At the same time, this prevents nan values from propagating through evolving contrails.
|
|
1842
|
+
#
|
|
1843
|
+
# If we want to change this, take note of the following consequences.
|
|
1844
|
+
# nan values flow from one variable to another as follows:
|
|
1845
|
+
# segment_length
|
|
1846
|
+
# seg_ratio
|
|
1847
|
+
# sigma_yz
|
|
1848
|
+
# area_eff_2
|
|
1849
|
+
# iwc, n_ice_per_m, r_vol_ice
|
|
1850
|
+
# terminal_fall_speed
|
|
1851
|
+
# level [level advection]
|
|
1852
|
+
# u_wind, v_wind [interp]
|
|
1853
|
+
# longitude, latitude [advection]
|
|
1854
|
+
# Finally, at the next evolution step, the previous waypoint will accrue
|
|
1855
|
+
# a nan value after segment_length is recalculated
|
|
1856
|
+
|
|
1857
|
+
segment_length[~continuous] = 0.0
|
|
1858
|
+
sin_a[~continuous] = 0.0
|
|
1859
|
+
cos_a[~continuous] = 0.0
|
|
1860
|
+
|
|
1861
|
+
np.nan_to_num(segment_length, copy=False)
|
|
1862
|
+
np.nan_to_num(sin_a, copy=False)
|
|
1863
|
+
np.nan_to_num(cos_a, copy=False)
|
|
1864
|
+
|
|
1865
|
+
# override values on model
|
|
1866
|
+
contrail.update(segment_length=segment_length)
|
|
1867
|
+
contrail.update(sin_a=sin_a)
|
|
1868
|
+
contrail.update(cos_a=cos_a)
|
|
1869
|
+
|
|
1870
|
+
|
|
1871
|
+
def calc_timestep_meteorology(
|
|
1872
|
+
contrail: GeoVectorDataset,
|
|
1873
|
+
met: MetDataset,
|
|
1874
|
+
params: dict[str, Any],
|
|
1875
|
+
**interp_kwargs: Any,
|
|
1876
|
+
) -> None:
|
|
1877
|
+
"""Get and store meteorology parameters.
|
|
1878
|
+
|
|
1879
|
+
Mutates parameter ``contrail`` in place by setting or updating the variables
|
|
1880
|
+
- "sin_a"
|
|
1881
|
+
- "cos_a"
|
|
1882
|
+
- "segment_length"
|
|
1883
|
+
|
|
1884
|
+
Parameters
|
|
1885
|
+
----------
|
|
1886
|
+
contrail : GeoVectorDataset
|
|
1887
|
+
GeoVectorDataset object onto which meterology variables are attached.
|
|
1888
|
+
met : MetDataset
|
|
1889
|
+
MetDataset with meteorology data variables.
|
|
1890
|
+
params : dict[str, Any]
|
|
1891
|
+
Cocip model ``params``.
|
|
1892
|
+
**interp_kwargs : Any
|
|
1893
|
+
Cocip model ``interp_kwargs``.
|
|
1894
|
+
"""
|
|
1895
|
+
# get contrail geometry
|
|
1896
|
+
sin_a = contrail["sin_a"]
|
|
1897
|
+
cos_a = contrail["cos_a"]
|
|
1898
|
+
|
|
1899
|
+
# get standard met parameters for timestep
|
|
1900
|
+
air_pressure = contrail.air_pressure
|
|
1901
|
+
air_temperature = contrail["air_temperature"]
|
|
1902
|
+
u_wind = interpolate_met(met, contrail, "eastward_wind", "u_wind", **interp_kwargs)
|
|
1903
|
+
v_wind = interpolate_met(met, contrail, "northward_wind", "v_wind", **interp_kwargs)
|
|
1904
|
+
interpolate_met(
|
|
1905
|
+
met,
|
|
1906
|
+
contrail,
|
|
1907
|
+
"lagrangian_tendency_of_air_pressure",
|
|
1908
|
+
"vertical_velocity",
|
|
1909
|
+
**interp_kwargs,
|
|
1910
|
+
)
|
|
1911
|
+
interpolate_met(met, contrail, "tau_cirrus", **interp_kwargs)
|
|
1912
|
+
|
|
1913
|
+
# get the pressure level `dz_m` lower than element pressure
|
|
1914
|
+
air_pressure_lower = thermo.pressure_dz(air_temperature, air_pressure, params["dz_m"])
|
|
1915
|
+
|
|
1916
|
+
# get meteorology at contrail waypoints interpolated at the pressure level `air_pressure_lower`
|
|
1917
|
+
level_lower = air_pressure_lower / 100.0 # Pa -> hPa
|
|
1918
|
+
|
|
1919
|
+
# if met is already interpolated, this will automatically skip interpolation
|
|
1920
|
+
air_temperature_lower = interpolate_met(
|
|
1921
|
+
met,
|
|
1922
|
+
contrail,
|
|
1923
|
+
"air_temperature",
|
|
1924
|
+
"air_temperature_lower",
|
|
1925
|
+
level=level_lower,
|
|
1926
|
+
**interp_kwargs,
|
|
1927
|
+
)
|
|
1928
|
+
u_wind_lower = interpolate_met(
|
|
1929
|
+
met,
|
|
1930
|
+
contrail,
|
|
1931
|
+
"eastward_wind",
|
|
1932
|
+
"u_wind_lower",
|
|
1933
|
+
level=level_lower,
|
|
1934
|
+
**interp_kwargs,
|
|
1935
|
+
)
|
|
1936
|
+
v_wind_lower = interpolate_met(
|
|
1937
|
+
met,
|
|
1938
|
+
contrail,
|
|
1939
|
+
"northward_wind",
|
|
1940
|
+
"v_wind_lower",
|
|
1941
|
+
level=level_lower,
|
|
1942
|
+
**interp_kwargs,
|
|
1943
|
+
)
|
|
1944
|
+
|
|
1945
|
+
# Temperature gradient
|
|
1946
|
+
dT_dz = thermo.T_potential_gradient(
|
|
1947
|
+
air_temperature,
|
|
1948
|
+
air_pressure,
|
|
1949
|
+
air_temperature_lower,
|
|
1950
|
+
air_pressure_lower,
|
|
1951
|
+
params["dz_m"],
|
|
1952
|
+
)
|
|
1953
|
+
|
|
1954
|
+
# wind shear
|
|
1955
|
+
ds_dz = wind_shear.wind_shear(u_wind, u_wind_lower, v_wind, v_wind_lower, params["dz_m"])
|
|
1956
|
+
|
|
1957
|
+
# wind shear normal
|
|
1958
|
+
dsn_dz = wind_shear.wind_shear_normal(
|
|
1959
|
+
u_wind_top=u_wind,
|
|
1960
|
+
u_wind_btm=u_wind_lower,
|
|
1961
|
+
v_wind_top=v_wind,
|
|
1962
|
+
v_wind_btm=v_wind_lower,
|
|
1963
|
+
cos_a=cos_a,
|
|
1964
|
+
sin_a=sin_a,
|
|
1965
|
+
dz=params["dz_m"],
|
|
1966
|
+
)
|
|
1967
|
+
|
|
1968
|
+
# store values on contrail model
|
|
1969
|
+
contrail.update(dT_dz=dT_dz)
|
|
1970
|
+
contrail.update(ds_dz=ds_dz)
|
|
1971
|
+
contrail.update(dsn_dz=dsn_dz)
|
|
1972
|
+
|
|
1973
|
+
|
|
1974
|
+
def calc_shortwave_radiation(
|
|
1975
|
+
rad: MetDataset,
|
|
1976
|
+
vector: GeoVectorDataset,
|
|
1977
|
+
**interp_kwargs: Any,
|
|
1978
|
+
) -> None:
|
|
1979
|
+
"""Calculate shortwave radiation variables.
|
|
1980
|
+
|
|
1981
|
+
Calculates theoretical incident (``sdr``) and
|
|
1982
|
+
reflected shortwave radiation (``rsr``) from the radiation data provided.
|
|
1983
|
+
|
|
1984
|
+
Mutates input ``vector`` to include ``"sdr"`` and ``"rsr"``
|
|
1985
|
+
keys with calculated SDR, RSR values, respectively.
|
|
1986
|
+
|
|
1987
|
+
Parameters
|
|
1988
|
+
----------
|
|
1989
|
+
rad : MetDataset
|
|
1990
|
+
Radiation data
|
|
1991
|
+
vector : GeoVectorDataset
|
|
1992
|
+
Flight or GeoVectorDataset instance
|
|
1993
|
+
**interp_kwargs : Any
|
|
1994
|
+
Interpolation keyword arguments
|
|
1995
|
+
|
|
1996
|
+
Raises
|
|
1997
|
+
------
|
|
1998
|
+
ValueError
|
|
1999
|
+
If ``rad`` does not contain ``"toa_net_downward_shortwave_flux"``,
|
|
2000
|
+
``"toa_upward_shortwave_flux"`` or ``"top_net_solar_radiation"`` variable.
|
|
2001
|
+
|
|
2002
|
+
Notes
|
|
2003
|
+
-----
|
|
2004
|
+
In accordance with the original CoCiP Fortran algorithm,
|
|
2005
|
+
the SDR is set to 0 when the cosine of the solar zenith angle is below 0.01.
|
|
2006
|
+
|
|
2007
|
+
See Also
|
|
2008
|
+
--------
|
|
2009
|
+
:func:`geo.solar_direct_radiation`
|
|
2010
|
+
"""
|
|
2011
|
+
if "sdr" in vector and "rsr" in vector:
|
|
2012
|
+
return
|
|
2013
|
+
|
|
2014
|
+
try:
|
|
2015
|
+
sdr = vector["sdr"]
|
|
2016
|
+
except KeyError:
|
|
2017
|
+
# calculate instantaneous theoretical solar direct radiation based on geo position and time
|
|
2018
|
+
longitude = vector["longitude"]
|
|
2019
|
+
latitude = vector["latitude"]
|
|
2020
|
+
time = vector["time"]
|
|
2021
|
+
sdr = geo.solar_direct_radiation(longitude, latitude, time, threshold_cos_sza=0.01)
|
|
2022
|
+
vector["sdr"] = sdr
|
|
2023
|
+
|
|
2024
|
+
# Generic contains net downward shortwave flux at TOA (SDR - RSR) in W/m2
|
|
2025
|
+
generic_key = "toa_net_downward_shortwave_flux"
|
|
2026
|
+
if generic_key in rad:
|
|
2027
|
+
tnsr = interpolate_met(rad, vector, generic_key, **interp_kwargs)
|
|
2028
|
+
vector["rsr"] = np.maximum(sdr - tnsr, 0.0)
|
|
2029
|
+
return
|
|
2030
|
+
|
|
2031
|
+
# GFS contains RSR (toa_upward_shortwave_flux) variable directly
|
|
2032
|
+
gfs_key = "toa_upward_shortwave_flux"
|
|
2033
|
+
if gfs_key in rad:
|
|
2034
|
+
interpolate_met(rad, vector, gfs_key, "rsr", **interp_kwargs)
|
|
2035
|
+
return
|
|
2036
|
+
|
|
2037
|
+
ecmwf_key = "top_net_solar_radiation"
|
|
2038
|
+
if ecmwf_key not in rad:
|
|
2039
|
+
msg = (
|
|
2040
|
+
f"'rad' data must contain either '{generic_key}' (generic), "
|
|
2041
|
+
f"'{gfs_key}' (GFS), or '{ecmwf_key}' (ECMWF) variable."
|
|
2042
|
+
)
|
|
2043
|
+
raise ValueError(msg)
|
|
2044
|
+
|
|
2045
|
+
# ECMWF also contains net downward shortwave flux at TOA, but possibly as an accumulation
|
|
2046
|
+
tnsr = interpolate_met(rad, vector, ecmwf_key, **interp_kwargs)
|
|
2047
|
+
tnsr = _rad_accumulation_to_average_instantaneous(rad, ecmwf_key, tnsr)
|
|
2048
|
+
vector.update({ecmwf_key: tnsr})
|
|
2049
|
+
|
|
2050
|
+
vector["rsr"] = np.maximum(sdr - tnsr, 0.0)
|
|
2051
|
+
|
|
2052
|
+
|
|
2053
|
+
def calc_outgoing_longwave_radiation(
|
|
2054
|
+
rad: MetDataset,
|
|
2055
|
+
vector: GeoVectorDataset,
|
|
2056
|
+
**interp_kwargs: Any,
|
|
2057
|
+
) -> None:
|
|
2058
|
+
"""Calculate outgoing longwave radiation (``olr``) from the radiation data provided.
|
|
2059
|
+
|
|
2060
|
+
Mutates input ``vector`` to include ``"olr"`` key with calculated OLR values.
|
|
2061
|
+
|
|
2062
|
+
Parameters
|
|
2063
|
+
----------
|
|
2064
|
+
rad : MetDataset
|
|
2065
|
+
Radiation data
|
|
2066
|
+
vector : GeoVectorDataset
|
|
2067
|
+
Flight or GeoVectorDataset instance
|
|
2068
|
+
**interp_kwargs : Any
|
|
2069
|
+
Interpolation keyword arguments
|
|
2070
|
+
|
|
2071
|
+
Raises
|
|
2072
|
+
------
|
|
2073
|
+
ValueError
|
|
2074
|
+
If ``rad`` does not contain a ``"toa_outgoing_longwave_flux"``,
|
|
2075
|
+
``"toa_upward_longwave_flux"`` or ``"top_net_thermal_radiation"`` variable.
|
|
2076
|
+
"""
|
|
2077
|
+
|
|
2078
|
+
if "olr" in vector:
|
|
2079
|
+
return
|
|
2080
|
+
|
|
2081
|
+
# Generic contains OLR (toa_outgoing_longwave_flux) directly
|
|
2082
|
+
generic_key = "toa_outgoing_longwave_flux"
|
|
2083
|
+
if generic_key in rad:
|
|
2084
|
+
interpolate_met(rad, vector, generic_key, "olr", **interp_kwargs)
|
|
2085
|
+
return
|
|
2086
|
+
|
|
2087
|
+
# GFS contains OLR (toa_upward_longwave_flux) directly
|
|
2088
|
+
gfs_key = "toa_upward_longwave_flux"
|
|
2089
|
+
if gfs_key in rad:
|
|
2090
|
+
interpolate_met(rad, vector, gfs_key, "olr", **interp_kwargs)
|
|
2091
|
+
return
|
|
2092
|
+
|
|
2093
|
+
# ECMWF contains "top_net_thermal_radiation" which is -1 * OLR
|
|
2094
|
+
ecmwf_key = "top_net_thermal_radiation"
|
|
2095
|
+
if ecmwf_key not in rad:
|
|
2096
|
+
msg = (
|
|
2097
|
+
f"'rad' data must contain either '{generic_key}' (generic), "
|
|
2098
|
+
f"'{gfs_key}' (GFS), or '{ecmwf_key}' (ECMWF) variable."
|
|
2099
|
+
)
|
|
2100
|
+
raise ValueError(msg)
|
|
2101
|
+
|
|
2102
|
+
tntr = interpolate_met(rad, vector, ecmwf_key, **interp_kwargs)
|
|
2103
|
+
tntr = _rad_accumulation_to_average_instantaneous(rad, ecmwf_key, tntr)
|
|
2104
|
+
vector.update({ecmwf_key: tntr})
|
|
2105
|
+
|
|
2106
|
+
vector["olr"] = np.maximum(-tntr, 0.0)
|
|
2107
|
+
|
|
2108
|
+
|
|
2109
|
+
def calc_radiative_properties(contrail: GeoVectorDataset, params: dict[str, Any]) -> None:
|
|
2110
|
+
"""Calculate radiative properties for contrail.
|
|
2111
|
+
|
|
2112
|
+
This function is used by both :class:`Cocip` and :class`CocipGrid`.
|
|
2113
|
+
|
|
2114
|
+
Mutates original `contrail` parameter with additional keys:
|
|
2115
|
+
- "rf_sw"
|
|
2116
|
+
- "rf_lw"
|
|
2117
|
+
- "rf_net"
|
|
2118
|
+
|
|
2119
|
+
Parameters
|
|
2120
|
+
----------
|
|
2121
|
+
contrail : GeoVectorDataset
|
|
2122
|
+
Grid points already interpolated against met and rad data. In particular,
|
|
2123
|
+
the variables
|
|
2124
|
+
- "air_temperature"
|
|
2125
|
+
- "r_ice_vol"
|
|
2126
|
+
- "tau_contrail"
|
|
2127
|
+
- "tau_cirrus"
|
|
2128
|
+
- "olr"
|
|
2129
|
+
- "rsr"
|
|
2130
|
+
- "sdr"
|
|
2131
|
+
|
|
2132
|
+
are required on the parameter `contrail`.
|
|
2133
|
+
params : dict[str, Any]
|
|
2134
|
+
Model parameters
|
|
2135
|
+
"""
|
|
2136
|
+
time = contrail["time"]
|
|
2137
|
+
air_temperature = contrail["air_temperature"]
|
|
2138
|
+
|
|
2139
|
+
if params["radiative_heating_effects"]:
|
|
2140
|
+
air_temperature += contrail["cumul_heat"]
|
|
2141
|
+
|
|
2142
|
+
r_ice_vol = contrail["r_ice_vol"]
|
|
2143
|
+
tau_contrail = contrail["tau_contrail"]
|
|
2144
|
+
tau_cirrus_ = contrail["tau_cirrus"]
|
|
2145
|
+
|
|
2146
|
+
# calculate solar constant
|
|
2147
|
+
theta_rad = geo.orbital_position(time)
|
|
2148
|
+
sd0 = geo.solar_constant(theta_rad)
|
|
2149
|
+
|
|
2150
|
+
# radiation dataset with contrail waypoints at timestep
|
|
2151
|
+
sdr = contrail["sdr"]
|
|
2152
|
+
rsr = contrail["rsr"]
|
|
2153
|
+
olr = contrail["olr"]
|
|
2154
|
+
|
|
2155
|
+
# radiation calculations
|
|
2156
|
+
r_vol_um = r_ice_vol * 1e6
|
|
2157
|
+
habit_weights = radiative_forcing.habit_weights(
|
|
2158
|
+
r_vol_um, params["habit_distributions"], params["radius_threshold_um"]
|
|
2159
|
+
)
|
|
2160
|
+
rf_lw = radiative_forcing.longwave_radiative_forcing(
|
|
2161
|
+
r_vol_um, olr, air_temperature, tau_contrail, tau_cirrus_, habit_weights
|
|
2162
|
+
)
|
|
2163
|
+
rf_sw = radiative_forcing.shortwave_radiative_forcing(
|
|
2164
|
+
r_vol_um, sdr, rsr, sd0, tau_contrail, tau_cirrus_, habit_weights
|
|
2165
|
+
)
|
|
2166
|
+
|
|
2167
|
+
# scale RF by enhancement factors
|
|
2168
|
+
rf_lw_scaled = rf_lw * params["rf_lw_enhancement_factor"]
|
|
2169
|
+
rf_sw_scaled = rf_sw * params["rf_sw_enhancement_factor"]
|
|
2170
|
+
rf_net = radiative_forcing.net_radiative_forcing(rf_lw_scaled, rf_sw_scaled)
|
|
2171
|
+
|
|
2172
|
+
# store values on contrail
|
|
2173
|
+
contrail["rf_sw"] = rf_sw
|
|
2174
|
+
contrail["rf_lw"] = rf_lw
|
|
2175
|
+
contrail["rf_net"] = rf_net
|
|
2176
|
+
|
|
2177
|
+
|
|
2178
|
+
def calc_contrail_properties(
|
|
2179
|
+
contrail: GeoVectorDataset,
|
|
2180
|
+
effective_vertical_resolution: float | npt.NDArray[np.floating],
|
|
2181
|
+
wind_shear_enhancement_exponent: float | npt.NDArray[np.floating],
|
|
2182
|
+
sedimentation_impact_factor: float | npt.NDArray[np.floating],
|
|
2183
|
+
radiative_heating_effects: bool,
|
|
2184
|
+
) -> None:
|
|
2185
|
+
"""Calculate geometric and ice-related properties of contrail.
|
|
2186
|
+
|
|
2187
|
+
This function is used by both :class:`Cocip` and :class`CocipGrid`.
|
|
2188
|
+
|
|
2189
|
+
This function modifies parameter `contrail` in place:
|
|
2190
|
+
- Mutates contrail data variables:
|
|
2191
|
+
- "ds_dz"
|
|
2192
|
+
- "dsn_dz"
|
|
2193
|
+
- Attaches additional variables:
|
|
2194
|
+
- "area_eff"
|
|
2195
|
+
- "plume_mass_per_m"
|
|
2196
|
+
- "r_ice_vol"
|
|
2197
|
+
- "terminal_fall_speed"
|
|
2198
|
+
- "diffuse_h"
|
|
2199
|
+
- "diffuse_v"
|
|
2200
|
+
- "n_ice_per_vol"
|
|
2201
|
+
- "tau_contrail"
|
|
2202
|
+
- "dn_dt_agg"
|
|
2203
|
+
- "dn_dt_turb"
|
|
2204
|
+
|
|
2205
|
+
Parameters
|
|
2206
|
+
----------
|
|
2207
|
+
contrail : GeoVectorDataset
|
|
2208
|
+
Grid points with many precomputed keys.
|
|
2209
|
+
effective_vertical_resolution : float | npt.NDArray[np.floating]
|
|
2210
|
+
Passed into :func:`wind_shear.wind_shear_enhancement_factor`.
|
|
2211
|
+
wind_shear_enhancement_exponent : float | npt.NDArray[np.floating]
|
|
2212
|
+
Passed into :func:`wind_shear.wind_shear_enhancement_factor`.
|
|
2213
|
+
sedimentation_impact_factor: float | npt.NDArray[np.floating]
|
|
2214
|
+
Passed into `contrail_properties.vertical_diffusivity`.
|
|
2215
|
+
radiative_heating_effects: bool
|
|
2216
|
+
Include radiative heating effects on contrail cirrus properties.
|
|
2217
|
+
"""
|
|
2218
|
+
time = contrail["time"]
|
|
2219
|
+
iwc = contrail["iwc"]
|
|
2220
|
+
depth = contrail["depth"]
|
|
2221
|
+
width = contrail["width"]
|
|
2222
|
+
n_ice_per_m = contrail["n_ice_per_m"]
|
|
2223
|
+
t_cirrus_ = contrail["tau_cirrus"]
|
|
2224
|
+
|
|
2225
|
+
# get required meteorology
|
|
2226
|
+
air_temperature = contrail["air_temperature"]
|
|
2227
|
+
air_pressure = contrail.air_pressure
|
|
2228
|
+
rhi = contrail["rhi"]
|
|
2229
|
+
rho_air = contrail["rho_air"]
|
|
2230
|
+
dT_dz = contrail["dT_dz"]
|
|
2231
|
+
ds_dz = contrail["ds_dz"]
|
|
2232
|
+
dsn_dz = contrail["dsn_dz"]
|
|
2233
|
+
sigma_yz = contrail["sigma_yz"]
|
|
2234
|
+
|
|
2235
|
+
if radiative_heating_effects:
|
|
2236
|
+
air_temperature += contrail["cumul_heat"]
|
|
2237
|
+
|
|
2238
|
+
# get required radiation
|
|
2239
|
+
sdr = contrail["sdr"]
|
|
2240
|
+
rsr = contrail["rsr"]
|
|
2241
|
+
olr = contrail["olr"]
|
|
2242
|
+
|
|
2243
|
+
# shear enhancements
|
|
2244
|
+
shear_enhancement = wind_shear.wind_shear_enhancement_factor(
|
|
2245
|
+
contrail_depth=depth,
|
|
2246
|
+
effective_vertical_resolution=effective_vertical_resolution,
|
|
2247
|
+
wind_shear_enhancement_exponent=wind_shear_enhancement_exponent,
|
|
2248
|
+
)
|
|
2249
|
+
ds_dz = ds_dz * shear_enhancement
|
|
2250
|
+
dsn_dz = dsn_dz * shear_enhancement
|
|
2251
|
+
|
|
2252
|
+
# effective area
|
|
2253
|
+
area_eff = contrail_properties.plume_effective_cross_sectional_area(width, depth, sigma_yz)
|
|
2254
|
+
depth_eff = contrail_properties.plume_effective_depth(width, area_eff)
|
|
2255
|
+
|
|
2256
|
+
# ice particles
|
|
2257
|
+
n_ice_per_vol = contrail_properties.ice_particle_number_per_volume_of_plume(
|
|
2258
|
+
n_ice_per_m, area_eff
|
|
2259
|
+
)
|
|
2260
|
+
n_ice_per_kg_air = contrail_properties.ice_particle_number_per_mass_of_air(
|
|
2261
|
+
n_ice_per_vol, rho_air
|
|
2262
|
+
)
|
|
2263
|
+
plume_mass_per_m = contrail_properties.plume_mass_per_distance(area_eff, rho_air)
|
|
2264
|
+
r_ice_vol = contrail_properties.ice_particle_volume_mean_radius(iwc, n_ice_per_kg_air)
|
|
2265
|
+
tau_contrail = contrail_properties.contrail_optical_depth(r_ice_vol, n_ice_per_m, width)
|
|
2266
|
+
|
|
2267
|
+
terminal_fall_speed = contrail_properties.ice_particle_terminal_fall_speed(
|
|
2268
|
+
air_pressure, air_temperature, r_ice_vol
|
|
2269
|
+
)
|
|
2270
|
+
diffuse_h = contrail_properties.horizontal_diffusivity(ds_dz, depth)
|
|
2271
|
+
|
|
2272
|
+
if radiative_heating_effects:
|
|
2273
|
+
# theta_rad has float64 dtype, convert back to float32 if needed
|
|
2274
|
+
theta_rad = geo.orbital_position(time).astype(sdr.dtype, copy=False)
|
|
2275
|
+
sd0 = geo.solar_constant(theta_rad)
|
|
2276
|
+
heat_rate = radiative_heating.heating_rate(
|
|
2277
|
+
air_temperature=air_temperature,
|
|
2278
|
+
rhi=rhi,
|
|
2279
|
+
rho_air=rho_air,
|
|
2280
|
+
r_ice_vol=r_ice_vol,
|
|
2281
|
+
depth_eff=depth_eff,
|
|
2282
|
+
tau_contrail=tau_contrail,
|
|
2283
|
+
tau_cirrus=t_cirrus_,
|
|
2284
|
+
sd0=sd0,
|
|
2285
|
+
sdr=sdr,
|
|
2286
|
+
rsr=rsr,
|
|
2287
|
+
olr=olr,
|
|
2288
|
+
)
|
|
2289
|
+
|
|
2290
|
+
cumul_differential_heat = contrail["cumul_differential_heat"]
|
|
2291
|
+
d_heat_rate = radiative_heating.differential_heating_rate(
|
|
2292
|
+
air_temperature=air_temperature,
|
|
2293
|
+
rhi=rhi,
|
|
2294
|
+
rho_air=rho_air,
|
|
2295
|
+
r_ice_vol=r_ice_vol,
|
|
2296
|
+
depth_eff=depth_eff,
|
|
2297
|
+
tau_contrail=tau_contrail,
|
|
2298
|
+
tau_cirrus=t_cirrus_,
|
|
2299
|
+
sd0=sd0,
|
|
2300
|
+
sdr=sdr,
|
|
2301
|
+
rsr=rsr,
|
|
2302
|
+
olr=olr,
|
|
2303
|
+
)
|
|
2304
|
+
|
|
2305
|
+
eff_heat_rate = radiative_heating.effective_heating_rate(
|
|
2306
|
+
d_heat_rate, cumul_differential_heat, dT_dz, depth
|
|
2307
|
+
)
|
|
2308
|
+
else:
|
|
2309
|
+
eff_heat_rate = None
|
|
2310
|
+
|
|
2311
|
+
diffuse_v = contrail_properties.vertical_diffusivity(
|
|
2312
|
+
air_pressure=air_pressure,
|
|
2313
|
+
air_temperature=air_temperature,
|
|
2314
|
+
dT_dz=dT_dz,
|
|
2315
|
+
depth_eff=depth_eff,
|
|
2316
|
+
terminal_fall_speed=terminal_fall_speed,
|
|
2317
|
+
sedimentation_impact_factor=sedimentation_impact_factor,
|
|
2318
|
+
eff_heat_rate=eff_heat_rate,
|
|
2319
|
+
)
|
|
2320
|
+
|
|
2321
|
+
dn_dt_agg = contrail_properties.particle_losses_aggregation(
|
|
2322
|
+
r_ice_vol, terminal_fall_speed, area_eff
|
|
2323
|
+
)
|
|
2324
|
+
dn_dt_turb = contrail_properties.particle_losses_turbulence(
|
|
2325
|
+
width, depth, depth_eff, diffuse_h, diffuse_v
|
|
2326
|
+
)
|
|
2327
|
+
|
|
2328
|
+
# Set properties to model
|
|
2329
|
+
contrail.update(ds_dz=ds_dz)
|
|
2330
|
+
contrail.update(dsn_dz=dsn_dz)
|
|
2331
|
+
contrail.update(area_eff=area_eff)
|
|
2332
|
+
contrail.update(plume_mass_per_m=plume_mass_per_m)
|
|
2333
|
+
contrail.update(r_ice_vol=r_ice_vol)
|
|
2334
|
+
contrail.update(terminal_fall_speed=terminal_fall_speed)
|
|
2335
|
+
contrail.update(diffuse_h=diffuse_h)
|
|
2336
|
+
contrail.update(diffuse_v=diffuse_v)
|
|
2337
|
+
contrail.update(n_ice_per_vol=n_ice_per_vol)
|
|
2338
|
+
contrail.update(tau_contrail=tau_contrail)
|
|
2339
|
+
contrail.update(dn_dt_agg=dn_dt_agg)
|
|
2340
|
+
contrail.update(dn_dt_turb=dn_dt_turb)
|
|
2341
|
+
if radiative_heating_effects:
|
|
2342
|
+
contrail.update(heat_rate=heat_rate, d_heat_rate=d_heat_rate)
|
|
2343
|
+
|
|
2344
|
+
|
|
2345
|
+
def calc_timestep_contrail_evolution(
|
|
2346
|
+
met: MetDataset,
|
|
2347
|
+
rad: MetDataset,
|
|
2348
|
+
contrail_1: GeoVectorDataset,
|
|
2349
|
+
time_2: np.datetime64,
|
|
2350
|
+
params: dict[str, Any],
|
|
2351
|
+
**interp_kwargs: Any,
|
|
2352
|
+
) -> GeoVectorDataset:
|
|
2353
|
+
"""Calculate the contrail evolution across timestep (t1 -> t2).
|
|
2354
|
+
|
|
2355
|
+
Note the variable suffix "_1" is used to reference the current time
|
|
2356
|
+
and the suffix "_2" is used to refer to the time at the next timestep.
|
|
2357
|
+
|
|
2358
|
+
Parameters
|
|
2359
|
+
----------
|
|
2360
|
+
met : MetDataset
|
|
2361
|
+
Meteorology data
|
|
2362
|
+
rad : MetDataset
|
|
2363
|
+
Radiation data
|
|
2364
|
+
contrail_1 : GeoVectorDataset
|
|
2365
|
+
Contrail waypoints at current timestep (1)
|
|
2366
|
+
time_2 : np.datetime64
|
|
2367
|
+
Time at the end of the evolution step (2)
|
|
2368
|
+
params : dict[str, Any]
|
|
2369
|
+
Model parameters
|
|
2370
|
+
**interp_kwargs : Any
|
|
2371
|
+
Interpolation keyword arguments
|
|
2372
|
+
|
|
2373
|
+
Returns
|
|
2374
|
+
-------
|
|
2375
|
+
GeoVectorDataset
|
|
2376
|
+
The contrail evolved to ``time_2``.
|
|
2377
|
+
"""
|
|
2378
|
+
|
|
2379
|
+
# get lat/lon for current timestep (t1)
|
|
2380
|
+
longitude_1 = contrail_1["longitude"]
|
|
2381
|
+
latitude_1 = contrail_1["latitude"]
|
|
2382
|
+
level_1 = contrail_1.level
|
|
2383
|
+
time_1 = contrail_1["time"]
|
|
2384
|
+
|
|
2385
|
+
# get contrail_1 geometry
|
|
2386
|
+
segment_length_1 = contrail_1["segment_length"]
|
|
2387
|
+
width_1 = contrail_1["width"]
|
|
2388
|
+
depth_1 = contrail_1["depth"]
|
|
2389
|
+
|
|
2390
|
+
# get required met values for evolution calculations
|
|
2391
|
+
q_sat_1 = contrail_1["q_sat"]
|
|
2392
|
+
rho_air_1 = contrail_1["rho_air"]
|
|
2393
|
+
u_wind_1 = contrail_1["u_wind"]
|
|
2394
|
+
v_wind_1 = contrail_1["v_wind"]
|
|
2395
|
+
|
|
2396
|
+
specific_humidity_1 = contrail_1["specific_humidity"]
|
|
2397
|
+
vertical_velocity_1 = contrail_1["vertical_velocity"]
|
|
2398
|
+
iwc_1 = contrail_1["iwc"]
|
|
2399
|
+
|
|
2400
|
+
# get required contrail_1 properties
|
|
2401
|
+
sigma_yz_1 = contrail_1["sigma_yz"]
|
|
2402
|
+
dsn_dz_1 = contrail_1["dsn_dz"]
|
|
2403
|
+
terminal_fall_speed_1 = contrail_1["terminal_fall_speed"]
|
|
2404
|
+
diffuse_h_1 = contrail_1["diffuse_h"]
|
|
2405
|
+
diffuse_v_1 = contrail_1["diffuse_v"]
|
|
2406
|
+
plume_mass_per_m_1 = contrail_1["plume_mass_per_m"]
|
|
2407
|
+
dn_dt_agg_1 = contrail_1["dn_dt_agg"]
|
|
2408
|
+
dn_dt_turb_1 = contrail_1["dn_dt_turb"]
|
|
2409
|
+
n_ice_per_m_1 = contrail_1["n_ice_per_m"]
|
|
2410
|
+
|
|
2411
|
+
# get contrail_1 radiative properties
|
|
2412
|
+
rf_net_1 = contrail_1["rf_net"]
|
|
2413
|
+
|
|
2414
|
+
# initialize new timestep with evolved coordinates
|
|
2415
|
+
# assume waypoints are the same to start
|
|
2416
|
+
waypoint_2 = contrail_1["waypoint"]
|
|
2417
|
+
formation_time_2 = contrail_1["formation_time"]
|
|
2418
|
+
time_2_array = np.full_like(time_1, time_2)
|
|
2419
|
+
dt = time_2_array - time_1
|
|
2420
|
+
|
|
2421
|
+
# get new contrail location & segment properties after t_step
|
|
2422
|
+
longitude_2, latitude_2 = geo.advect_horizontal(longitude_1, latitude_1, u_wind_1, v_wind_1, dt)
|
|
2423
|
+
level_2 = geo.advect_level(level_1, vertical_velocity_1, rho_air_1, terminal_fall_speed_1, dt)
|
|
2424
|
+
altitude_2 = units.pl_to_m(level_2)
|
|
2425
|
+
|
|
2426
|
+
contrail_2 = GeoVectorDataset._from_fastpath(
|
|
2427
|
+
{
|
|
2428
|
+
"waypoint": waypoint_2,
|
|
2429
|
+
"flight_id": contrail_1["flight_id"],
|
|
2430
|
+
"formation_time": formation_time_2,
|
|
2431
|
+
"time": time_2_array,
|
|
2432
|
+
"age": time_2_array - formation_time_2,
|
|
2433
|
+
"longitude": longitude_2,
|
|
2434
|
+
"latitude": latitude_2,
|
|
2435
|
+
"altitude": altitude_2,
|
|
2436
|
+
"level": level_2,
|
|
2437
|
+
},
|
|
2438
|
+
)
|
|
2439
|
+
intersection = contrail_2.coords_intersect_met(met)
|
|
2440
|
+
if not np.any(intersection):
|
|
2441
|
+
warnings.warn(
|
|
2442
|
+
f"At time {time_2}, the contrail has no intersection with the met data. "
|
|
2443
|
+
"This is likely due to the contrail being advected outside the met domain."
|
|
2444
|
+
)
|
|
2445
|
+
|
|
2446
|
+
# Update cumulative radiative heating energy absorbed by the contrail
|
|
2447
|
+
# This will always be zero if radiative_heating_effects is not activated in cocip_params
|
|
2448
|
+
if params["radiative_heating_effects"]:
|
|
2449
|
+
dt_sec = dt / np.timedelta64(1, "s")
|
|
2450
|
+
heat_rate_1 = contrail_1["heat_rate"]
|
|
2451
|
+
cumul_heat = contrail_1["cumul_heat"]
|
|
2452
|
+
cumul_heat += heat_rate_1 * dt_sec
|
|
2453
|
+
cumul_heat.clip(max=1.5, out=cumul_heat) # Constrain additional heat to 1.5 K as precaution
|
|
2454
|
+
contrail_2["cumul_heat"] = cumul_heat
|
|
2455
|
+
|
|
2456
|
+
d_heat_rate_1 = contrail_1["d_heat_rate"]
|
|
2457
|
+
cumul_differential_heat = contrail_1["cumul_differential_heat"]
|
|
2458
|
+
cumul_differential_heat += -d_heat_rate_1 * dt_sec
|
|
2459
|
+
contrail_2["cumul_differential_heat"] = cumul_differential_heat
|
|
2460
|
+
|
|
2461
|
+
# Attach a few more artifacts for disabled filtering
|
|
2462
|
+
if not params["filter_sac"]:
|
|
2463
|
+
contrail_2["sac"] = contrail_1["sac"]
|
|
2464
|
+
if not params["filter_initially_persistent"]:
|
|
2465
|
+
contrail_2["initially_persistent"] = contrail_1["initially_persistent"]
|
|
2466
|
+
if params["persistent_buffer"] is not None:
|
|
2467
|
+
contrail_2["end_of_life"] = contrail_1["end_of_life"]
|
|
2468
|
+
|
|
2469
|
+
# calculate initial contrail properties for the next timestep
|
|
2470
|
+
calc_continuous(contrail_2)
|
|
2471
|
+
calc_timestep_geometry(contrail_2)
|
|
2472
|
+
|
|
2473
|
+
# get next segment lengths
|
|
2474
|
+
segment_length_2 = contrail_2["segment_length"]
|
|
2475
|
+
|
|
2476
|
+
# new contrail dimensions
|
|
2477
|
+
seg_ratio_2 = contrail_properties.segment_length_ratio(segment_length_1, segment_length_2)
|
|
2478
|
+
sigma_yy_2, sigma_zz_2, sigma_yz_2 = contrail_properties.plume_temporal_evolution(
|
|
2479
|
+
width_1,
|
|
2480
|
+
depth_1,
|
|
2481
|
+
sigma_yz_1,
|
|
2482
|
+
dsn_dz_1,
|
|
2483
|
+
diffuse_h_1,
|
|
2484
|
+
diffuse_v_1,
|
|
2485
|
+
seg_ratio_2,
|
|
2486
|
+
dt,
|
|
2487
|
+
max_depth=params["max_depth"],
|
|
2488
|
+
)
|
|
2489
|
+
width_2, depth_2 = contrail_properties.new_contrail_dimensions(sigma_yy_2, sigma_zz_2)
|
|
2490
|
+
|
|
2491
|
+
# store data back in model to support next time step calculations
|
|
2492
|
+
contrail_2["width"] = width_2
|
|
2493
|
+
contrail_2["depth"] = depth_2
|
|
2494
|
+
contrail_2["sigma_yz"] = sigma_yz_2
|
|
2495
|
+
|
|
2496
|
+
# new contrail meteorology parameters
|
|
2497
|
+
air_temperature_2 = interpolate_met(met, contrail_2, "air_temperature", **interp_kwargs)
|
|
2498
|
+
|
|
2499
|
+
if params["radiative_heating_effects"]:
|
|
2500
|
+
air_temperature_2 += contrail_2["cumul_heat"]
|
|
2501
|
+
|
|
2502
|
+
interpolate_met(met, contrail_2, "specific_humidity", **interp_kwargs)
|
|
2503
|
+
|
|
2504
|
+
humidity_scaling = params["humidity_scaling"]
|
|
2505
|
+
if humidity_scaling is not None:
|
|
2506
|
+
humidity_scaling.eval(contrail_2, copy_source=False)
|
|
2507
|
+
else:
|
|
2508
|
+
contrail_2["air_pressure"] = contrail_2.air_pressure
|
|
2509
|
+
contrail_2["rhi"] = thermo.rhi(
|
|
2510
|
+
contrail_2["specific_humidity"], air_temperature_2, contrail_2["air_pressure"]
|
|
2511
|
+
)
|
|
2512
|
+
|
|
2513
|
+
air_pressure_2 = contrail_2["air_pressure"]
|
|
2514
|
+
specific_humidity_2 = contrail_2["specific_humidity"]
|
|
2515
|
+
rho_air_2 = thermo.rho_d(air_temperature_2, air_pressure_2)
|
|
2516
|
+
q_sat_2 = thermo.q_sat_ice(air_temperature_2, air_pressure_2)
|
|
2517
|
+
|
|
2518
|
+
# store data back in model to support next ``calc_timestep_meteorology``
|
|
2519
|
+
contrail_2["rho_air"] = rho_air_2
|
|
2520
|
+
contrail_2["q_sat"] = q_sat_2
|
|
2521
|
+
|
|
2522
|
+
# New contrail ice particle mass and number
|
|
2523
|
+
area_eff_2 = contrail_properties.new_effective_area_from_sigma(
|
|
2524
|
+
sigma_yy_2, sigma_zz_2, sigma_yz_2
|
|
2525
|
+
)
|
|
2526
|
+
plume_mass_per_m_2 = contrail_properties.plume_mass_per_distance(area_eff_2, rho_air_2)
|
|
2527
|
+
iwc_2 = contrail_properties.new_ice_water_content(
|
|
2528
|
+
iwc_1,
|
|
2529
|
+
specific_humidity_1,
|
|
2530
|
+
specific_humidity_2,
|
|
2531
|
+
q_sat_1,
|
|
2532
|
+
q_sat_2,
|
|
2533
|
+
plume_mass_per_m_1,
|
|
2534
|
+
plume_mass_per_m_2,
|
|
2535
|
+
)
|
|
2536
|
+
n_ice_per_m_2 = contrail_properties.new_ice_particle_number(
|
|
2537
|
+
n_ice_per_m_1, dn_dt_agg_1, dn_dt_turb_1, seg_ratio_2, dt
|
|
2538
|
+
)
|
|
2539
|
+
|
|
2540
|
+
contrail_2["n_ice_per_m"] = n_ice_per_m_2
|
|
2541
|
+
contrail_2["iwc"] = iwc_2
|
|
2542
|
+
|
|
2543
|
+
# calculate next timestep meteorology, contrail, and radiative properties
|
|
2544
|
+
calc_timestep_meteorology(contrail_2, met, params, **interp_kwargs)
|
|
2545
|
+
|
|
2546
|
+
# Intersect with rad dataset
|
|
2547
|
+
calc_shortwave_radiation(rad, contrail_2, **interp_kwargs)
|
|
2548
|
+
calc_outgoing_longwave_radiation(rad, contrail_2, **interp_kwargs)
|
|
2549
|
+
calc_contrail_properties(
|
|
2550
|
+
contrail_2,
|
|
2551
|
+
params["effective_vertical_resolution"],
|
|
2552
|
+
params["wind_shear_enhancement_exponent"],
|
|
2553
|
+
params["sedimentation_impact_factor"],
|
|
2554
|
+
params["radiative_heating_effects"],
|
|
2555
|
+
)
|
|
2556
|
+
calc_radiative_properties(contrail_2, params)
|
|
2557
|
+
|
|
2558
|
+
# get properties to measure persistence
|
|
2559
|
+
latitude_2 = contrail_2["latitude"]
|
|
2560
|
+
altitude_2 = contrail_2.altitude
|
|
2561
|
+
age_2 = contrail_2["age"]
|
|
2562
|
+
tau_contrail_2 = contrail_2["tau_contrail"]
|
|
2563
|
+
n_ice_per_vol_2 = contrail_2["n_ice_per_vol"]
|
|
2564
|
+
|
|
2565
|
+
# calculate next persistent
|
|
2566
|
+
persistent_2 = contrail_2["persistent"] = contrail_properties.contrail_persistent(
|
|
2567
|
+
latitude=latitude_2,
|
|
2568
|
+
altitude=altitude_2,
|
|
2569
|
+
segment_length=segment_length_2,
|
|
2570
|
+
age=age_2,
|
|
2571
|
+
tau_contrail=tau_contrail_2,
|
|
2572
|
+
n_ice_per_m3=n_ice_per_vol_2,
|
|
2573
|
+
params=params,
|
|
2574
|
+
)
|
|
2575
|
+
|
|
2576
|
+
# Get energy forcing by looking forward to the next time step radiative forcing
|
|
2577
|
+
rf_net_2 = contrail_2["rf_net"]
|
|
2578
|
+
energy_forcing_2 = contrail_properties.energy_forcing(
|
|
2579
|
+
rf_net_t1=rf_net_1,
|
|
2580
|
+
rf_net_t2=rf_net_2,
|
|
2581
|
+
width_t1=width_1,
|
|
2582
|
+
width_t2=width_2,
|
|
2583
|
+
seg_length_t2=segment_length_2,
|
|
2584
|
+
dt=dt,
|
|
2585
|
+
)
|
|
2586
|
+
|
|
2587
|
+
# NOTE: Because of our geometry-continuity conventions, any waypoint without
|
|
2588
|
+
# continuity automatically has 0 EF. This is because calc_timestep_geometry
|
|
2589
|
+
# sets the segment_length entries to 0, thereby eliminating EF.
|
|
2590
|
+
# If we change conventions in calc_timestep_geometry, we may want to
|
|
2591
|
+
# explicitly set the discontinuous entries here to 0. We do this for age
|
|
2592
|
+
# here as well. It's somewhat of a hack, but it ensures nonzero contrail_age
|
|
2593
|
+
# is consistent with nonzero EF.
|
|
2594
|
+
contrail_2["ef"] = energy_forcing_2
|
|
2595
|
+
contrail_2["age"][energy_forcing_2 == 0.0] = np.timedelta64(0, "ns")
|
|
2596
|
+
|
|
2597
|
+
if params["compute_atr20"]:
|
|
2598
|
+
contrail_2["global_yearly_mean_rf"] = (
|
|
2599
|
+
contrail_2["ef"] / constants.surface_area_earth / constants.seconds_per_year
|
|
2600
|
+
)
|
|
2601
|
+
contrail_2["atr20"] = (
|
|
2602
|
+
params["global_rf_to_atr20_factor"] * contrail_2["global_yearly_mean_rf"]
|
|
2603
|
+
)
|
|
2604
|
+
|
|
2605
|
+
# filter contrail_2 by persistent waypoints, if any continuous segments are left
|
|
2606
|
+
logger.debug(
|
|
2607
|
+
"Fraction of waypoints surviving: %s / %s",
|
|
2608
|
+
persistent_2.sum(),
|
|
2609
|
+
persistent_2.size,
|
|
2610
|
+
)
|
|
2611
|
+
|
|
2612
|
+
if (buff := params["persistent_buffer"]) is not None:
|
|
2613
|
+
# Here mask gets waypoints that are just now losing persistence
|
|
2614
|
+
mask = (~persistent_2) & np.isnat(contrail_2["end_of_life"])
|
|
2615
|
+
contrail_2["end_of_life"][mask] = time_2
|
|
2616
|
+
|
|
2617
|
+
# Keep waypoints that are still persistent, which is determined by filt2
|
|
2618
|
+
# And waypoints within the persistent buffer, which is determined by filt1
|
|
2619
|
+
# So we only drop waypoints that are outside of the persistent buffer
|
|
2620
|
+
filt1 = contrail_2["time"] - contrail_2["end_of_life"] < buff
|
|
2621
|
+
filt2 = np.isnat(contrail_2["end_of_life"])
|
|
2622
|
+
filt = filt1 | filt2
|
|
2623
|
+
logger.debug(
|
|
2624
|
+
"Fraction of waypoints surviving with buffer %s: %s / %s",
|
|
2625
|
+
buff,
|
|
2626
|
+
filt.sum(),
|
|
2627
|
+
filt.size,
|
|
2628
|
+
)
|
|
2629
|
+
return contrail_2.filter(filt)
|
|
2630
|
+
|
|
2631
|
+
# filter persistent contrails
|
|
2632
|
+
final_contrail = contrail_2.filter(persistent_2)
|
|
2633
|
+
|
|
2634
|
+
# recalculate continuous contrail segments
|
|
2635
|
+
# and set EF, age for any newly discontinuous segments to 0
|
|
2636
|
+
if final_contrail:
|
|
2637
|
+
calc_continuous(final_contrail)
|
|
2638
|
+
continuous = final_contrail["continuous"]
|
|
2639
|
+
final_contrail["ef"][~continuous] = 0.0
|
|
2640
|
+
final_contrail["age"][~continuous] = np.timedelta64(0, "ns")
|
|
2641
|
+
if params["compute_atr20"]:
|
|
2642
|
+
final_contrail["global_yearly_mean_rf"][~continuous] = 0.0
|
|
2643
|
+
final_contrail["atr20"][~continuous] = 0.0
|
|
2644
|
+
return final_contrail
|
|
2645
|
+
|
|
2646
|
+
|
|
2647
|
+
def _rad_accumulation_to_average_instantaneous(
|
|
2648
|
+
rad: MetDataset,
|
|
2649
|
+
name: str,
|
|
2650
|
+
arr: npt.NDArray[np.floating],
|
|
2651
|
+
) -> npt.NDArray[np.floating]:
|
|
2652
|
+
"""Convert from radiation accumulation to average instantaneous values.
|
|
2653
|
+
|
|
2654
|
+
.. versionadded:: 0.48.0
|
|
2655
|
+
|
|
2656
|
+
Parameters
|
|
2657
|
+
----------
|
|
2658
|
+
rad : MetDataset
|
|
2659
|
+
Radiation data
|
|
2660
|
+
name : str
|
|
2661
|
+
Variable name
|
|
2662
|
+
arr : npt.NDArray[np.floating]
|
|
2663
|
+
Array of values already interpolated from ``rad``
|
|
2664
|
+
|
|
2665
|
+
Returns
|
|
2666
|
+
-------
|
|
2667
|
+
npt.NDArray[np.floating]
|
|
2668
|
+
Array of values converted from accumulation to average instantaneous values
|
|
2669
|
+
|
|
2670
|
+
Raises
|
|
2671
|
+
------
|
|
2672
|
+
KeyError
|
|
2673
|
+
If units are not provided on ``rad``.
|
|
2674
|
+
ValueError
|
|
2675
|
+
If unknown units are provided on ``rad``.
|
|
2676
|
+
"""
|
|
2677
|
+
mda = rad[name]
|
|
2678
|
+
try:
|
|
2679
|
+
unit = mda.attrs["units"]
|
|
2680
|
+
except KeyError as e:
|
|
2681
|
+
msg = (
|
|
2682
|
+
f"Radiation data contains '{name}' variable "
|
|
2683
|
+
"but units are not specified. Provide units in the "
|
|
2684
|
+
f"rad['{name}'].attrs passed into Cocip."
|
|
2685
|
+
)
|
|
2686
|
+
raise KeyError(msg) from e
|
|
2687
|
+
|
|
2688
|
+
# The unit is already instantaneous
|
|
2689
|
+
if unit == "W m**-2":
|
|
2690
|
+
return arr
|
|
2691
|
+
|
|
2692
|
+
if unit != "J m**-2":
|
|
2693
|
+
msg = f"Unexpected units '{unit}' for '{name}' variable. Expected 'J m**-2' or 'W m**-2'."
|
|
2694
|
+
raise ValueError(msg)
|
|
2695
|
+
|
|
2696
|
+
# Convert from J m**-2 to W m**-2
|
|
2697
|
+
if rad.dataset_attr == "ERA5" and rad.product_attr == "ensemble":
|
|
2698
|
+
n_seconds = 3.0 * 3600.0 # 3 hour interval
|
|
2699
|
+
else:
|
|
2700
|
+
n_seconds = 3600.0 # 1 hour interval
|
|
2701
|
+
|
|
2702
|
+
return arr / n_seconds
|
|
2703
|
+
|
|
2704
|
+
|
|
2705
|
+
def _emissions_variables() -> tuple[str, ...]:
|
|
2706
|
+
"""Return variables required for emissions calculation."""
|
|
2707
|
+
return (
|
|
2708
|
+
"engine_efficiency",
|
|
2709
|
+
"fuel_flow",
|
|
2710
|
+
"aircraft_mass",
|
|
2711
|
+
"nvpm_ei_n",
|
|
2712
|
+
"wingspan",
|
|
2713
|
+
)
|
|
2714
|
+
|
|
2715
|
+
|
|
2716
|
+
def _contrail_contrail_overlapping(
|
|
2717
|
+
contrail: GeoVectorDataset, params: dict[str, Any]
|
|
2718
|
+
) -> GeoVectorDataset:
|
|
2719
|
+
"""Mutate ``contrail`` to account for contrail-contrail overlapping effects."""
|
|
2720
|
+
|
|
2721
|
+
if not contrail:
|
|
2722
|
+
return contrail
|
|
2723
|
+
|
|
2724
|
+
contrail = radiative_forcing.contrail_contrail_overlap_radiative_effects(
|
|
2725
|
+
contrail,
|
|
2726
|
+
habit_distributions=params["habit_distributions"],
|
|
2727
|
+
radius_threshold_um=params["radius_threshold_um"],
|
|
2728
|
+
min_altitude_m=params["min_altitude_m"],
|
|
2729
|
+
max_altitude_m=params["max_altitude_m"],
|
|
2730
|
+
dz_overlap_m=params["dz_overlap_m"],
|
|
2731
|
+
)
|
|
2732
|
+
|
|
2733
|
+
contrail.update(
|
|
2734
|
+
rsr=contrail.data.pop("rsr_overlap"),
|
|
2735
|
+
olr=contrail.data.pop("olr_overlap"),
|
|
2736
|
+
tau_cirrus=contrail.data.pop("tau_cirrus_overlap"),
|
|
2737
|
+
rf_sw=contrail.data.pop("rf_sw_overlap"),
|
|
2738
|
+
rf_lw=contrail.data.pop("rf_lw_overlap"),
|
|
2739
|
+
rf_net=contrail.data.pop("rf_net_overlap"),
|
|
2740
|
+
)
|
|
2741
|
+
|
|
2742
|
+
return contrail
|