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,2270 @@
|
|
|
1
|
+
"""CoCiP output formats.
|
|
2
|
+
|
|
3
|
+
This module includes functions to produce additional output formats, including the:
|
|
4
|
+
(1) Flight waypoint outputs.
|
|
5
|
+
See :func:`flight_waypoint_summary_statistics`.
|
|
6
|
+
(2) Contrail flight summary outputs.
|
|
7
|
+
See :func:`contrail_flight_summary_statistics`.
|
|
8
|
+
(3) Gridded outputs.
|
|
9
|
+
See :func:`longitude_latitude_grid`.
|
|
10
|
+
(4) Time-slice statistics.
|
|
11
|
+
See :func:`time_slice_statistics`.
|
|
12
|
+
(5) Aggregate contrail segment optical depth/RF to a high-resolution longitude-latitude grid.
|
|
13
|
+
See :func:`contrails_to_hi_res_grid`.
|
|
14
|
+
(6) Increase spatial resolution of natural cirrus properties, required to estimate the
|
|
15
|
+
high-resolution contrail cirrus coverage for (5).
|
|
16
|
+
See :func:`natural_cirrus_properties_to_hi_res_grid`.
|
|
17
|
+
(7) Comparing simulated contrails from CoCiP with GOES satellite imagery.
|
|
18
|
+
See :func:`compare_cocip_with_goes`.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import pathlib
|
|
24
|
+
import warnings
|
|
25
|
+
from collections.abc import Hashable
|
|
26
|
+
|
|
27
|
+
import numpy as np
|
|
28
|
+
import numpy.typing as npt
|
|
29
|
+
import pandas as pd
|
|
30
|
+
import xarray as xr
|
|
31
|
+
|
|
32
|
+
from pycontrails.core.met import MetDataArray, MetDataset
|
|
33
|
+
from pycontrails.core.vector import GeoVectorDataset, vector_to_lon_lat_grid
|
|
34
|
+
from pycontrails.models.cocip.contrail_properties import contrail_edges, plume_mass_per_distance
|
|
35
|
+
from pycontrails.models.cocip.radiative_forcing import albedo
|
|
36
|
+
from pycontrails.models.humidity_scaling import HumidityScaling
|
|
37
|
+
from pycontrails.models.tau_cirrus import tau_cirrus
|
|
38
|
+
from pycontrails.physics import geo, thermo, units
|
|
39
|
+
from pycontrails.utils import dependencies
|
|
40
|
+
|
|
41
|
+
# -----------------------
|
|
42
|
+
# Flight waypoint outputs
|
|
43
|
+
# -----------------------
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def flight_waypoint_summary_statistics(
|
|
47
|
+
flight_waypoints: GeoVectorDataset | pd.DataFrame,
|
|
48
|
+
contrails: GeoVectorDataset | pd.DataFrame,
|
|
49
|
+
) -> GeoVectorDataset:
|
|
50
|
+
"""
|
|
51
|
+
Calculate the contrail summary statistics at each flight waypoint.
|
|
52
|
+
|
|
53
|
+
Parameters
|
|
54
|
+
----------
|
|
55
|
+
flight_waypoints : GeoVectorDataset | pd.DataFrame
|
|
56
|
+
Flight waypoints that were used in :meth:`Cocip.eval` to produce ``contrails``.
|
|
57
|
+
contrails : GeoVectorDataset | pd.DataFrame
|
|
58
|
+
Contrail evolution outputs from CoCiP, :attr:`Cocip.contrail`
|
|
59
|
+
|
|
60
|
+
Returns
|
|
61
|
+
-------
|
|
62
|
+
GeoVectorDataset
|
|
63
|
+
Contrail summary statistics attached to each flight waypoint.
|
|
64
|
+
|
|
65
|
+
Notes
|
|
66
|
+
-----
|
|
67
|
+
Outputs and units:
|
|
68
|
+
- ``mean_contrail_altitude``, [:math:`m`]
|
|
69
|
+
- ``mean_rhi``, [dimensionless]
|
|
70
|
+
- ``mean_n_ice_per_m``, [:math:`m^{-1}`]
|
|
71
|
+
- ``mean_r_ice_vol``, [:math:`m`]
|
|
72
|
+
- ``mean_width``, [:math:`m`]
|
|
73
|
+
- ``mean_depth``, [:math:`m`]
|
|
74
|
+
- ``mean_tau_contrail``, [dimensionless]
|
|
75
|
+
- ``mean_tau_cirrus``, [dimensionless]
|
|
76
|
+
- ``max_age``, [:math:`h`]
|
|
77
|
+
- ``mean_rf_sw``, [:math:`W m^{-2}`]
|
|
78
|
+
- ``mean_rf_lw``, [:math:`W m^{-2}`]
|
|
79
|
+
- ``mean_rf_net``, [:math:`W m^{-2}`]
|
|
80
|
+
- ``ef``, [:math:`J`]
|
|
81
|
+
- ``mean_olr``, [:math:`W m^{-2}`]
|
|
82
|
+
- ``mean_sdr``, [:math:`W m^{-2}`]
|
|
83
|
+
- ``mean_rsr``, [:math:`W m^{-2}`]
|
|
84
|
+
"""
|
|
85
|
+
# Aggregation map
|
|
86
|
+
agg_map = {
|
|
87
|
+
# Location, ambient meteorology and properties
|
|
88
|
+
"altitude": "mean",
|
|
89
|
+
"rhi": ["mean", "std"],
|
|
90
|
+
"n_ice_per_m": ["mean", "std"],
|
|
91
|
+
"r_ice_vol": "mean",
|
|
92
|
+
"width": "mean",
|
|
93
|
+
"depth": "mean",
|
|
94
|
+
"tau_contrail": "mean",
|
|
95
|
+
"tau_cirrus": "mean",
|
|
96
|
+
"age": "max",
|
|
97
|
+
# Radiative properties
|
|
98
|
+
"rf_sw": "mean",
|
|
99
|
+
"rf_lw": "mean",
|
|
100
|
+
"rf_net": "mean",
|
|
101
|
+
"olr": "mean",
|
|
102
|
+
"sdr": "mean",
|
|
103
|
+
"rsr": "mean",
|
|
104
|
+
}
|
|
105
|
+
if "ef" not in flight_waypoints:
|
|
106
|
+
agg_map["ef"] = "sum"
|
|
107
|
+
|
|
108
|
+
# Check and pre-process `flights`
|
|
109
|
+
if isinstance(flight_waypoints, GeoVectorDataset):
|
|
110
|
+
flight_waypoints.ensure_vars(["flight_id", "waypoint"])
|
|
111
|
+
flight_waypoints = flight_waypoints.dataframe
|
|
112
|
+
|
|
113
|
+
flight_waypoints = flight_waypoints.set_index(["flight_id", "waypoint"])
|
|
114
|
+
|
|
115
|
+
# Check and pre-process `contrails`
|
|
116
|
+
if isinstance(contrails, GeoVectorDataset):
|
|
117
|
+
contrail_vars = ["flight_id", "waypoint", "formation_time", *agg_map]
|
|
118
|
+
contrail_vars.remove("age")
|
|
119
|
+
contrails.ensure_vars(contrail_vars)
|
|
120
|
+
contrails = contrails.dataframe
|
|
121
|
+
|
|
122
|
+
contrails["age"] = (contrails["time"] - contrails["formation_time"]) / np.timedelta64(1, "h")
|
|
123
|
+
|
|
124
|
+
# Calculate contrail statistics at each flight waypoint
|
|
125
|
+
contrails = contrails.groupby(["flight_id", "waypoint"]).agg(agg_map)
|
|
126
|
+
contrails.columns = (
|
|
127
|
+
contrails.columns.get_level_values(1) + "_" + contrails.columns.get_level_values(0)
|
|
128
|
+
)
|
|
129
|
+
rename_cols = {"mean_altitude": "mean_contrail_altitude", "sum_ef": "ef"}
|
|
130
|
+
contrails = contrails.rename(columns=rename_cols)
|
|
131
|
+
|
|
132
|
+
# Concatenate to flight-waypoint outputs
|
|
133
|
+
out = flight_waypoints.join(contrails, how="left")
|
|
134
|
+
out = out.reset_index()
|
|
135
|
+
return GeoVectorDataset(out)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
# -------------------------------
|
|
139
|
+
# Contrail flight summary outputs
|
|
140
|
+
# -------------------------------
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def contrail_flight_summary_statistics(flight_waypoints: GeoVectorDataset) -> pd.DataFrame:
|
|
144
|
+
"""
|
|
145
|
+
Calculate contrail summary statistics for each flight.
|
|
146
|
+
|
|
147
|
+
Parameters
|
|
148
|
+
----------
|
|
149
|
+
flight_waypoints : GeoVectorDataset
|
|
150
|
+
Flight waypoint outputs with contrail summary statistics attached.
|
|
151
|
+
See :func:`flight_waypoint_summary_statistics`.
|
|
152
|
+
|
|
153
|
+
Returns
|
|
154
|
+
-------
|
|
155
|
+
pd.DataFrame
|
|
156
|
+
Contrail summary statistics for each flight
|
|
157
|
+
|
|
158
|
+
Notes
|
|
159
|
+
-----
|
|
160
|
+
Outputs and units:
|
|
161
|
+
- ``total_flight_distance_flown``, [:math:`m`]
|
|
162
|
+
- ``total_contrails_formed``, [:math:`m`]
|
|
163
|
+
- ``total_persistent_contrails_formed``, [:math:`m`]
|
|
164
|
+
- ``mean_lifetime_contrail_altitude``, [:math:`m`]
|
|
165
|
+
- ``mean_lifetime_rhi``, [dimensionless]
|
|
166
|
+
- ``mean_lifetime_n_ice_per_m``, [:math:`m^{-1}`]
|
|
167
|
+
- ``mean_lifetime_r_ice_vol``, [:math:`m`]
|
|
168
|
+
- ``mean_lifetime_contrail_width``, [:math:`m`]
|
|
169
|
+
- ``mean_lifetime_contrail_depth``, [:math:`m`]
|
|
170
|
+
- ``mean_lifetime_tau_contrail``, [dimensionless]
|
|
171
|
+
- ``mean_lifetime_tau_cirrus``, [dimensionless]
|
|
172
|
+
- ``mean_contrail_lifetime``, [:math:`h`]
|
|
173
|
+
- ``max_contrail_lifetime``, [:math:`h`]
|
|
174
|
+
- ``mean_lifetime_rf_sw``, [:math:`W m^{-2}`]
|
|
175
|
+
- ``mean_lifetime_rf_lw``, [:math:`W m^{-2}`]
|
|
176
|
+
- ``mean_lifetime_rf_net``, [:math:`W m^{-2}`]
|
|
177
|
+
- ``total_energy_forcing``, [:math:`J`]
|
|
178
|
+
- ``mean_lifetime_olr``, [:math:`W m^{-2}`]
|
|
179
|
+
- ``mean_lifetime_sdr``, [:math:`W m^{-2}`]
|
|
180
|
+
- ``mean_lifetime_rsr``, [:math:`W m^{-2}`]
|
|
181
|
+
"""
|
|
182
|
+
# Aggregation map
|
|
183
|
+
agg_map = {
|
|
184
|
+
# Contrail properties and ambient meteorology
|
|
185
|
+
"segment_length": "sum",
|
|
186
|
+
"contrail_length": "sum",
|
|
187
|
+
"persistent_contrail_length": "sum",
|
|
188
|
+
"mean_contrail_altitude": "mean",
|
|
189
|
+
"mean_rhi": "mean",
|
|
190
|
+
"mean_n_ice_per_m": "mean",
|
|
191
|
+
"mean_r_ice_vol": "mean",
|
|
192
|
+
"mean_width": "mean",
|
|
193
|
+
"mean_depth": "mean",
|
|
194
|
+
"mean_tau_contrail": "mean",
|
|
195
|
+
"mean_tau_cirrus": "mean",
|
|
196
|
+
"max_age": ["mean", "max"],
|
|
197
|
+
# Radiative properties
|
|
198
|
+
"mean_rf_sw": "mean",
|
|
199
|
+
"mean_rf_lw": "mean",
|
|
200
|
+
"mean_rf_net": "mean",
|
|
201
|
+
"ef": "sum",
|
|
202
|
+
"mean_olr": "mean",
|
|
203
|
+
"mean_sdr": "mean",
|
|
204
|
+
"mean_rsr": "mean",
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
# Check and pre-process `flight_waypoints`
|
|
208
|
+
vars_required = ["flight_id", "sac", *agg_map]
|
|
209
|
+
vars_required.remove("contrail_length")
|
|
210
|
+
vars_required.remove("persistent_contrail_length")
|
|
211
|
+
flight_waypoints.ensure_vars(vars_required)
|
|
212
|
+
|
|
213
|
+
flight_waypoints["contrail_length"] = np.where(
|
|
214
|
+
flight_waypoints["sac"] == 1.0, flight_waypoints["segment_length"], 0.0
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
flight_waypoints["persistent_contrail_length"] = np.where(
|
|
218
|
+
np.nan_to_num(flight_waypoints["ef"]) == 0.0, 0.0, flight_waypoints["segment_length"]
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
# Calculate contrail statistics for each flight
|
|
222
|
+
flight_summary = flight_waypoints.dataframe.groupby(["flight_id"]).agg(agg_map)
|
|
223
|
+
flight_summary.columns = (
|
|
224
|
+
flight_summary.columns.get_level_values(1)
|
|
225
|
+
+ "_"
|
|
226
|
+
+ flight_summary.columns.get_level_values(0)
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
rename_flight_summary_cols = {
|
|
230
|
+
"sum_segment_length": "total_flight_distance_flown",
|
|
231
|
+
"sum_contrail_length": "total_contrails_formed",
|
|
232
|
+
"sum_persistent_contrail_length": "total_persistent_contrails_formed",
|
|
233
|
+
"mean_mean_contrail_altitude": "mean_lifetime_contrail_altitude",
|
|
234
|
+
"mean_mean_rhi": "mean_lifetime_rhi",
|
|
235
|
+
"mean_mean_n_ice_per_m": "mean_lifetime_n_ice_per_m",
|
|
236
|
+
"mean_mean_r_ice_vol": "mean_lifetime_r_ice_vol",
|
|
237
|
+
"mean_mean_width": "mean_lifetime_contrail_width",
|
|
238
|
+
"mean_mean_depth": "mean_lifetime_contrail_depth",
|
|
239
|
+
"mean_mean_tau_contrail": "mean_lifetime_tau_contrail",
|
|
240
|
+
"mean_mean_tau_cirrus": "mean_lifetime_tau_cirrus",
|
|
241
|
+
"mean_max_age": "mean_contrail_lifetime",
|
|
242
|
+
"max_max_age": "max_contrail_lifetime",
|
|
243
|
+
"mean_mean_rf_sw": "mean_lifetime_rf_sw",
|
|
244
|
+
"mean_mean_rf_lw": "mean_lifetime_rf_lw",
|
|
245
|
+
"mean_mean_rf_net": "mean_lifetime_rf_net",
|
|
246
|
+
"sum_ef": "total_energy_forcing",
|
|
247
|
+
"mean_mean_olr": "mean_lifetime_olr",
|
|
248
|
+
"mean_mean_sdr": "mean_lifetime_sdr",
|
|
249
|
+
"mean_mean_rsr": "mean_lifetime_rsr",
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return flight_summary.rename(columns=rename_flight_summary_cols).reset_index(["flight_id"])
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
# ---------------
|
|
256
|
+
# Gridded outputs
|
|
257
|
+
# ---------------
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def longitude_latitude_grid(
|
|
261
|
+
t_start: np.datetime64 | pd.Timestamp,
|
|
262
|
+
t_end: np.datetime64 | pd.Timestamp,
|
|
263
|
+
flight_waypoints: GeoVectorDataset,
|
|
264
|
+
contrails: GeoVectorDataset,
|
|
265
|
+
*,
|
|
266
|
+
met: MetDataset,
|
|
267
|
+
spatial_bbox: tuple[float, float, float, float] = (-180.0, -90.0, 180.0, 90.0),
|
|
268
|
+
spatial_grid_res: float = 0.5,
|
|
269
|
+
) -> xr.Dataset:
|
|
270
|
+
r"""
|
|
271
|
+
Aggregate air traffic and contrail outputs to a longitude-latitude grid.
|
|
272
|
+
|
|
273
|
+
Parameters
|
|
274
|
+
----------
|
|
275
|
+
t_start : np.datetime64 | pd.Timestamp
|
|
276
|
+
UTC time at beginning of time step.
|
|
277
|
+
t_end : np.datetime64 | pd.Timestamp
|
|
278
|
+
UTC time at end of time step.
|
|
279
|
+
flight_waypoints : GeoVectorDataset
|
|
280
|
+
Flight waypoint outputs with contrail summary statistics attached.
|
|
281
|
+
See :func:`flight_waypoint_summary_statistics`.
|
|
282
|
+
contrails : GeoVectorDataset
|
|
283
|
+
Contrail evolution outputs from CoCiP, :attr:`Cocip.contrail`.
|
|
284
|
+
met : MetDataset
|
|
285
|
+
Pressure level dataset containing 'air_temperature', 'specific_humidity',
|
|
286
|
+
'specific_cloud_ice_water_content', and 'geopotential'.
|
|
287
|
+
spatial_bbox : tuple[float, float, float, float]
|
|
288
|
+
Spatial bounding box, ``(lon_min, lat_min, lon_max, lat_max)``, [:math:`\deg`]
|
|
289
|
+
spatial_grid_res : float
|
|
290
|
+
Spatial grid resolution, [:math:`\deg`]
|
|
291
|
+
|
|
292
|
+
Returns
|
|
293
|
+
-------
|
|
294
|
+
xr.Dataset
|
|
295
|
+
Air traffic and contrail outputs at a longitude-latitude grid.
|
|
296
|
+
"""
|
|
297
|
+
# Ensure the required columns are included in `flight_waypoints`, `contrails` and `met`
|
|
298
|
+
flight_waypoints.ensure_vars(("segment_length", "ef"))
|
|
299
|
+
contrails.ensure_vars(
|
|
300
|
+
(
|
|
301
|
+
"formation_time",
|
|
302
|
+
"segment_length",
|
|
303
|
+
"width",
|
|
304
|
+
"tau_contrail",
|
|
305
|
+
"rf_sw",
|
|
306
|
+
"rf_lw",
|
|
307
|
+
"rf_net",
|
|
308
|
+
"ef",
|
|
309
|
+
)
|
|
310
|
+
)
|
|
311
|
+
met.ensure_vars(
|
|
312
|
+
("air_temperature", "specific_humidity", "specific_cloud_ice_water_content", "geopotential")
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
# Downselect `met` to specified spatial bounding box
|
|
316
|
+
met = met.downselect(spatial_bbox)
|
|
317
|
+
|
|
318
|
+
# Ensure that `flight_waypoints` and `contrails` are within `t_start` and `t_end`
|
|
319
|
+
is_in_time = flight_waypoints.dataframe["time"].between(t_start, t_end, inclusive="right")
|
|
320
|
+
if not np.all(is_in_time):
|
|
321
|
+
warnings.warn(
|
|
322
|
+
"Flight waypoints have times that are outside the range of `t_start` and `t_end`. "
|
|
323
|
+
"Waypoints outside the defined time bounds are removed. "
|
|
324
|
+
)
|
|
325
|
+
flight_waypoints = flight_waypoints.filter(is_in_time)
|
|
326
|
+
|
|
327
|
+
is_in_time = contrails.dataframe["time"].between(t_start, t_end, inclusive="right")
|
|
328
|
+
|
|
329
|
+
if not np.all(is_in_time):
|
|
330
|
+
warnings.warn(
|
|
331
|
+
"Contrail waypoints have times that are outside the range of `t_start` and `t_end`."
|
|
332
|
+
"Waypoints outside the defined time bounds are removed. "
|
|
333
|
+
)
|
|
334
|
+
contrails = contrails.filter(is_in_time)
|
|
335
|
+
|
|
336
|
+
# Calculate additional variables
|
|
337
|
+
t_slices = np.unique(contrails["time"])
|
|
338
|
+
dt_integration_sec = (t_slices[1] - t_slices[0]) / np.timedelta64(1, "s")
|
|
339
|
+
|
|
340
|
+
da_area = geo.grid_surface_area(met["longitude"].values, met["latitude"].values)
|
|
341
|
+
|
|
342
|
+
flight_waypoints["persistent_contrails"] = np.where(
|
|
343
|
+
np.isnan(flight_waypoints["ef"]), 0.0, flight_waypoints["segment_length"]
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
# ----------------
|
|
347
|
+
# Grid aggregation
|
|
348
|
+
# ----------------
|
|
349
|
+
# (1) Waypoint properties between `t_start` and `t_end`
|
|
350
|
+
is_between_time = flight_waypoints.dataframe["time"].between(t_start, t_end, inclusive="right")
|
|
351
|
+
ds_wypts_t = vector_to_lon_lat_grid(
|
|
352
|
+
flight_waypoints.filter(is_between_time, copy=True),
|
|
353
|
+
agg={"segment_length": "sum", "persistent_contrails": "sum", "ef": "sum"},
|
|
354
|
+
spatial_bbox=spatial_bbox,
|
|
355
|
+
spatial_grid_res=spatial_grid_res,
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
# (2) Contrail properties at `t_end`
|
|
359
|
+
contrails_t_end = contrails.filter(contrails["time"] == t_end)
|
|
360
|
+
|
|
361
|
+
contrails_t_end["tau_contrail_area"] = (
|
|
362
|
+
contrails_t_end["tau_contrail"]
|
|
363
|
+
* contrails_t_end["segment_length"]
|
|
364
|
+
* contrails_t_end["width"]
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
contrails_t_end["age"] = (
|
|
368
|
+
contrails_t_end["time"] - contrails_t_end["formation_time"]
|
|
369
|
+
) / np.timedelta64(1, "h")
|
|
370
|
+
|
|
371
|
+
ds_contrails_t_end = vector_to_lon_lat_grid(
|
|
372
|
+
contrails_t_end,
|
|
373
|
+
agg={"segment_length": "sum", "tau_contrail_area": "sum", "age": "mean"},
|
|
374
|
+
spatial_bbox=spatial_bbox,
|
|
375
|
+
spatial_grid_res=spatial_grid_res,
|
|
376
|
+
)
|
|
377
|
+
ds_contrails_t_end["tau_contrail"] = ds_contrails_t_end["tau_contrail_area"] / da_area
|
|
378
|
+
|
|
379
|
+
# (3) Contrail and natural cirrus coverage area at `t_end`
|
|
380
|
+
mds_cirrus_coverage = cirrus_coverage_single_level(t_end, met, contrails)
|
|
381
|
+
ds_cirrus_coverage = mds_cirrus_coverage.data.squeeze(dim=["level", "time"])
|
|
382
|
+
|
|
383
|
+
# (4) Contrail climate forcing between `t_start` and `t_end`
|
|
384
|
+
contrails["ef_sw"] = np.where(
|
|
385
|
+
contrails["ef"] == 0.0,
|
|
386
|
+
0.0,
|
|
387
|
+
contrails["rf_sw"] * contrails["segment_length"] * contrails["width"] * dt_integration_sec,
|
|
388
|
+
)
|
|
389
|
+
contrails["ef_lw"] = np.where(
|
|
390
|
+
contrails["ef"] == 0.0,
|
|
391
|
+
0.0,
|
|
392
|
+
contrails["rf_lw"] * contrails["segment_length"] * contrails["width"] * dt_integration_sec,
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
ds_forcing = vector_to_lon_lat_grid(
|
|
396
|
+
contrails,
|
|
397
|
+
agg={"ef_sw": "sum", "ef_lw": "sum", "ef": "sum"},
|
|
398
|
+
spatial_bbox=spatial_bbox,
|
|
399
|
+
spatial_grid_res=spatial_grid_res,
|
|
400
|
+
)
|
|
401
|
+
ds_forcing["rf_sw"] = ds_forcing["ef_sw"] / (da_area * dt_integration_sec)
|
|
402
|
+
ds_forcing["rf_lw"] = ds_forcing["ef_lw"] / (da_area * dt_integration_sec)
|
|
403
|
+
ds_forcing["rf_net"] = ds_forcing["ef"] / (da_area * dt_integration_sec)
|
|
404
|
+
|
|
405
|
+
# -----------------------
|
|
406
|
+
# Package gridded outputs
|
|
407
|
+
# -----------------------
|
|
408
|
+
ds = xr.Dataset(
|
|
409
|
+
data_vars=dict(
|
|
410
|
+
flight_distance_flown=ds_wypts_t["segment_length"] / 1000.0,
|
|
411
|
+
persistent_contrails_formed=ds_wypts_t["persistent_contrails"] / 1000.0,
|
|
412
|
+
persistent_contrails=ds_contrails_t_end["segment_length"] / 1000.0,
|
|
413
|
+
tau_contrail=ds_contrails_t_end["tau_contrail"],
|
|
414
|
+
contrail_age=ds_contrails_t_end["age"],
|
|
415
|
+
cc_natural_cirrus=ds_cirrus_coverage["natural_cirrus"],
|
|
416
|
+
cc_contrails=ds_cirrus_coverage["contrails"],
|
|
417
|
+
cc_contrails_clear_sky=ds_cirrus_coverage["contrails_clear_sky"],
|
|
418
|
+
rf_sw=ds_forcing["rf_sw"] * 1000.0,
|
|
419
|
+
rf_lw=ds_forcing["rf_lw"] * 1000.0,
|
|
420
|
+
rf_net=ds_forcing["rf_net"] * 1000.0,
|
|
421
|
+
ef=ds_forcing["ef"],
|
|
422
|
+
ef_initial_loc=ds_wypts_t["ef"],
|
|
423
|
+
),
|
|
424
|
+
coords=ds_wypts_t.coords,
|
|
425
|
+
)
|
|
426
|
+
ds = ds.fillna(0.0)
|
|
427
|
+
ds = ds.expand_dims({"time": np.array([t_end])})
|
|
428
|
+
|
|
429
|
+
# Assign attributes
|
|
430
|
+
attrs = _create_attributes()
|
|
431
|
+
|
|
432
|
+
for name in ds.data_vars:
|
|
433
|
+
ds[name].attrs = attrs[name]
|
|
434
|
+
|
|
435
|
+
return ds
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def _create_attributes() -> dict[Hashable, dict[str, str]]:
|
|
439
|
+
return {
|
|
440
|
+
"flight_distance_flown": {
|
|
441
|
+
"long_name": "Total flight distance flown between t_start and t_end",
|
|
442
|
+
"units": "km",
|
|
443
|
+
},
|
|
444
|
+
"persistent_contrails_formed": {
|
|
445
|
+
"long_name": "Persistent contrails formed between t_start and t_end",
|
|
446
|
+
"units": "km",
|
|
447
|
+
},
|
|
448
|
+
"persistent_contrails": {
|
|
449
|
+
"long_name": "Persistent contrails at t_end",
|
|
450
|
+
"units": "km",
|
|
451
|
+
},
|
|
452
|
+
"tau_contrail": {
|
|
453
|
+
"long_name": "Area-normalised mean contrail optical depth at t_end",
|
|
454
|
+
"units": " ",
|
|
455
|
+
},
|
|
456
|
+
"contrail_age": {
|
|
457
|
+
"long_name": "Mean contrail age at t_end",
|
|
458
|
+
"units": "h",
|
|
459
|
+
},
|
|
460
|
+
"cc_natural_cirrus": {
|
|
461
|
+
"long_name": "Natural cirrus cover at t_end",
|
|
462
|
+
"units": " ",
|
|
463
|
+
},
|
|
464
|
+
"cc_contrails": {
|
|
465
|
+
"long_name": "Contrail cirrus cover at t_end",
|
|
466
|
+
"units": " ",
|
|
467
|
+
},
|
|
468
|
+
"cc_contrails_clear_sky": {
|
|
469
|
+
"long_name": "Contrail cirrus cover under clear sky conditions at t_end",
|
|
470
|
+
"units": " ",
|
|
471
|
+
},
|
|
472
|
+
"rf_sw": {
|
|
473
|
+
"long_name": "Mean contrail cirrus shortwave radiative forcing at t_end",
|
|
474
|
+
"units": "mW/m**2",
|
|
475
|
+
},
|
|
476
|
+
"rf_lw": {
|
|
477
|
+
"long_name": "Mean contrail cirrus longwave radiative forcing at t_end",
|
|
478
|
+
"units": "mW/m**2",
|
|
479
|
+
},
|
|
480
|
+
"rf_net": {
|
|
481
|
+
"long_name": "Mean contrail cirrus net radiative forcing at t_end",
|
|
482
|
+
"units": "mW/m**2",
|
|
483
|
+
},
|
|
484
|
+
"ef": {
|
|
485
|
+
"long_name": "Total contrail energy forcing between t_start and t_end",
|
|
486
|
+
"units": "J",
|
|
487
|
+
},
|
|
488
|
+
"ef_initial_loc": {
|
|
489
|
+
"long_name": "Total contrail energy forcing attributed back to the flight waypoint.",
|
|
490
|
+
"units": "J",
|
|
491
|
+
},
|
|
492
|
+
"contrails_clear_sky": {
|
|
493
|
+
"long_name": "Contrail cirrus cover in clear sky conditions.",
|
|
494
|
+
"units": " ",
|
|
495
|
+
},
|
|
496
|
+
"natural_cirrus": {
|
|
497
|
+
"long_name": "Natural cirrus cover.",
|
|
498
|
+
"units": " ",
|
|
499
|
+
},
|
|
500
|
+
"contrails": {
|
|
501
|
+
"long_name": "Contrail cirrus cover without overlap with natural cirrus.",
|
|
502
|
+
"units": " ",
|
|
503
|
+
},
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def cirrus_coverage_single_level(
|
|
508
|
+
time: np.datetime64 | pd.Timestamp,
|
|
509
|
+
met: MetDataset,
|
|
510
|
+
contrails: GeoVectorDataset,
|
|
511
|
+
*,
|
|
512
|
+
optical_depth_threshold: float = 0.1,
|
|
513
|
+
) -> MetDataset:
|
|
514
|
+
"""
|
|
515
|
+
Identify presence of contrail and natural cirrus in a longitude-latitude grid.
|
|
516
|
+
|
|
517
|
+
Parameters
|
|
518
|
+
----------
|
|
519
|
+
met : MetDataset
|
|
520
|
+
Pressure level dataset containing 'air_temperature', 'specific_cloud_ice_water_content',
|
|
521
|
+
and 'geopotential' fields.
|
|
522
|
+
contrails : GeoVectorDataset
|
|
523
|
+
Contrail waypoints containing 'tau_contrail' field.
|
|
524
|
+
time : np.datetime64 | pd.Timestamp
|
|
525
|
+
Time when the cirrus statistics is computed.
|
|
526
|
+
optical_depth_threshold : float
|
|
527
|
+
Sensitivity of cirrus detection, set at 0.1 to match the capability of satellites.
|
|
528
|
+
|
|
529
|
+
Returns
|
|
530
|
+
-------
|
|
531
|
+
MetDataset
|
|
532
|
+
Single level dataset containing the contrail and natural cirrus coverage.
|
|
533
|
+
"""
|
|
534
|
+
# Ensure `met` and `contrails` contains the required variables
|
|
535
|
+
met.ensure_vars(("air_temperature", "specific_cloud_ice_water_content", "geopotential"))
|
|
536
|
+
contrails.ensure_vars("tau_contrail")
|
|
537
|
+
|
|
538
|
+
# Spatial bounding box and resolution of `met`
|
|
539
|
+
spatial_bbox = (
|
|
540
|
+
np.min(met["longitude"].values),
|
|
541
|
+
np.min(met["latitude"].values),
|
|
542
|
+
np.max(met["longitude"].values),
|
|
543
|
+
np.max(met["latitude"].values),
|
|
544
|
+
)
|
|
545
|
+
spatial_grid_res = np.diff(met["longitude"].values)[0]
|
|
546
|
+
|
|
547
|
+
# Contrail cirrus optical depth in a longitude-latitude grid
|
|
548
|
+
tau_contrail = vector_to_lon_lat_grid(
|
|
549
|
+
contrails.filter(contrails["time"] == time),
|
|
550
|
+
agg={"tau_contrail": "sum"},
|
|
551
|
+
spatial_bbox=spatial_bbox,
|
|
552
|
+
spatial_grid_res=spatial_grid_res,
|
|
553
|
+
)["tau_contrail"]
|
|
554
|
+
tau_contrail = tau_contrail.expand_dims({"level": np.array([-1])})
|
|
555
|
+
tau_contrail = tau_contrail.expand_dims({"time": np.array([time])})
|
|
556
|
+
mda_tau_contrail = MetDataArray(tau_contrail)
|
|
557
|
+
|
|
558
|
+
# Natural cirrus optical depth in a longitude-latitude grid
|
|
559
|
+
met["tau_cirrus"] = tau_cirrus(met)
|
|
560
|
+
tau_cirrus_max = met["tau_cirrus"].data.sel(level=met["level"].data[-1], time=time)
|
|
561
|
+
tau_cirrus_max = tau_cirrus_max.expand_dims({"level": np.array([-1])})
|
|
562
|
+
tau_cirrus_max = tau_cirrus_max.expand_dims({"time": np.array([time])})
|
|
563
|
+
mda_tau_cirrus_max = MetDataArray(tau_cirrus_max)
|
|
564
|
+
mda_tau_all = MetDataArray(mda_tau_contrail.data + mda_tau_cirrus_max.data)
|
|
565
|
+
|
|
566
|
+
# Contrail and natural cirrus coverage in a longitude-latitude grid
|
|
567
|
+
mda_cc_contrails_clear_sky = optical_depth_to_cirrus_coverage(
|
|
568
|
+
mda_tau_contrail, threshold=optical_depth_threshold
|
|
569
|
+
)
|
|
570
|
+
mda_cc_natural_cirrus = optical_depth_to_cirrus_coverage(
|
|
571
|
+
mda_tau_cirrus_max, threshold=optical_depth_threshold
|
|
572
|
+
)
|
|
573
|
+
mda_cc_total = optical_depth_to_cirrus_coverage(mda_tau_all, threshold=optical_depth_threshold)
|
|
574
|
+
mda_cc_contrails = MetDataArray(mda_cc_total.data - mda_cc_natural_cirrus.data)
|
|
575
|
+
|
|
576
|
+
# Concatenate data
|
|
577
|
+
ds = xr.Dataset(
|
|
578
|
+
data_vars=dict(
|
|
579
|
+
contrails_clear_sky=mda_cc_contrails_clear_sky.data,
|
|
580
|
+
natural_cirrus=mda_cc_natural_cirrus.data,
|
|
581
|
+
contrails=mda_cc_contrails.data,
|
|
582
|
+
),
|
|
583
|
+
coords=mda_cc_contrails_clear_sky.coords,
|
|
584
|
+
)
|
|
585
|
+
|
|
586
|
+
# Update attributes
|
|
587
|
+
attrs = _create_attributes()
|
|
588
|
+
|
|
589
|
+
for name in ds.data_vars:
|
|
590
|
+
ds[name].attrs = attrs[name]
|
|
591
|
+
|
|
592
|
+
return MetDataset(ds)
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
def optical_depth_to_cirrus_coverage(
|
|
596
|
+
optical_depth: MetDataArray,
|
|
597
|
+
*,
|
|
598
|
+
threshold: float = 0.1,
|
|
599
|
+
) -> MetDataArray:
|
|
600
|
+
"""
|
|
601
|
+
Calculate contrail or natural cirrus coverage in a longitude-latitude grid.
|
|
602
|
+
|
|
603
|
+
A grid cell is assumed to be covered by cirrus if the optical depth is above ``threshold``.
|
|
604
|
+
|
|
605
|
+
Parameters
|
|
606
|
+
----------
|
|
607
|
+
optical_depth : MetDataArray
|
|
608
|
+
Contrail or natural cirrus optical depth in a longitude-latitude grid
|
|
609
|
+
threshold : float
|
|
610
|
+
Sensitivity of cirrus detection, set at 0.1 to match the capability of satellites.
|
|
611
|
+
|
|
612
|
+
Returns
|
|
613
|
+
-------
|
|
614
|
+
MetDataArray
|
|
615
|
+
Contrail or natural cirrus coverage in a longitude-latitude grid
|
|
616
|
+
"""
|
|
617
|
+
cirrus_cover = (optical_depth.data > threshold).astype(int)
|
|
618
|
+
return MetDataArray(cirrus_cover)
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
def regional_statistics(da_var: xr.DataArray, *, agg: str) -> pd.Series:
|
|
622
|
+
"""
|
|
623
|
+
Calculate regional statistics from longitude-latitude grid.
|
|
624
|
+
|
|
625
|
+
Parameters
|
|
626
|
+
----------
|
|
627
|
+
da_var : xr.DataArray
|
|
628
|
+
Air traffic or contrail variable in a longitude-latitude grid.
|
|
629
|
+
agg : str
|
|
630
|
+
Function selected for aggregation, (i.e., "sum" and "mean").
|
|
631
|
+
|
|
632
|
+
Returns
|
|
633
|
+
-------
|
|
634
|
+
pd.Series
|
|
635
|
+
Regional statistics
|
|
636
|
+
|
|
637
|
+
Notes
|
|
638
|
+
-----
|
|
639
|
+
- The spatial bounding box for each region is defined in Teoh et al. (2023)
|
|
640
|
+
- Teoh, R., Engberg, Z., Shapiro, M., Dray, L., and Stettler, M.: A high-resolution Global
|
|
641
|
+
Aviation emissions Inventory based on ADS-B (GAIA) for 2019-2021, EGUsphere [preprint],
|
|
642
|
+
https://doi.org/10.5194/egusphere-2023-724, 2023.
|
|
643
|
+
"""
|
|
644
|
+
if (agg == "mean") and (len(da_var.time) > 1):
|
|
645
|
+
da_var = da_var.mean(dim=["time"])
|
|
646
|
+
da_var = da_var.fillna(0.0)
|
|
647
|
+
|
|
648
|
+
# Get regional domain
|
|
649
|
+
vars_regional = _regional_data_arrays(da_var)
|
|
650
|
+
|
|
651
|
+
if agg == "sum":
|
|
652
|
+
vals = {
|
|
653
|
+
"World": np.nansum(vars_regional["world"].values),
|
|
654
|
+
"USA": np.nansum(vars_regional["usa"].values),
|
|
655
|
+
"Europe": np.nansum(vars_regional["europe"].values),
|
|
656
|
+
"East Asia": np.nansum(vars_regional["east_asia"].values),
|
|
657
|
+
"SEA": np.nansum(vars_regional["sea"].values),
|
|
658
|
+
"Latin America": np.nansum(vars_regional["latin_america"].values),
|
|
659
|
+
"Africa": np.nansum(vars_regional["africa"].values),
|
|
660
|
+
"China": np.nansum(vars_regional["china"].values),
|
|
661
|
+
"India": np.nansum(vars_regional["india"].values),
|
|
662
|
+
"North Atlantic": np.nansum(vars_regional["n_atlantic"].values),
|
|
663
|
+
"North Pacific": np.nansum(vars_regional["n_pacific_1"].values)
|
|
664
|
+
+ np.nansum(vars_regional["n_pacific_2"].values),
|
|
665
|
+
"Arctic": np.nansum(vars_regional["arctic"].values),
|
|
666
|
+
}
|
|
667
|
+
elif agg == "mean":
|
|
668
|
+
area_world = geo.grid_surface_area(da_var["longitude"].values, da_var["latitude"].values)
|
|
669
|
+
area_regional = _regional_data_arrays(area_world)
|
|
670
|
+
|
|
671
|
+
vals = {
|
|
672
|
+
"World": _area_mean_properties(vars_regional["world"], area_regional["world"]),
|
|
673
|
+
"USA": _area_mean_properties(vars_regional["usa"], area_regional["usa"]),
|
|
674
|
+
"Europe": _area_mean_properties(vars_regional["europe"], area_regional["europe"]),
|
|
675
|
+
"East Asia": _area_mean_properties(
|
|
676
|
+
vars_regional["east_asia"], area_regional["east_asia"]
|
|
677
|
+
),
|
|
678
|
+
"SEA": _area_mean_properties(vars_regional["sea"], area_regional["sea"]),
|
|
679
|
+
"Latin America": _area_mean_properties(
|
|
680
|
+
vars_regional["latin_america"], area_regional["latin_america"]
|
|
681
|
+
),
|
|
682
|
+
"Africa": _area_mean_properties(vars_regional["africa"], area_regional["africa"]),
|
|
683
|
+
"China": _area_mean_properties(vars_regional["china"], area_regional["china"]),
|
|
684
|
+
"India": _area_mean_properties(vars_regional["india"], area_regional["india"]),
|
|
685
|
+
"North Atlantic": _area_mean_properties(
|
|
686
|
+
vars_regional["n_atlantic"], area_regional["n_atlantic"]
|
|
687
|
+
),
|
|
688
|
+
"North Pacific": 0.4
|
|
689
|
+
* _area_mean_properties(vars_regional["n_pacific_1"], area_regional["n_pacific_1"])
|
|
690
|
+
+ 0.6
|
|
691
|
+
* _area_mean_properties(vars_regional["n_pacific_2"], area_regional["n_pacific_2"]),
|
|
692
|
+
"Arctic": _area_mean_properties(vars_regional["arctic"], area_regional["arctic"]),
|
|
693
|
+
}
|
|
694
|
+
else:
|
|
695
|
+
raise NotImplementedError('Aggregation only accepts operations of "mean" or "sum".')
|
|
696
|
+
|
|
697
|
+
return pd.Series(vals)
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
def _regional_data_arrays(da_global: xr.DataArray) -> dict[str, xr.DataArray]:
|
|
701
|
+
"""
|
|
702
|
+
Extract regional data arrays from global data array.
|
|
703
|
+
|
|
704
|
+
Parameters
|
|
705
|
+
----------
|
|
706
|
+
da_global : xr.DataArray
|
|
707
|
+
Global air traffic or contrail variable in a longitude-latitude grid.
|
|
708
|
+
|
|
709
|
+
Returns
|
|
710
|
+
-------
|
|
711
|
+
dict[str, xr.DataArray]
|
|
712
|
+
Regional data arrays.
|
|
713
|
+
|
|
714
|
+
Notes
|
|
715
|
+
-----
|
|
716
|
+
- The spatial bounding box for each region is defined in Teoh et al. (2023)
|
|
717
|
+
- Teoh, R., Engberg, Z., Shapiro, M., Dray, L., and Stettler, M.: A high-resolution Global
|
|
718
|
+
Aviation emissions Inventory based on ADS-B (GAIA) for 2019-2021, EGUsphere [preprint],
|
|
719
|
+
https://doi.org/10.5194/egusphere-2023-724, 2023.
|
|
720
|
+
"""
|
|
721
|
+
return {
|
|
722
|
+
"world": da_global.copy(),
|
|
723
|
+
"usa": da_global.sel(longitude=slice(-126.0, -66.0), latitude=slice(23.0, 50.0)),
|
|
724
|
+
"europe": da_global.sel(longitude=slice(-12.0, 20.0), latitude=slice(35.0, 60.0)),
|
|
725
|
+
"east_asia": da_global.sel(longitude=slice(103.0, 150.0), latitude=slice(15.0, 48.0)),
|
|
726
|
+
"sea": da_global.sel(longitude=slice(87.5, 130.0), latitude=slice(-10.0, 20.0)),
|
|
727
|
+
"latin_america": da_global.sel(longitude=slice(-85.0, -35.0), latitude=slice(-60.0, 15.0)),
|
|
728
|
+
"africa": da_global.sel(longitude=slice(-20.0, 50.0), latitude=slice(-35.0, 40.0)),
|
|
729
|
+
"china": da_global.sel(longitude=slice(73.5, 135.0), latitude=slice(18.0, 53.5)),
|
|
730
|
+
"india": da_global.sel(longitude=slice(68.0, 97.5), latitude=slice(8.0, 35.5)),
|
|
731
|
+
"n_atlantic": da_global.sel(longitude=slice(-70.0, -5.0), latitude=slice(40.0, 63.0)),
|
|
732
|
+
"n_pacific_1": da_global.sel(longitude=slice(-180.0, -140.0), latitude=slice(35.0, 65.0)),
|
|
733
|
+
"n_pacific_2": da_global.sel(longitude=slice(120.0, 180.0), latitude=slice(35.0, 65.0)),
|
|
734
|
+
"arctic": da_global.sel(latitude=slice(66.5, 90.0)),
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
def _area_mean_properties(da_var_region: xr.DataArray, da_area_region: xr.DataArray) -> float:
|
|
739
|
+
"""
|
|
740
|
+
Calculate area-mean properties.
|
|
741
|
+
|
|
742
|
+
Parameters
|
|
743
|
+
----------
|
|
744
|
+
da_var_region : xr.DataArray
|
|
745
|
+
Regional air traffic or contrail variable in a longitude-latitude grid.
|
|
746
|
+
da_area_region : xr.DataArray
|
|
747
|
+
Regional surface area in a longitude-latitude grid.
|
|
748
|
+
|
|
749
|
+
Returns
|
|
750
|
+
-------
|
|
751
|
+
float
|
|
752
|
+
Area-mean properties
|
|
753
|
+
"""
|
|
754
|
+
return np.nansum(da_var_region.values * da_area_region.values) / np.nansum(
|
|
755
|
+
da_area_region.values
|
|
756
|
+
)
|
|
757
|
+
|
|
758
|
+
|
|
759
|
+
# ---------------------
|
|
760
|
+
# Time-slice statistics
|
|
761
|
+
# ---------------------
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
def time_slice_statistics(
|
|
765
|
+
t_start: np.datetime64 | pd.Timestamp,
|
|
766
|
+
t_end: np.datetime64 | pd.Timestamp,
|
|
767
|
+
flight_waypoints: GeoVectorDataset,
|
|
768
|
+
contrails: GeoVectorDataset,
|
|
769
|
+
*,
|
|
770
|
+
humidity_scaling: HumidityScaling,
|
|
771
|
+
met: MetDataset | None = None,
|
|
772
|
+
rad: MetDataset | None = None,
|
|
773
|
+
spatial_bbox: tuple[float, float, float, float] = (-180.0, -90.0, 180.0, 90.0),
|
|
774
|
+
) -> pd.Series:
|
|
775
|
+
r"""
|
|
776
|
+
Calculate the flight and contrail summary statistics between `t_start` and `t_end`.
|
|
777
|
+
|
|
778
|
+
Parameters
|
|
779
|
+
----------
|
|
780
|
+
t_start : np.datetime64 | pd.Timestamp
|
|
781
|
+
UTC time at beginning of time step.
|
|
782
|
+
t_end : np.datetime64 | pd.Timestamp
|
|
783
|
+
UTC time at end of time step.
|
|
784
|
+
flight_waypoints : GeoVectorDataset
|
|
785
|
+
Flight waypoint outputs.
|
|
786
|
+
contrails : GeoVectorDataset
|
|
787
|
+
Contrail evolution outputs from CoCiP, `cocip.contrail`.
|
|
788
|
+
humidity_scaling : HumidityScaling
|
|
789
|
+
Humidity scaling methodology.
|
|
790
|
+
See :attr:`CocipParams.humidity_scaling`
|
|
791
|
+
met : MetDataset | None
|
|
792
|
+
Pressure level dataset containing 'air_temperature', 'specific_humidity',
|
|
793
|
+
'specific_cloud_ice_water_content', and 'geopotential'.
|
|
794
|
+
Meteorological statistics will not be computed if `None` is provided.
|
|
795
|
+
rad : MetDataset | None
|
|
796
|
+
Single level dataset containing the `sdr`, `rsr` and `olr`.Radiation statistics
|
|
797
|
+
will not be computed if `None` is provided.
|
|
798
|
+
|
|
799
|
+
spatial_bbox : tuple[float, float, float, float]
|
|
800
|
+
Spatial bounding box, `(lon_min, lat_min, lon_max, lat_max)`, [:math:`\deg`]
|
|
801
|
+
|
|
802
|
+
Returns
|
|
803
|
+
-------
|
|
804
|
+
pd.Series
|
|
805
|
+
Flight and contrail summary statistics. Contrail statistics are provided at `t_end`.
|
|
806
|
+
The units for each output are outlined in `Notes`.
|
|
807
|
+
|
|
808
|
+
Notes
|
|
809
|
+
-----
|
|
810
|
+
Outputs and units:
|
|
811
|
+
- ``n_flights``, [dimensionless]
|
|
812
|
+
- ``n_flights_forming_contrails``, [dimensionless]
|
|
813
|
+
- ``n_flights_forming_persistent_contrails``, [dimensionless]
|
|
814
|
+
- ``n_flights_with_persistent_contrails_at_t_end``, [dimensionless]
|
|
815
|
+
|
|
816
|
+
- ``n_waypoints``, [dimensionless]
|
|
817
|
+
- ``n_waypoints_forming_contrails``, [dimensionless]
|
|
818
|
+
- ``n_waypoints_forming_persistent_contrails``, [dimensionless]
|
|
819
|
+
- ``n_waypoints_with_persistent_contrails_at_t_end``, [dimensionless]
|
|
820
|
+
- ``n_contrail_waypoints_at_night``, [dimensionless]
|
|
821
|
+
- ``pct_contrail_waypoints_at_night``, [%]
|
|
822
|
+
|
|
823
|
+
- ``total_flight_distance``, [:math:`km`]
|
|
824
|
+
- ``total_contrails_formed``, [:math:`km`]
|
|
825
|
+
- ``total_persistent_contrails_formed``, [:math:`km`]
|
|
826
|
+
- ``total_persistent_contrails_at_t_end``, [:math:`km`]
|
|
827
|
+
|
|
828
|
+
- ``total_fuel_burn``, [:math:`kg`]
|
|
829
|
+
- ``mean_propulsion_efficiency_all_flights``, [dimensionless]
|
|
830
|
+
- ``mean_propulsion_efficiency_flights_with_persistent_contrails``, [dimensionless]
|
|
831
|
+
- ``mean_nvpm_ei_n_all_flights``, [:math:`kg^{-1}`]
|
|
832
|
+
- ``mean_nvpm_ei_n_flights_with_persistent_contrails``, [:math:`kg^{-1}`]
|
|
833
|
+
|
|
834
|
+
- ``mean_contrail_age``, [:math:`h`]
|
|
835
|
+
- ``max_contrail_age``, [:math:`h`]
|
|
836
|
+
- ``mean_n_ice_per_m``, [:math:`m^{-1}`]
|
|
837
|
+
- ``mean_contrail_ice_water_path``, [:math:`kg m^{-2}`]
|
|
838
|
+
- ``area_mean_contrail_ice_radius``, [:math:`\mu m`]
|
|
839
|
+
- ``volume_mean_contrail_ice_radius``, [:math:`\mu m`]
|
|
840
|
+
- ``mean_contrail_ice_effective_radius``, [:math:`\mu m`]
|
|
841
|
+
- ``mean_tau_contrail``, [dimensionless]
|
|
842
|
+
- ``mean_tau_cirrus``, [dimensionless]
|
|
843
|
+
|
|
844
|
+
- ``mean_rf_sw``, [:math:`W m^{-2}`]
|
|
845
|
+
- ``mean_rf_lw``, [:math:`W m^{-2}`]
|
|
846
|
+
- ``mean_rf_net``, [:math:`W m^{-2}`]
|
|
847
|
+
- ``total_contrail_ef``, [:math:`J`]
|
|
848
|
+
|
|
849
|
+
- ``issr_percentage_coverage``, [%]
|
|
850
|
+
- ``mean_rhi_in_issr``, [dimensionless]
|
|
851
|
+
- ``contrail_cirrus_percentage_coverage``, [%]
|
|
852
|
+
- ``contrail_cirrus_clear_sky_percentage_coverage``, [%]
|
|
853
|
+
- ``natural_cirrus_percentage_coverage``, [%]
|
|
854
|
+
- ``cloud_contrail_overlap_percentage``, [%]
|
|
855
|
+
|
|
856
|
+
- ``mean_sdr_domain``, [:math:`W m^{-2}`]
|
|
857
|
+
- ``mean_sdr_at_contrail_wypts``, [:math:`W m^{-2}`]
|
|
858
|
+
- ``mean_rsr_domain``, [:math:`W m^{-2}`]
|
|
859
|
+
- ``mean_rsr_at_contrail_wypts``, [:math:`W m^{-2}`]
|
|
860
|
+
- ``mean_olr_domain``, [:math:`W m^{-2}`]
|
|
861
|
+
- ``mean_olr_at_contrail_wypts``, [:math:`W m^{-2}`]
|
|
862
|
+
- ``mean_albedo_at_contrail_wypts``, [dimensionless]
|
|
863
|
+
"""
|
|
864
|
+
# Ensure the required columns are included in `flight_waypoints`, `contrails`, `met` and `rad`
|
|
865
|
+
flight_waypoints.ensure_vars(
|
|
866
|
+
(
|
|
867
|
+
"flight_id",
|
|
868
|
+
"segment_length",
|
|
869
|
+
"true_airspeed",
|
|
870
|
+
"fuel_flow",
|
|
871
|
+
"engine_efficiency",
|
|
872
|
+
"nvpm_ei_n",
|
|
873
|
+
"sac",
|
|
874
|
+
"persistent_1",
|
|
875
|
+
)
|
|
876
|
+
)
|
|
877
|
+
contrails.ensure_vars(
|
|
878
|
+
(
|
|
879
|
+
"flight_id",
|
|
880
|
+
"segment_length",
|
|
881
|
+
"air_temperature",
|
|
882
|
+
"iwc",
|
|
883
|
+
"r_ice_vol",
|
|
884
|
+
"n_ice_per_m",
|
|
885
|
+
"tau_contrail",
|
|
886
|
+
"tau_cirrus",
|
|
887
|
+
"width",
|
|
888
|
+
"area_eff",
|
|
889
|
+
"sdr",
|
|
890
|
+
"rsr",
|
|
891
|
+
"olr",
|
|
892
|
+
"rf_sw",
|
|
893
|
+
"rf_lw",
|
|
894
|
+
"rf_net",
|
|
895
|
+
"ef",
|
|
896
|
+
)
|
|
897
|
+
)
|
|
898
|
+
|
|
899
|
+
# Ensure that the waypoints are within `t_start` and `t_end`
|
|
900
|
+
is_in_time = flight_waypoints.dataframe["time"].between(t_start, t_end, inclusive="right")
|
|
901
|
+
|
|
902
|
+
if not np.all(is_in_time):
|
|
903
|
+
warnings.warn(
|
|
904
|
+
"Flight waypoints have times that are outside the range of `t_start` and `t_end`. "
|
|
905
|
+
"Waypoints outside the defined time bounds are removed. "
|
|
906
|
+
)
|
|
907
|
+
flight_waypoints = flight_waypoints.filter(is_in_time)
|
|
908
|
+
|
|
909
|
+
is_in_time = contrails.dataframe["time"].between(t_start, t_end, inclusive="right")
|
|
910
|
+
if not np.all(is_in_time):
|
|
911
|
+
warnings.warn(
|
|
912
|
+
"Contrail waypoints have times that are outside the range of `t_start` and `t_end`."
|
|
913
|
+
"Waypoints outside the defined time bounds are removed. "
|
|
914
|
+
)
|
|
915
|
+
contrails = contrails.filter(is_in_time)
|
|
916
|
+
|
|
917
|
+
# Additional variables
|
|
918
|
+
flight_waypoints["fuel_burn"] = (
|
|
919
|
+
flight_waypoints["fuel_flow"]
|
|
920
|
+
* (1 / flight_waypoints["true_airspeed"])
|
|
921
|
+
* flight_waypoints["segment_length"]
|
|
922
|
+
)
|
|
923
|
+
contrails["pressure"] = units.m_to_pl(contrails["altitude"])
|
|
924
|
+
contrails["rho_air"] = thermo.rho_d(contrails["air_temperature"], contrails["pressure"])
|
|
925
|
+
contrails["plume_mass_per_m"] = plume_mass_per_distance(
|
|
926
|
+
contrails["area_eff"], contrails["rho_air"]
|
|
927
|
+
)
|
|
928
|
+
contrails["age"] = (contrails["time"] - contrails["formation_time"]) / np.timedelta64(1, "h")
|
|
929
|
+
|
|
930
|
+
# Meteorology domain statistics
|
|
931
|
+
if met is not None:
|
|
932
|
+
met.ensure_vars(
|
|
933
|
+
(
|
|
934
|
+
"air_temperature",
|
|
935
|
+
"specific_humidity",
|
|
936
|
+
"specific_cloud_ice_water_content",
|
|
937
|
+
"geopotential",
|
|
938
|
+
)
|
|
939
|
+
)
|
|
940
|
+
met = met.downselect(spatial_bbox)
|
|
941
|
+
met_stats = meteorological_time_slice_statistics(t_end, contrails, met, humidity_scaling)
|
|
942
|
+
|
|
943
|
+
# Radiation domain statistics
|
|
944
|
+
if rad is not None:
|
|
945
|
+
rad.ensure_vars(("sdr", "rsr", "olr"))
|
|
946
|
+
rad = rad.downselect(spatial_bbox)
|
|
947
|
+
rad_stats = radiation_time_slice_statistics(rad, t_end)
|
|
948
|
+
|
|
949
|
+
# Calculate time-slice statistics
|
|
950
|
+
is_sac = flight_waypoints["sac"] == 1.0
|
|
951
|
+
is_persistent = flight_waypoints["persistent_1"] == 1.0
|
|
952
|
+
is_at_t_end = contrails["time"] == t_end
|
|
953
|
+
is_night_time = contrails["sdr"] < 0.1
|
|
954
|
+
domain_area = geo.domain_surface_area(spatial_bbox)
|
|
955
|
+
|
|
956
|
+
stats_t = {
|
|
957
|
+
"time_start": t_start,
|
|
958
|
+
"time_end": t_end,
|
|
959
|
+
# Flight statistics
|
|
960
|
+
"n_flights": len(flight_waypoints.dataframe["flight_id"].unique()),
|
|
961
|
+
"n_flights_forming_contrails": len(
|
|
962
|
+
flight_waypoints.filter(is_sac).dataframe["flight_id"].unique()
|
|
963
|
+
),
|
|
964
|
+
"n_flights_forming_persistent_contrails": len(
|
|
965
|
+
flight_waypoints.filter(is_persistent).dataframe["flight_id"].unique()
|
|
966
|
+
),
|
|
967
|
+
"n_flights_with_persistent_contrails_at_t_end": len(
|
|
968
|
+
contrails.filter(is_at_t_end).dataframe["flight_id"].unique()
|
|
969
|
+
),
|
|
970
|
+
# Waypoint statistics
|
|
971
|
+
"n_waypoints": len(flight_waypoints),
|
|
972
|
+
"n_waypoints_forming_contrails": len(flight_waypoints.filter(is_sac)),
|
|
973
|
+
"n_waypoints_forming_persistent_contrails": len(flight_waypoints.filter(is_persistent)),
|
|
974
|
+
"n_waypoints_with_persistent_contrails_at_t_end": len(contrails.filter(is_at_t_end)),
|
|
975
|
+
"n_contrail_waypoints_at_night": len(contrails.filter(is_at_t_end)),
|
|
976
|
+
"pct_contrail_waypoints_at_night": (
|
|
977
|
+
len(contrails.filter(is_night_time)) / len(contrails) * 100
|
|
978
|
+
),
|
|
979
|
+
# Distance statistics
|
|
980
|
+
"total_flight_distance": np.nansum(flight_waypoints["segment_length"]) / 1000,
|
|
981
|
+
"total_contrails_formed": (
|
|
982
|
+
np.nansum(flight_waypoints.filter(is_sac)["segment_length"]) / 1000
|
|
983
|
+
),
|
|
984
|
+
"total_persistent_contrails_formed": (
|
|
985
|
+
np.nansum(flight_waypoints.filter(is_persistent)["segment_length"]) / 1000
|
|
986
|
+
),
|
|
987
|
+
"total_persistent_contrails_at_t_end": (
|
|
988
|
+
np.nansum(contrails.filter(is_at_t_end)["segment_length"]) / 1000
|
|
989
|
+
),
|
|
990
|
+
# Aircraft performance statistics
|
|
991
|
+
"total_fuel_burn": np.nansum(flight_waypoints["fuel_burn"]),
|
|
992
|
+
"mean_propulsion_efficiency_all_flights": np.nanmean(flight_waypoints["engine_efficiency"]),
|
|
993
|
+
"mean_propulsion_efficiency_flights_with_persistent_contrails": (
|
|
994
|
+
np.nanmean(flight_waypoints.filter(is_persistent)["engine_efficiency"])
|
|
995
|
+
if np.any(is_persistent)
|
|
996
|
+
else np.nan
|
|
997
|
+
),
|
|
998
|
+
"mean_nvpm_ei_n_all_flights": np.nanmean(flight_waypoints["nvpm_ei_n"]),
|
|
999
|
+
"mean_nvpm_ei_n_flights_with_persistent_contrails": (
|
|
1000
|
+
np.nanmean(flight_waypoints.filter(is_persistent)["nvpm_ei_n"])
|
|
1001
|
+
if np.any(is_persistent)
|
|
1002
|
+
else np.nan
|
|
1003
|
+
),
|
|
1004
|
+
# Contrail properties at `time_end`
|
|
1005
|
+
"mean_contrail_age": (
|
|
1006
|
+
np.nanmean(contrails.filter(is_at_t_end)["age"]) if np.any(is_at_t_end) else np.nan
|
|
1007
|
+
),
|
|
1008
|
+
"max_contrail_age": (
|
|
1009
|
+
np.nanmax(contrails.filter(is_at_t_end)["age"]) if np.any(is_at_t_end) else np.nan
|
|
1010
|
+
),
|
|
1011
|
+
"mean_n_ice_per_m": (
|
|
1012
|
+
np.nanmean(contrails.filter(is_at_t_end)["n_ice_per_m"])
|
|
1013
|
+
if np.any(is_at_t_end)
|
|
1014
|
+
else np.nan
|
|
1015
|
+
),
|
|
1016
|
+
"mean_contrail_ice_water_path": (
|
|
1017
|
+
area_mean_ice_water_path(
|
|
1018
|
+
contrails.filter(is_at_t_end)["iwc"],
|
|
1019
|
+
contrails.filter(is_at_t_end)["plume_mass_per_m"],
|
|
1020
|
+
contrails.filter(is_at_t_end)["segment_length"],
|
|
1021
|
+
domain_area,
|
|
1022
|
+
)
|
|
1023
|
+
if np.any(is_at_t_end)
|
|
1024
|
+
else np.nan
|
|
1025
|
+
),
|
|
1026
|
+
"area_mean_contrail_ice_radius": (
|
|
1027
|
+
area_mean_ice_particle_radius(
|
|
1028
|
+
contrails.filter(is_at_t_end)["r_ice_vol"],
|
|
1029
|
+
contrails.filter(is_at_t_end)["n_ice_per_m"],
|
|
1030
|
+
contrails.filter(is_at_t_end)["segment_length"],
|
|
1031
|
+
)
|
|
1032
|
+
if np.any(is_at_t_end)
|
|
1033
|
+
else np.nan
|
|
1034
|
+
),
|
|
1035
|
+
"volume_mean_contrail_ice_radius": (
|
|
1036
|
+
volume_mean_ice_particle_radius(
|
|
1037
|
+
contrails.filter(is_at_t_end)["r_ice_vol"],
|
|
1038
|
+
contrails.filter(is_at_t_end)["n_ice_per_m"],
|
|
1039
|
+
contrails.filter(is_at_t_end)["segment_length"],
|
|
1040
|
+
)
|
|
1041
|
+
if np.any(is_at_t_end)
|
|
1042
|
+
else np.nan
|
|
1043
|
+
),
|
|
1044
|
+
"mean_contrail_ice_effective_radius": (
|
|
1045
|
+
mean_ice_particle_effective_radius(
|
|
1046
|
+
contrails.filter(is_at_t_end)["r_ice_vol"],
|
|
1047
|
+
contrails.filter(is_at_t_end)["n_ice_per_m"],
|
|
1048
|
+
contrails.filter(is_at_t_end)["segment_length"],
|
|
1049
|
+
)
|
|
1050
|
+
if np.any(is_at_t_end)
|
|
1051
|
+
else np.nan
|
|
1052
|
+
),
|
|
1053
|
+
"mean_tau_contrail": (
|
|
1054
|
+
area_mean_contrail_property(
|
|
1055
|
+
contrails.filter(is_at_t_end)["tau_contrail"],
|
|
1056
|
+
contrails.filter(is_at_t_end)["segment_length"],
|
|
1057
|
+
contrails.filter(is_at_t_end)["width"],
|
|
1058
|
+
domain_area,
|
|
1059
|
+
)
|
|
1060
|
+
if np.any(is_at_t_end)
|
|
1061
|
+
else np.nan
|
|
1062
|
+
),
|
|
1063
|
+
"mean_tau_cirrus": (
|
|
1064
|
+
area_mean_contrail_property(
|
|
1065
|
+
contrails.filter(is_at_t_end)["tau_cirrus"],
|
|
1066
|
+
contrails.filter(is_at_t_end)["segment_length"],
|
|
1067
|
+
contrails.filter(is_at_t_end)["width"],
|
|
1068
|
+
domain_area,
|
|
1069
|
+
)
|
|
1070
|
+
if np.any(is_at_t_end)
|
|
1071
|
+
else np.nan
|
|
1072
|
+
),
|
|
1073
|
+
# Contrail climate forcing
|
|
1074
|
+
"mean_rf_sw": (
|
|
1075
|
+
area_mean_contrail_property(
|
|
1076
|
+
contrails.filter(is_at_t_end)["rf_sw"],
|
|
1077
|
+
contrails.filter(is_at_t_end)["segment_length"],
|
|
1078
|
+
contrails.filter(is_at_t_end)["width"],
|
|
1079
|
+
domain_area,
|
|
1080
|
+
)
|
|
1081
|
+
if np.any(is_at_t_end)
|
|
1082
|
+
else np.nan
|
|
1083
|
+
),
|
|
1084
|
+
"mean_rf_lw": (
|
|
1085
|
+
area_mean_contrail_property(
|
|
1086
|
+
contrails.filter(is_at_t_end)["rf_lw"],
|
|
1087
|
+
contrails.filter(is_at_t_end)["segment_length"],
|
|
1088
|
+
contrails.filter(is_at_t_end)["width"],
|
|
1089
|
+
domain_area,
|
|
1090
|
+
)
|
|
1091
|
+
if np.any(is_at_t_end)
|
|
1092
|
+
else np.nan
|
|
1093
|
+
),
|
|
1094
|
+
"mean_rf_net": (
|
|
1095
|
+
area_mean_contrail_property(
|
|
1096
|
+
contrails.filter(is_at_t_end)["rf_net"],
|
|
1097
|
+
contrails.filter(is_at_t_end)["segment_length"],
|
|
1098
|
+
contrails.filter(is_at_t_end)["width"],
|
|
1099
|
+
domain_area,
|
|
1100
|
+
)
|
|
1101
|
+
if np.any(is_at_t_end)
|
|
1102
|
+
else np.nan
|
|
1103
|
+
),
|
|
1104
|
+
"total_contrail_ef": np.nansum(contrails["ef"]) if np.any(is_at_t_end) else np.nan,
|
|
1105
|
+
# Meteorology statistics
|
|
1106
|
+
"issr_percentage_coverage": (
|
|
1107
|
+
(met_stats["issr_percentage_coverage"]) if met is not None else np.nan
|
|
1108
|
+
),
|
|
1109
|
+
"mean_rhi_in_issr": met_stats["mean_rhi_in_issr"] if met is not None else np.nan,
|
|
1110
|
+
"contrail_cirrus_percentage_coverage": (
|
|
1111
|
+
(met_stats["contrail_cirrus_percentage_coverage"]) if met is not None else np.nan
|
|
1112
|
+
),
|
|
1113
|
+
"contrail_cirrus_clear_sky_percentage_coverage": (
|
|
1114
|
+
(met_stats["contrail_cirrus_clear_sky_percentage_coverage"])
|
|
1115
|
+
if met is not None
|
|
1116
|
+
else np.nan
|
|
1117
|
+
),
|
|
1118
|
+
"natural_cirrus_percentage_coverage": (
|
|
1119
|
+
(met_stats["natural_cirrus_percentage_coverage"]) if met is not None else np.nan
|
|
1120
|
+
),
|
|
1121
|
+
"cloud_contrail_overlap_percentage": (
|
|
1122
|
+
percentage_cloud_contrail_overlap(
|
|
1123
|
+
met_stats["contrail_cirrus_percentage_coverage"],
|
|
1124
|
+
met_stats["contrail_cirrus_clear_sky_percentage_coverage"],
|
|
1125
|
+
)
|
|
1126
|
+
if met is not None
|
|
1127
|
+
else np.nan
|
|
1128
|
+
),
|
|
1129
|
+
# Radiation statistics
|
|
1130
|
+
"mean_sdr_domain": rad_stats["mean_sdr_domain"] if rad is not None else np.nan,
|
|
1131
|
+
"mean_sdr_at_contrail_wypts": (
|
|
1132
|
+
np.nanmean(contrails.filter(is_at_t_end)["sdr"]) if np.any(is_at_t_end) else np.nan
|
|
1133
|
+
),
|
|
1134
|
+
"mean_rsr_domain": rad_stats["mean_rsr_domain"] if rad is not None else np.nan,
|
|
1135
|
+
"mean_rsr_at_contrail_wypts": (
|
|
1136
|
+
np.nanmean(contrails.filter(is_at_t_end)["rsr"]) if np.any(is_at_t_end) else np.nan
|
|
1137
|
+
),
|
|
1138
|
+
"mean_olr_domain": rad_stats["mean_olr_domain"] if rad is not None else np.nan,
|
|
1139
|
+
"mean_olr_at_contrail_wypts": (
|
|
1140
|
+
np.nanmean(contrails.filter(is_at_t_end)["olr"]) if np.any(is_at_t_end) else np.nan
|
|
1141
|
+
),
|
|
1142
|
+
"mean_albedo_at_contrail_wypts": (
|
|
1143
|
+
np.nanmean(
|
|
1144
|
+
albedo(contrails.filter(is_at_t_end)["sdr"], contrails.filter(is_at_t_end)["rsr"])
|
|
1145
|
+
)
|
|
1146
|
+
if np.any(is_at_t_end)
|
|
1147
|
+
else np.nan
|
|
1148
|
+
),
|
|
1149
|
+
}
|
|
1150
|
+
return pd.Series(stats_t)
|
|
1151
|
+
|
|
1152
|
+
|
|
1153
|
+
def meteorological_time_slice_statistics(
|
|
1154
|
+
time: np.datetime64 | pd.Timestamp,
|
|
1155
|
+
contrails: GeoVectorDataset,
|
|
1156
|
+
met: MetDataset,
|
|
1157
|
+
humidity_scaling: HumidityScaling,
|
|
1158
|
+
cirrus_coverage: MetDataset | None = None,
|
|
1159
|
+
) -> pd.Series:
|
|
1160
|
+
"""
|
|
1161
|
+
Calculate meteorological statistics in the domain provided.
|
|
1162
|
+
|
|
1163
|
+
Parameters
|
|
1164
|
+
----------
|
|
1165
|
+
time : np.datetime64 | pd.Timestamp
|
|
1166
|
+
Time when the meteorological statistics is computed.
|
|
1167
|
+
contrails : GeoVectorDataset
|
|
1168
|
+
Contrail waypoints containing `tau_contrail`.
|
|
1169
|
+
met : MetDataset
|
|
1170
|
+
Pressure level dataset containing 'air_temperature', 'specific_humidity',
|
|
1171
|
+
'specific_cloud_ice_water_content', and 'geopotential'
|
|
1172
|
+
humidity_scaling : HumidityScaling
|
|
1173
|
+
Humidity scaling methodology.
|
|
1174
|
+
See :attr:`CocipParams.humidity_scaling`
|
|
1175
|
+
cirrus_coverage : MetDataset
|
|
1176
|
+
Single level dataset containing the contrail and natural cirrus coverage, including
|
|
1177
|
+
`cc_contrails_clear_sky`, `cc_natural_cirrus`, `cc_contrails`
|
|
1178
|
+
|
|
1179
|
+
Returns
|
|
1180
|
+
-------
|
|
1181
|
+
pd.Series
|
|
1182
|
+
Mean ISSR characteristics, and the percentage of contrail and natural cirrus coverage in
|
|
1183
|
+
domain area.
|
|
1184
|
+
"""
|
|
1185
|
+
# Ensure vars
|
|
1186
|
+
met.ensure_vars(
|
|
1187
|
+
("air_temperature", "specific_humidity", "specific_cloud_ice_water_content", "geopotential")
|
|
1188
|
+
)
|
|
1189
|
+
|
|
1190
|
+
# ISSR: Volume of airspace with RHi > 100% between FL300 and FL450
|
|
1191
|
+
met_cruise = MetDataset(met.data.sel(level=slice(150, 300)))
|
|
1192
|
+
rhi = humidity_scaling.eval(met_cruise)["rhi"].data
|
|
1193
|
+
|
|
1194
|
+
try:
|
|
1195
|
+
# If the given time is already in the dataset, select the time slice
|
|
1196
|
+
i = rhi.get_index("time").get_loc(time)
|
|
1197
|
+
except KeyError:
|
|
1198
|
+
rhi = rhi.interp(time=time)
|
|
1199
|
+
else:
|
|
1200
|
+
rhi = rhi.isel(time=i)
|
|
1201
|
+
|
|
1202
|
+
is_issr = rhi > 1.0
|
|
1203
|
+
|
|
1204
|
+
# Cirrus in a longitude-latitude grid
|
|
1205
|
+
if cirrus_coverage is None:
|
|
1206
|
+
cirrus_coverage = cirrus_coverage_single_level(time, met, contrails)
|
|
1207
|
+
|
|
1208
|
+
# Calculate statistics
|
|
1209
|
+
area = geo.grid_surface_area(met["longitude"].values, met["latitude"].values)
|
|
1210
|
+
weights = area / np.nansum(area)
|
|
1211
|
+
|
|
1212
|
+
stats = {
|
|
1213
|
+
"issr_percentage_coverage": (
|
|
1214
|
+
np.nansum(is_issr * weights) / (np.nansum(weights) * len(rhi.level))
|
|
1215
|
+
)
|
|
1216
|
+
* 100,
|
|
1217
|
+
"mean_rhi_in_issr": np.nanmean(rhi.values[is_issr.values]),
|
|
1218
|
+
"contrail_cirrus_percentage_coverage": (
|
|
1219
|
+
np.nansum(area * cirrus_coverage["contrails"].data) / np.nansum(area)
|
|
1220
|
+
)
|
|
1221
|
+
* 100,
|
|
1222
|
+
"contrail_cirrus_clear_sky_percentage_coverage": (
|
|
1223
|
+
np.nansum(area * cirrus_coverage["contrails_clear_sky"].data) / np.nansum(area)
|
|
1224
|
+
)
|
|
1225
|
+
* 100,
|
|
1226
|
+
"natural_cirrus_percentage_coverage": (
|
|
1227
|
+
np.nansum(area * cirrus_coverage["natural_cirrus"].data) / np.nansum(area)
|
|
1228
|
+
)
|
|
1229
|
+
* 100,
|
|
1230
|
+
}
|
|
1231
|
+
return pd.Series(stats)
|
|
1232
|
+
|
|
1233
|
+
|
|
1234
|
+
def radiation_time_slice_statistics(
|
|
1235
|
+
rad: MetDataset, time: np.datetime64 | pd.Timestamp
|
|
1236
|
+
) -> pd.Series:
|
|
1237
|
+
"""
|
|
1238
|
+
Calculate radiation statistics in the domain provided.
|
|
1239
|
+
|
|
1240
|
+
Parameters
|
|
1241
|
+
----------
|
|
1242
|
+
rad : MetDataset
|
|
1243
|
+
Single level dataset containing the `sdr`, `rsr` and `olr`.
|
|
1244
|
+
time : np.datetime64 | pd.Timestamp
|
|
1245
|
+
Time when the radiation statistics is computed.
|
|
1246
|
+
|
|
1247
|
+
Returns
|
|
1248
|
+
-------
|
|
1249
|
+
pd.Series
|
|
1250
|
+
Mean SDR, RSR and OLR in domain area.
|
|
1251
|
+
"""
|
|
1252
|
+
rad.ensure_vars(("sdr", "rsr", "olr"))
|
|
1253
|
+
surface_area = geo.grid_surface_area(rad["longitude"].values, rad["latitude"].values)
|
|
1254
|
+
weights = surface_area.values / np.nansum(surface_area)
|
|
1255
|
+
stats = {
|
|
1256
|
+
"mean_sdr_domain": np.nansum(
|
|
1257
|
+
np.squeeze(rad["sdr"].data.interp(time=time).values) * weights
|
|
1258
|
+
),
|
|
1259
|
+
"mean_rsr_domain": np.nansum(
|
|
1260
|
+
np.squeeze(rad["rsr"].data.interp(time=time).values) * weights
|
|
1261
|
+
),
|
|
1262
|
+
"mean_olr_domain": np.nansum(
|
|
1263
|
+
np.squeeze(rad["olr"].data.interp(time=time).values) * weights
|
|
1264
|
+
),
|
|
1265
|
+
}
|
|
1266
|
+
return pd.Series(stats)
|
|
1267
|
+
|
|
1268
|
+
|
|
1269
|
+
def area_mean_ice_water_path(
|
|
1270
|
+
iwc: npt.NDArray[np.floating],
|
|
1271
|
+
plume_mass_per_m: npt.NDArray[np.floating],
|
|
1272
|
+
segment_length: npt.NDArray[np.floating],
|
|
1273
|
+
domain_area: float,
|
|
1274
|
+
) -> float:
|
|
1275
|
+
"""
|
|
1276
|
+
Calculate area-mean contrail ice water path.
|
|
1277
|
+
|
|
1278
|
+
Ice water path (IWC) is the contrail ice mass divided by the domain area of interest.
|
|
1279
|
+
|
|
1280
|
+
Parameters
|
|
1281
|
+
----------
|
|
1282
|
+
iwc : npt.NDArray[np.floating]
|
|
1283
|
+
Contrail ice water content, i.e., contrail ice mass per kg of
|
|
1284
|
+
air, [:math:`kg_{H_{2}O}/kg_{air}`]
|
|
1285
|
+
plume_mass_per_m : npt.NDArray[np.floating]
|
|
1286
|
+
Contrail plume mass per unit length, [:math:`kg m^{-1}`]
|
|
1287
|
+
segment_length : npt.NDArray[np.floating]
|
|
1288
|
+
Contrail segment length for each waypoint, [:math:`m`]
|
|
1289
|
+
domain_area : float
|
|
1290
|
+
Domain surface area, [:math:`m^{2}`]
|
|
1291
|
+
|
|
1292
|
+
Returns
|
|
1293
|
+
-------
|
|
1294
|
+
float
|
|
1295
|
+
Mean contrail ice water path, [:math:`kg m^{-2}`]
|
|
1296
|
+
"""
|
|
1297
|
+
return np.nansum(iwc * plume_mass_per_m * segment_length) / domain_area
|
|
1298
|
+
|
|
1299
|
+
|
|
1300
|
+
def area_mean_ice_particle_radius(
|
|
1301
|
+
r_ice_vol: npt.NDArray[np.floating],
|
|
1302
|
+
n_ice_per_m: npt.NDArray[np.floating],
|
|
1303
|
+
segment_length: npt.NDArray[np.floating],
|
|
1304
|
+
) -> float:
|
|
1305
|
+
r"""
|
|
1306
|
+
Calculate the area-mean contrail ice particle radius.
|
|
1307
|
+
|
|
1308
|
+
Parameters
|
|
1309
|
+
----------
|
|
1310
|
+
r_ice_vol : npt.NDArray[np.floating]
|
|
1311
|
+
Ice particle volume mean radius for each waypoint, [:math:`m`]
|
|
1312
|
+
n_ice_per_m : npt.NDArray[np.floating]
|
|
1313
|
+
Number of ice particles per distance for each waypoint, [:math:`m^{-1}`]
|
|
1314
|
+
segment_length : npt.NDArray[np.floating]
|
|
1315
|
+
Contrail segment length for each waypoint, [:math:`m`]
|
|
1316
|
+
|
|
1317
|
+
Returns
|
|
1318
|
+
-------
|
|
1319
|
+
float
|
|
1320
|
+
Area-mean contrail ice particle radius `r_area`, [:math:`\mu m`]
|
|
1321
|
+
|
|
1322
|
+
Notes
|
|
1323
|
+
-----
|
|
1324
|
+
- Re-arranged from `tot_ice_cross_sec_area` = `tot_n_ice_particles` * (np.pi * `r_ice_vol`**2)
|
|
1325
|
+
- Assumes that the contrail ice crystals are spherical.
|
|
1326
|
+
"""
|
|
1327
|
+
tot_ice_cross_sec_area = _total_ice_particle_cross_sectional_area(
|
|
1328
|
+
r_ice_vol, n_ice_per_m, segment_length
|
|
1329
|
+
)
|
|
1330
|
+
tot_n_ice_particles = _total_ice_particle_number(n_ice_per_m, segment_length)
|
|
1331
|
+
return (tot_ice_cross_sec_area / (np.pi * tot_n_ice_particles)) ** (1 / 2) * 10**6
|
|
1332
|
+
|
|
1333
|
+
|
|
1334
|
+
def volume_mean_ice_particle_radius(
|
|
1335
|
+
r_ice_vol: npt.NDArray[np.floating],
|
|
1336
|
+
n_ice_per_m: npt.NDArray[np.floating],
|
|
1337
|
+
segment_length: npt.NDArray[np.floating],
|
|
1338
|
+
) -> float:
|
|
1339
|
+
r"""
|
|
1340
|
+
Calculate the volume-mean contrail ice particle radius.
|
|
1341
|
+
|
|
1342
|
+
Parameters
|
|
1343
|
+
----------
|
|
1344
|
+
r_ice_vol : npt.NDArray[np.floating]
|
|
1345
|
+
Ice particle volume mean radius for each waypoint, [:math:`m`]
|
|
1346
|
+
n_ice_per_m : npt.NDArray[np.floating]
|
|
1347
|
+
Number of ice particles per distance for each waypoint, [:math:`m^{-1}`]
|
|
1348
|
+
segment_length : npt.NDArray[np.floating]
|
|
1349
|
+
Contrail segment length for each waypoint, [:math:`m`]
|
|
1350
|
+
|
|
1351
|
+
Returns
|
|
1352
|
+
-------
|
|
1353
|
+
float
|
|
1354
|
+
Volume-mean contrail ice particle radius `r_vol`, [:math:`\mu m`]
|
|
1355
|
+
|
|
1356
|
+
Notes
|
|
1357
|
+
-----
|
|
1358
|
+
- Re-arranged from `tot_ice_vol` = `tot_n_ice_particles` * (4 / 3 * np.pi * `r_ice_vol`**3)
|
|
1359
|
+
- Assumes that the contrail ice crystals are spherical.
|
|
1360
|
+
"""
|
|
1361
|
+
tot_ice_vol = _total_ice_particle_volume(r_ice_vol, n_ice_per_m, segment_length)
|
|
1362
|
+
tot_n_ice_particles = _total_ice_particle_number(n_ice_per_m, segment_length)
|
|
1363
|
+
return (tot_ice_vol / ((4 / 3) * np.pi * tot_n_ice_particles)) ** (1 / 3) * 10**6
|
|
1364
|
+
|
|
1365
|
+
|
|
1366
|
+
def mean_ice_particle_effective_radius(
|
|
1367
|
+
r_ice_vol: npt.NDArray[np.floating],
|
|
1368
|
+
n_ice_per_m: npt.NDArray[np.floating],
|
|
1369
|
+
segment_length: npt.NDArray[np.floating],
|
|
1370
|
+
) -> float:
|
|
1371
|
+
r"""
|
|
1372
|
+
Calculate the mean contrail ice particle effective radius.
|
|
1373
|
+
|
|
1374
|
+
Parameters
|
|
1375
|
+
----------
|
|
1376
|
+
r_ice_vol : npt.NDArray[np.floating]
|
|
1377
|
+
Ice particle volume mean radius for each waypoint, [:math:`m`]
|
|
1378
|
+
n_ice_per_m : npt.NDArray[np.floating]
|
|
1379
|
+
Number of ice particles per distance for each waypoint, [:math:`m^{-1}`]
|
|
1380
|
+
segment_length : npt.NDArray[np.floating]
|
|
1381
|
+
Contrail segment length for each waypoint, [:math:`m`]
|
|
1382
|
+
|
|
1383
|
+
Returns
|
|
1384
|
+
-------
|
|
1385
|
+
float
|
|
1386
|
+
Mean contrail ice particle effective radius `r_eff`, [:math:`\mu m`]
|
|
1387
|
+
|
|
1388
|
+
Notes
|
|
1389
|
+
-----
|
|
1390
|
+
- `r_eff` is the ratio of the particle volume to particle projected area.
|
|
1391
|
+
- `r_eff` = (3 / 4) * (`tot_ice_vol` / `tot_ice_cross_sec_area`)
|
|
1392
|
+
- See Eq. (62) of :cite:`schumannContrailCirrusPrediction2012`.
|
|
1393
|
+
"""
|
|
1394
|
+
tot_ice_vol = _total_ice_particle_volume(r_ice_vol, n_ice_per_m, segment_length)
|
|
1395
|
+
tot_ice_cross_sec_area = _total_ice_particle_cross_sectional_area(
|
|
1396
|
+
r_ice_vol, n_ice_per_m, segment_length
|
|
1397
|
+
)
|
|
1398
|
+
return (3 / 4) * (tot_ice_vol / tot_ice_cross_sec_area) * 10**6
|
|
1399
|
+
|
|
1400
|
+
|
|
1401
|
+
def _total_ice_particle_cross_sectional_area(
|
|
1402
|
+
r_ice_vol: npt.NDArray[np.floating],
|
|
1403
|
+
n_ice_per_m: npt.NDArray[np.floating],
|
|
1404
|
+
segment_length: npt.NDArray[np.floating],
|
|
1405
|
+
) -> float:
|
|
1406
|
+
"""
|
|
1407
|
+
Calculate total contrail ice particle cross-sectional area.
|
|
1408
|
+
|
|
1409
|
+
Parameters
|
|
1410
|
+
----------
|
|
1411
|
+
r_ice_vol : npt.NDArray[np.floating]
|
|
1412
|
+
Ice particle volume mean radius for each waypoint, [:math:`m`]
|
|
1413
|
+
n_ice_per_m : npt.NDArray[np.floating]
|
|
1414
|
+
Number of ice particles per distance for each waypoint, [:math:`m^{-1}`]
|
|
1415
|
+
segment_length : npt.NDArray[np.floating]
|
|
1416
|
+
Contrail segment length for each waypoint, [:math:`m`]
|
|
1417
|
+
|
|
1418
|
+
Returns
|
|
1419
|
+
-------
|
|
1420
|
+
float
|
|
1421
|
+
Total ice particle cross-sectional area from all contrail waypoints, [:math:`m^{2}`]
|
|
1422
|
+
"""
|
|
1423
|
+
ice_cross_sec_area = 0.9 * np.pi * r_ice_vol**2
|
|
1424
|
+
return np.nansum(ice_cross_sec_area * n_ice_per_m * segment_length)
|
|
1425
|
+
|
|
1426
|
+
|
|
1427
|
+
def _total_ice_particle_volume(
|
|
1428
|
+
r_ice_vol: npt.NDArray[np.floating],
|
|
1429
|
+
n_ice_per_m: npt.NDArray[np.floating],
|
|
1430
|
+
segment_length: npt.NDArray[np.floating],
|
|
1431
|
+
) -> float:
|
|
1432
|
+
"""
|
|
1433
|
+
Calculate total contrail ice particle volume.
|
|
1434
|
+
|
|
1435
|
+
Parameters
|
|
1436
|
+
----------
|
|
1437
|
+
r_ice_vol : npt.NDArray[np.floating]
|
|
1438
|
+
Ice particle volume mean radius for each waypoint, [:math:`m`]
|
|
1439
|
+
n_ice_per_m : npt.NDArray[np.floating]
|
|
1440
|
+
Number of ice particles per distance for each waypoint, [:math:`m^{-1}`]
|
|
1441
|
+
segment_length : npt.NDArray[np.floating]
|
|
1442
|
+
Contrail segment length for each waypoint, [:math:`m`]
|
|
1443
|
+
|
|
1444
|
+
Returns
|
|
1445
|
+
-------
|
|
1446
|
+
float
|
|
1447
|
+
Total ice particle volume from all contrail waypoints, [:math:`m^{2}`]
|
|
1448
|
+
"""
|
|
1449
|
+
ice_vol = (4 / 3) * np.pi * r_ice_vol**3
|
|
1450
|
+
return np.nansum(ice_vol * n_ice_per_m * segment_length)
|
|
1451
|
+
|
|
1452
|
+
|
|
1453
|
+
def _total_ice_particle_number(
|
|
1454
|
+
n_ice_per_m: npt.NDArray[np.floating], segment_length: npt.NDArray[np.floating]
|
|
1455
|
+
) -> float:
|
|
1456
|
+
"""
|
|
1457
|
+
Calculate total number of contrail ice particles.
|
|
1458
|
+
|
|
1459
|
+
Parameters
|
|
1460
|
+
----------
|
|
1461
|
+
n_ice_per_m : npt.NDArray[np.floating]
|
|
1462
|
+
Number of ice particles per distance for each waypoint, [:math:`m^{-1}`]
|
|
1463
|
+
segment_length : npt.NDArray[np.floating]
|
|
1464
|
+
Contrail segment length for each waypoint, [:math:`m`]
|
|
1465
|
+
|
|
1466
|
+
Returns
|
|
1467
|
+
-------
|
|
1468
|
+
float
|
|
1469
|
+
Total number of ice particles from all contrail waypoints.
|
|
1470
|
+
"""
|
|
1471
|
+
return np.nansum(n_ice_per_m * segment_length)
|
|
1472
|
+
|
|
1473
|
+
|
|
1474
|
+
def area_mean_contrail_property(
|
|
1475
|
+
contrail_property: npt.NDArray[np.floating],
|
|
1476
|
+
segment_length: npt.NDArray[np.floating],
|
|
1477
|
+
width: npt.NDArray[np.floating],
|
|
1478
|
+
domain_area: float,
|
|
1479
|
+
) -> float:
|
|
1480
|
+
"""
|
|
1481
|
+
Calculate area mean contrail property.
|
|
1482
|
+
|
|
1483
|
+
Used to calculate the area mean `tau_contrail`, `tau_cirrus`, `sdr`, `rsr`, `olr`, `rf_sw`,
|
|
1484
|
+
`rf_lw` and `rf_net`.
|
|
1485
|
+
|
|
1486
|
+
Parameters
|
|
1487
|
+
----------
|
|
1488
|
+
contrail_property : npt.NDArray[np.floating]
|
|
1489
|
+
Selected contrail property for each waypoint
|
|
1490
|
+
segment_length : npt.NDArray[np.floating]
|
|
1491
|
+
Contrail segment length for each waypoint, [:math:`m`]
|
|
1492
|
+
width : npt.NDArray[np.floating]
|
|
1493
|
+
Contrail width for each waypoint, [:math:`m`]
|
|
1494
|
+
domain_area : float
|
|
1495
|
+
Domain surface area, [:math:`m^{2}`]
|
|
1496
|
+
|
|
1497
|
+
Returns
|
|
1498
|
+
-------
|
|
1499
|
+
float
|
|
1500
|
+
Area mean contrail property
|
|
1501
|
+
"""
|
|
1502
|
+
return np.nansum(contrail_property * segment_length * width) / domain_area
|
|
1503
|
+
|
|
1504
|
+
|
|
1505
|
+
def percentage_cloud_contrail_overlap(
|
|
1506
|
+
contrail_cover: float | np.ndarray, contrail_cover_clear_sky: float | np.ndarray
|
|
1507
|
+
) -> float | np.ndarray:
|
|
1508
|
+
"""
|
|
1509
|
+
Calculate the percentage area of cloud-contrail overlap.
|
|
1510
|
+
|
|
1511
|
+
Parameters
|
|
1512
|
+
----------
|
|
1513
|
+
contrail_cover : float | np.ndarray
|
|
1514
|
+
Percentage of contrail cirrus cover without overlap with natural cirrus.
|
|
1515
|
+
See `cirrus_coverage_single_level` function.
|
|
1516
|
+
contrail_cover_clear_sky : float | np.ndarray
|
|
1517
|
+
Percentage of contrail cirrus cover in clear sky conditions.
|
|
1518
|
+
See `cirrus_coverage_single_level` function.
|
|
1519
|
+
|
|
1520
|
+
Returns
|
|
1521
|
+
-------
|
|
1522
|
+
float | np.ndarray
|
|
1523
|
+
Percentage of cloud-contrail overlap
|
|
1524
|
+
"""
|
|
1525
|
+
return np.where(
|
|
1526
|
+
contrail_cover_clear_sky > 0,
|
|
1527
|
+
100 - (contrail_cover / contrail_cover_clear_sky * 100),
|
|
1528
|
+
0,
|
|
1529
|
+
)
|
|
1530
|
+
|
|
1531
|
+
|
|
1532
|
+
# ---------------------------------------
|
|
1533
|
+
# High resolution grid: contrail segments
|
|
1534
|
+
# ---------------------------------------
|
|
1535
|
+
|
|
1536
|
+
|
|
1537
|
+
def contrails_to_hi_res_grid(
|
|
1538
|
+
time: pd.Timestamp | np.datetime64,
|
|
1539
|
+
contrails_t: GeoVectorDataset,
|
|
1540
|
+
*,
|
|
1541
|
+
var_name: str,
|
|
1542
|
+
spatial_bbox: tuple[float, float, float, float] = (-180.0, -90.0, 180.0, 90.0),
|
|
1543
|
+
spatial_grid_res: float = 0.05,
|
|
1544
|
+
) -> xr.DataArray:
|
|
1545
|
+
r"""
|
|
1546
|
+
Aggregate contrail segments to a high-resolution longitude-latitude grid.
|
|
1547
|
+
|
|
1548
|
+
Parameters
|
|
1549
|
+
----------
|
|
1550
|
+
time : pd.Timestamp | np.datetime64
|
|
1551
|
+
UTC time of interest.
|
|
1552
|
+
contrails_t : GeoVectorDataset
|
|
1553
|
+
All contrail waypoint outputs at `time`.
|
|
1554
|
+
var_name : str
|
|
1555
|
+
Contrail property for aggregation, where `var_name` must be included in `contrail_segment`.
|
|
1556
|
+
For example, `tau_contrail`, `rf_sw`, `rf_lw`, and `rf_net`
|
|
1557
|
+
spatial_bbox : tuple[float, float, float, float]
|
|
1558
|
+
Spatial bounding box, `(lon_min, lat_min, lon_max, lat_max)`, [:math:`\deg`]
|
|
1559
|
+
spatial_grid_res : float
|
|
1560
|
+
Spatial grid resolution, [:math:`\deg`]
|
|
1561
|
+
|
|
1562
|
+
Returns
|
|
1563
|
+
-------
|
|
1564
|
+
xr.DataArray
|
|
1565
|
+
Contrail segments and their properties aggregated to a longitude-latitude grid.
|
|
1566
|
+
"""
|
|
1567
|
+
# Ensure the required columns are included in `contrails_t`
|
|
1568
|
+
cols_req = [
|
|
1569
|
+
"flight_id",
|
|
1570
|
+
"waypoint",
|
|
1571
|
+
"longitude",
|
|
1572
|
+
"latitude",
|
|
1573
|
+
"altitude",
|
|
1574
|
+
"time",
|
|
1575
|
+
"sin_a",
|
|
1576
|
+
"cos_a",
|
|
1577
|
+
"width",
|
|
1578
|
+
var_name,
|
|
1579
|
+
]
|
|
1580
|
+
contrails_t.ensure_vars(cols_req)
|
|
1581
|
+
|
|
1582
|
+
# Ensure that the times in `contrails_t` are the same.
|
|
1583
|
+
is_in_time = contrails_t["time"] == time
|
|
1584
|
+
if not np.all(is_in_time):
|
|
1585
|
+
warnings.warn(
|
|
1586
|
+
f"Contrails have inconsistent times. Waypoints that are not in {time} are removed."
|
|
1587
|
+
)
|
|
1588
|
+
contrails_t = contrails_t.filter(is_in_time)
|
|
1589
|
+
|
|
1590
|
+
main_grid = _initialise_longitude_latitude_grid(spatial_bbox, spatial_grid_res)
|
|
1591
|
+
|
|
1592
|
+
# Contrail head and tails: continuous segments only
|
|
1593
|
+
heads_t = contrails_t.dataframe
|
|
1594
|
+
heads_t = heads_t.sort_values(["flight_id", "waypoint"])
|
|
1595
|
+
tails_t = heads_t.shift(periods=-1)
|
|
1596
|
+
|
|
1597
|
+
is_continuous = heads_t["continuous"]
|
|
1598
|
+
heads_t = heads_t[is_continuous].copy()
|
|
1599
|
+
tails_t = tails_t[is_continuous].copy()
|
|
1600
|
+
tails_t["waypoint"] = tails_t["waypoint"].astype("int")
|
|
1601
|
+
|
|
1602
|
+
heads_t = heads_t.set_index(["flight_id", "waypoint"], drop=False)
|
|
1603
|
+
tails_t.index = heads_t.index
|
|
1604
|
+
|
|
1605
|
+
# Aggregate contrail segments to a high resolution longitude-latitude grid
|
|
1606
|
+
try:
|
|
1607
|
+
from tqdm.auto import tqdm
|
|
1608
|
+
except ModuleNotFoundError as exc:
|
|
1609
|
+
dependencies.raise_module_not_found_error(
|
|
1610
|
+
name="contrails_to_hi_res_grid function",
|
|
1611
|
+
package_name="tqdm",
|
|
1612
|
+
module_not_found_error=exc,
|
|
1613
|
+
)
|
|
1614
|
+
|
|
1615
|
+
for i in tqdm(heads_t.index):
|
|
1616
|
+
contrail_segment = GeoVectorDataset(
|
|
1617
|
+
pd.concat([heads_t[cols_req].loc[i], tails_t[cols_req].loc[i]], axis=1).T, copy=True
|
|
1618
|
+
)
|
|
1619
|
+
|
|
1620
|
+
segment_grid = segment_property_to_hi_res_grid(
|
|
1621
|
+
contrail_segment, var_name=var_name, spatial_grid_res=spatial_grid_res
|
|
1622
|
+
)
|
|
1623
|
+
main_grid = _add_segment_to_main_grid(main_grid, segment_grid)
|
|
1624
|
+
|
|
1625
|
+
return main_grid
|
|
1626
|
+
|
|
1627
|
+
|
|
1628
|
+
def _initialise_longitude_latitude_grid(
|
|
1629
|
+
spatial_bbox: tuple[float, float, float, float] = (-180.0, -90.0, 180.0, 90.0),
|
|
1630
|
+
spatial_grid_res: float = 0.05,
|
|
1631
|
+
) -> xr.DataArray:
|
|
1632
|
+
r"""
|
|
1633
|
+
Create longitude-latitude grid of specified coordinates and spatial resolution.
|
|
1634
|
+
|
|
1635
|
+
Parameters
|
|
1636
|
+
----------
|
|
1637
|
+
spatial_bbox : tuple[float, float, float, float]
|
|
1638
|
+
Spatial bounding box, `(lon_min, lat_min, lon_max, lat_max)`, [:math:`\deg`]
|
|
1639
|
+
spatial_grid_res : float
|
|
1640
|
+
Spatial grid resolution, [:math:`\deg`]
|
|
1641
|
+
|
|
1642
|
+
Returns
|
|
1643
|
+
-------
|
|
1644
|
+
xr.DataArray
|
|
1645
|
+
Longitude-latitude grid of specified coordinates and spatial resolution, filled with zeros.
|
|
1646
|
+
|
|
1647
|
+
Notes
|
|
1648
|
+
-----
|
|
1649
|
+
This empty grid is used to store the aggregated contrail properties of the individual
|
|
1650
|
+
contrail segments, such as the gridded contrail optical depth and radiative forcing.
|
|
1651
|
+
"""
|
|
1652
|
+
lon_coords = np.arange(spatial_bbox[0], spatial_bbox[2] + spatial_grid_res, spatial_grid_res)
|
|
1653
|
+
lat_coords = np.arange(spatial_bbox[1], spatial_bbox[3] + spatial_grid_res, spatial_grid_res)
|
|
1654
|
+
return xr.DataArray(
|
|
1655
|
+
np.zeros((len(lon_coords), len(lat_coords))),
|
|
1656
|
+
dims=["longitude", "latitude"],
|
|
1657
|
+
coords={"longitude": lon_coords, "latitude": lat_coords},
|
|
1658
|
+
)
|
|
1659
|
+
|
|
1660
|
+
|
|
1661
|
+
def segment_property_to_hi_res_grid(
|
|
1662
|
+
contrail_segment: GeoVectorDataset,
|
|
1663
|
+
*,
|
|
1664
|
+
var_name: str,
|
|
1665
|
+
spatial_grid_res: float = 0.05,
|
|
1666
|
+
) -> xr.DataArray:
|
|
1667
|
+
r"""
|
|
1668
|
+
Convert the contrail segment property to a high-resolution longitude-latitude grid.
|
|
1669
|
+
|
|
1670
|
+
Parameters
|
|
1671
|
+
----------
|
|
1672
|
+
contrail_segment : GeoVectorDataset
|
|
1673
|
+
Contrail segment waypoints (head and tail).
|
|
1674
|
+
var_name : str
|
|
1675
|
+
Contrail property of interest, where `var_name` must be included in `contrail_segment`.
|
|
1676
|
+
For example, `tau_contrail`, `rf_sw`, `rf_lw`, and `rf_net`
|
|
1677
|
+
spatial_grid_res : float
|
|
1678
|
+
Spatial grid resolution, [:math:`\deg`]
|
|
1679
|
+
|
|
1680
|
+
Returns
|
|
1681
|
+
-------
|
|
1682
|
+
xr.DataArray
|
|
1683
|
+
Contrail segment dimension and property projected to a longitude-latitude grid.
|
|
1684
|
+
|
|
1685
|
+
Notes
|
|
1686
|
+
-----
|
|
1687
|
+
- See Appendix A11 and A12 of :cite:`schumannContrailCirrusPrediction2012`.
|
|
1688
|
+
"""
|
|
1689
|
+
# Ensure that `contrail_segment` contains the required variables
|
|
1690
|
+
contrail_segment.ensure_vars(("sin_a", "cos_a", "width", var_name))
|
|
1691
|
+
|
|
1692
|
+
# Ensure that `contrail_segment` only contains two waypoints and have the same time.
|
|
1693
|
+
assert len(contrail_segment) == 2
|
|
1694
|
+
assert contrail_segment["time"][0] == contrail_segment["time"][1]
|
|
1695
|
+
|
|
1696
|
+
# Calculate contrail edges
|
|
1697
|
+
(
|
|
1698
|
+
contrail_segment["lon_edge_l"],
|
|
1699
|
+
contrail_segment["lat_edge_l"],
|
|
1700
|
+
contrail_segment["lon_edge_r"],
|
|
1701
|
+
contrail_segment["lat_edge_r"],
|
|
1702
|
+
) = contrail_edges(
|
|
1703
|
+
contrail_segment["longitude"],
|
|
1704
|
+
contrail_segment["latitude"],
|
|
1705
|
+
contrail_segment["sin_a"],
|
|
1706
|
+
contrail_segment["cos_a"],
|
|
1707
|
+
contrail_segment["width"],
|
|
1708
|
+
)
|
|
1709
|
+
|
|
1710
|
+
# Initialise contrail segment grid with spatial domain that covers the contrail area.
|
|
1711
|
+
lon_edges = np.concatenate(
|
|
1712
|
+
[contrail_segment["lon_edge_l"], contrail_segment["lon_edge_r"]], axis=0
|
|
1713
|
+
)
|
|
1714
|
+
lat_edges = np.concatenate(
|
|
1715
|
+
[contrail_segment["lat_edge_l"], contrail_segment["lat_edge_r"]], axis=0
|
|
1716
|
+
)
|
|
1717
|
+
spatial_bbox = geo.spatial_bounding_box(lon_edges, lat_edges, buffer=0.5)
|
|
1718
|
+
segment_grid = _initialise_longitude_latitude_grid(spatial_bbox, spatial_grid_res)
|
|
1719
|
+
|
|
1720
|
+
# Calculate gridded contrail segment properties
|
|
1721
|
+
weights = _pixel_weights(contrail_segment, segment_grid)
|
|
1722
|
+
dist_perpendicular = _segment_perpendicular_distance_to_pixels(contrail_segment, weights)
|
|
1723
|
+
plume_concentration = _gaussian_plume_concentration(
|
|
1724
|
+
contrail_segment, weights, dist_perpendicular
|
|
1725
|
+
)
|
|
1726
|
+
|
|
1727
|
+
# Distribute selected contrail property to grid
|
|
1728
|
+
return plume_concentration * (
|
|
1729
|
+
weights * xr.ones_like(weights) * contrail_segment[var_name][1]
|
|
1730
|
+
+ (1 - weights) * xr.ones_like(weights) * contrail_segment[var_name][0]
|
|
1731
|
+
)
|
|
1732
|
+
|
|
1733
|
+
|
|
1734
|
+
def _pixel_weights(contrail_segment: GeoVectorDataset, segment_grid: xr.DataArray) -> xr.DataArray:
|
|
1735
|
+
"""
|
|
1736
|
+
Calculate the pixel weights for `segment_grid`.
|
|
1737
|
+
|
|
1738
|
+
Parameters
|
|
1739
|
+
----------
|
|
1740
|
+
contrail_segment : GeoVectorDataset
|
|
1741
|
+
Contrail segment waypoints (head and tail).
|
|
1742
|
+
segment_grid : xr.DataArray
|
|
1743
|
+
Contrail segment grid with spatial domain that covers the contrail area.
|
|
1744
|
+
|
|
1745
|
+
Returns
|
|
1746
|
+
-------
|
|
1747
|
+
xr.DataArray
|
|
1748
|
+
Pixel weights for `segment_grid`
|
|
1749
|
+
|
|
1750
|
+
Notes
|
|
1751
|
+
-----
|
|
1752
|
+
- See Appendix A12 of :cite:`schumannContrailCirrusPrediction2012`.
|
|
1753
|
+
- This is the weights (from the beginning of the contrail segment) to the nearest longitude and
|
|
1754
|
+
latitude pixel in the `segment_grid`.
|
|
1755
|
+
- The contrail segment do not contribute to the pixel if weight < 0 or > 1.
|
|
1756
|
+
"""
|
|
1757
|
+
head = contrail_segment.dataframe.iloc[0]
|
|
1758
|
+
tail = contrail_segment.dataframe.iloc[1]
|
|
1759
|
+
|
|
1760
|
+
# Calculate determinant
|
|
1761
|
+
dx = units.longitude_distance_to_m(
|
|
1762
|
+
(tail["longitude"] - head["longitude"]),
|
|
1763
|
+
0.5 * (head["latitude"] + tail["latitude"]),
|
|
1764
|
+
)
|
|
1765
|
+
dy = units.latitude_distance_to_m(tail["latitude"] - head["latitude"])
|
|
1766
|
+
det = dx**2 + dy**2
|
|
1767
|
+
|
|
1768
|
+
# Calculate pixel weights
|
|
1769
|
+
lon_grid, lat_grid = np.meshgrid(
|
|
1770
|
+
segment_grid["longitude"].values, segment_grid["latitude"].values
|
|
1771
|
+
)
|
|
1772
|
+
dx_grid = units.longitude_distance_to_m(
|
|
1773
|
+
(lon_grid - head["longitude"]),
|
|
1774
|
+
0.5 * (head["latitude"] + lat_grid),
|
|
1775
|
+
)
|
|
1776
|
+
dy_grid = units.latitude_distance_to_m(lat_grid - head["latitude"])
|
|
1777
|
+
weights = (dx * dx_grid + dy * dy_grid) / det
|
|
1778
|
+
return xr.DataArray(
|
|
1779
|
+
data=weights.T,
|
|
1780
|
+
dims=["longitude", "latitude"],
|
|
1781
|
+
coords={"longitude": segment_grid["longitude"], "latitude": segment_grid["latitude"]},
|
|
1782
|
+
)
|
|
1783
|
+
|
|
1784
|
+
|
|
1785
|
+
def _segment_perpendicular_distance_to_pixels(
|
|
1786
|
+
contrail_segment: GeoVectorDataset, weights: xr.DataArray
|
|
1787
|
+
) -> xr.DataArray:
|
|
1788
|
+
"""
|
|
1789
|
+
Calculate perpendicular distance from contrail segment to each segment grid pixel.
|
|
1790
|
+
|
|
1791
|
+
Parameters
|
|
1792
|
+
----------
|
|
1793
|
+
contrail_segment : GeoVectorDataset
|
|
1794
|
+
Contrail segment waypoints (head and tail).
|
|
1795
|
+
weights : xr.DataArray
|
|
1796
|
+
Pixel weights for `segment_grid`.
|
|
1797
|
+
See `_pixel_weights` function.
|
|
1798
|
+
|
|
1799
|
+
Returns
|
|
1800
|
+
-------
|
|
1801
|
+
xr.DataArray
|
|
1802
|
+
Perpendicular distance from contrail segment to each segment grid pixel, [:math:`m`]
|
|
1803
|
+
|
|
1804
|
+
Notes
|
|
1805
|
+
-----
|
|
1806
|
+
- See Figure A7 of :cite:`schumannContrailCirrusPrediction2012`.
|
|
1807
|
+
"""
|
|
1808
|
+
head = contrail_segment.dataframe.iloc[0]
|
|
1809
|
+
tail = contrail_segment.dataframe.iloc[1]
|
|
1810
|
+
|
|
1811
|
+
# Longitude and latitude along contrail segment
|
|
1812
|
+
lon_grid, lat_grid = np.meshgrid(weights["longitude"].values, weights["latitude"].values)
|
|
1813
|
+
|
|
1814
|
+
lon_s = head["longitude"] + weights.T.values * (tail["longitude"] - head["longitude"])
|
|
1815
|
+
lat_s = head["latitude"] + weights.T.values * (tail["latitude"] - head["latitude"])
|
|
1816
|
+
|
|
1817
|
+
lon_dist = units.longitude_distance_to_m(np.abs(lon_grid - lon_s), 0.5 * (lat_s + lat_grid))
|
|
1818
|
+
|
|
1819
|
+
lat_dist = units.latitude_distance_to_m(np.abs(lat_grid - lat_s))
|
|
1820
|
+
dist_perp = (lon_dist**2 + lat_dist**2) ** 0.5
|
|
1821
|
+
return xr.DataArray(dist_perp.T, coords=weights.coords)
|
|
1822
|
+
|
|
1823
|
+
|
|
1824
|
+
def _gaussian_plume_concentration(
|
|
1825
|
+
contrail_segment: GeoVectorDataset,
|
|
1826
|
+
weights: xr.DataArray,
|
|
1827
|
+
dist_perpendicular: xr.DataArray,
|
|
1828
|
+
) -> xr.DataArray:
|
|
1829
|
+
"""
|
|
1830
|
+
Calculate relative gaussian plume concentration along the contrail width.
|
|
1831
|
+
|
|
1832
|
+
Parameters
|
|
1833
|
+
----------
|
|
1834
|
+
contrail_segment : GeoVectorDataset
|
|
1835
|
+
Contrail segment waypoints (head and tail).
|
|
1836
|
+
weights : xr.DataArray
|
|
1837
|
+
Pixel weights for `segment_grid`.
|
|
1838
|
+
See `_pixel_weights` function.
|
|
1839
|
+
dist_perpendicular : xr.DataArray
|
|
1840
|
+
Perpendicular distance from contrail segment to each segment grid pixel, [:math:`m`]
|
|
1841
|
+
See `_segment_perpendicular_distance_to_pixels` function.
|
|
1842
|
+
|
|
1843
|
+
Returns
|
|
1844
|
+
-------
|
|
1845
|
+
xr.DataArray
|
|
1846
|
+
Relative gaussian plume concentration along the contrail width
|
|
1847
|
+
|
|
1848
|
+
Notes
|
|
1849
|
+
-----
|
|
1850
|
+
- Assume a one-dimensional Gaussian plume.
|
|
1851
|
+
- See Appendix A11 of :cite:`schumannContrailCirrusPrediction2012`.
|
|
1852
|
+
"""
|
|
1853
|
+
head = contrail_segment.dataframe.iloc[0]
|
|
1854
|
+
tail = contrail_segment.dataframe.iloc[1]
|
|
1855
|
+
|
|
1856
|
+
width = weights.values * tail["width"] + (1 - weights.values) * head["width"]
|
|
1857
|
+
sigma_yy = 0.125 * width**2
|
|
1858
|
+
|
|
1859
|
+
concentration = np.where(
|
|
1860
|
+
(weights.values < 0) | (weights.values > 1),
|
|
1861
|
+
0,
|
|
1862
|
+
(4 / np.pi) ** 0.5 * np.exp(-0.5 * dist_perpendicular.values**2 / sigma_yy),
|
|
1863
|
+
)
|
|
1864
|
+
return xr.DataArray(concentration, coords=weights.coords)
|
|
1865
|
+
|
|
1866
|
+
|
|
1867
|
+
def _add_segment_to_main_grid(main_grid: xr.DataArray, segment_grid: xr.DataArray) -> xr.DataArray:
|
|
1868
|
+
"""
|
|
1869
|
+
Add the gridded contrail segment to the main grid.
|
|
1870
|
+
|
|
1871
|
+
Parameters
|
|
1872
|
+
----------
|
|
1873
|
+
main_grid : xr.DataArray
|
|
1874
|
+
Aggregated contrail segment properties in a longitude-latitude grid.
|
|
1875
|
+
segment_grid : xr.DataArray
|
|
1876
|
+
Contrail segment dimension and property projected to a longitude-latitude grid.
|
|
1877
|
+
|
|
1878
|
+
Returns
|
|
1879
|
+
-------
|
|
1880
|
+
xr.DataArray
|
|
1881
|
+
Aggregated contrail segment properties, including `segment_grid`.
|
|
1882
|
+
|
|
1883
|
+
Notes
|
|
1884
|
+
-----
|
|
1885
|
+
- The spatial domain of `segment_grid` only covers the contrail segment, which is added to
|
|
1886
|
+
the `main_grid` which is expected to have a larger spatial domain than the `segment_grid`.
|
|
1887
|
+
- This architecture is used to reduce the computational resources.
|
|
1888
|
+
"""
|
|
1889
|
+
lon_main = main_grid["longitude"].values
|
|
1890
|
+
lat_main = main_grid["latitude"].values
|
|
1891
|
+
|
|
1892
|
+
lon_segment_grid = np.round(segment_grid["longitude"].values, decimals=2)
|
|
1893
|
+
lat_segment_grid = np.round(segment_grid["latitude"].values, decimals=2)
|
|
1894
|
+
|
|
1895
|
+
main_grid_arr = main_grid.values
|
|
1896
|
+
subgrid_arr = segment_grid.values
|
|
1897
|
+
|
|
1898
|
+
try:
|
|
1899
|
+
ix_ = np.searchsorted(lon_main, lon_segment_grid[0])
|
|
1900
|
+
ix = np.searchsorted(lon_main, lon_segment_grid[-1]) + 1
|
|
1901
|
+
iy_ = np.searchsorted(lat_main, lat_segment_grid[0])
|
|
1902
|
+
iy = np.searchsorted(lat_main, lat_segment_grid[-1]) + 1
|
|
1903
|
+
except IndexError:
|
|
1904
|
+
warnings.warn(
|
|
1905
|
+
"Contrail segment ignored as it is outside spatial bounding box of the main grid. "
|
|
1906
|
+
)
|
|
1907
|
+
else:
|
|
1908
|
+
main_grid_arr[ix_:ix, iy_:iy] = main_grid_arr[ix_:ix, iy_:iy] + subgrid_arr
|
|
1909
|
+
|
|
1910
|
+
return xr.DataArray(main_grid_arr, coords=main_grid.coords)
|
|
1911
|
+
|
|
1912
|
+
|
|
1913
|
+
# ------------------------------------
|
|
1914
|
+
# High resolution grid: natural cirrus
|
|
1915
|
+
# ------------------------------------
|
|
1916
|
+
|
|
1917
|
+
|
|
1918
|
+
def natural_cirrus_properties_to_hi_res_grid(
|
|
1919
|
+
met: MetDataset,
|
|
1920
|
+
*,
|
|
1921
|
+
spatial_grid_res: float = 0.05,
|
|
1922
|
+
optical_depth_threshold: float = 0.1,
|
|
1923
|
+
random_state: np.random.Generator | int | None = None,
|
|
1924
|
+
) -> MetDataset:
|
|
1925
|
+
r"""
|
|
1926
|
+
Increase the longitude-latitude resolution of natural cirrus cover and optical depth.
|
|
1927
|
+
|
|
1928
|
+
Parameters
|
|
1929
|
+
----------
|
|
1930
|
+
met : MetDataset
|
|
1931
|
+
Pressure level dataset for one time step containing 'air_temperature', 'specific_humidity',
|
|
1932
|
+
'specific_cloud_ice_water_content', 'geopotential',and `fraction_of_cloud_cover`
|
|
1933
|
+
spatial_grid_res : float
|
|
1934
|
+
Spatial grid resolution for the output, [:math:`\deg`]
|
|
1935
|
+
optical_depth_threshold : float
|
|
1936
|
+
Sensitivity of cirrus detection, set at 0.1 to match the capability of satellites.
|
|
1937
|
+
random_state : np.random.Generator | int | None
|
|
1938
|
+
A number used to initialize a pseudorandom number generator.
|
|
1939
|
+
|
|
1940
|
+
Returns
|
|
1941
|
+
-------
|
|
1942
|
+
MetDataset
|
|
1943
|
+
Single-level dataset containing the high resolution natural cirrus properties.
|
|
1944
|
+
|
|
1945
|
+
References
|
|
1946
|
+
----------
|
|
1947
|
+
- :cite:`schumannContrailCirrusPrediction2012`
|
|
1948
|
+
|
|
1949
|
+
Notes
|
|
1950
|
+
-----
|
|
1951
|
+
- The high-resolution natural cirrus coverage and optical depth is distributed randomly,
|
|
1952
|
+
ensuring that the mean value is equal to the value of the original grid.
|
|
1953
|
+
- Enhancing the spatial resolution is necessary because the existing spatial resolution of
|
|
1954
|
+
numerical weather prediction (NWP) models are too coarse to resolve the coverage area of
|
|
1955
|
+
relatively narrow contrails.
|
|
1956
|
+
"""
|
|
1957
|
+
# Ensure the required columns are included in `met`
|
|
1958
|
+
met.ensure_vars(
|
|
1959
|
+
(
|
|
1960
|
+
"air_temperature",
|
|
1961
|
+
"specific_humidity",
|
|
1962
|
+
"specific_cloud_ice_water_content",
|
|
1963
|
+
"geopotential",
|
|
1964
|
+
"fraction_of_cloud_cover",
|
|
1965
|
+
)
|
|
1966
|
+
)
|
|
1967
|
+
|
|
1968
|
+
# Ensure `met` only contains one time step, constraint can be relaxed in the future.
|
|
1969
|
+
if len(met["time"].data) > 1:
|
|
1970
|
+
raise AssertionError(
|
|
1971
|
+
"`met` contains more than one time step, but function only accepts one time step. "
|
|
1972
|
+
)
|
|
1973
|
+
|
|
1974
|
+
# Calculate tau_cirrus as observed by satellites
|
|
1975
|
+
met["tau_cirrus"] = tau_cirrus(met)
|
|
1976
|
+
tau_cirrus_max = met["tau_cirrus"].data.sel(level=met["level"].data[-1])
|
|
1977
|
+
|
|
1978
|
+
# Calculate cirrus coverage as observed by satellites, cc_max(x,y,t) = max[cc(x,y,z,t)]
|
|
1979
|
+
cirrus_cover_max = met["fraction_of_cloud_cover"].data.max(dim="level")
|
|
1980
|
+
|
|
1981
|
+
# Increase resolution of longitude and latitude dimensions
|
|
1982
|
+
lon_coords_hi_res, lat_coords_hi_res = _hi_res_grid_coordinates(
|
|
1983
|
+
met["longitude"].values, met["latitude"].values, spatial_grid_res=spatial_grid_res
|
|
1984
|
+
)
|
|
1985
|
+
|
|
1986
|
+
# Increase spatial resolution by repeating existing values (temporarily)
|
|
1987
|
+
n_reps = int(
|
|
1988
|
+
np.round(np.diff(met["longitude"].values)[0], decimals=2)
|
|
1989
|
+
/ np.round(np.diff(lon_coords_hi_res)[0], decimals=2)
|
|
1990
|
+
)
|
|
1991
|
+
cc_rep = _repeat_rows_and_columns(cirrus_cover_max.values, n_reps=n_reps)
|
|
1992
|
+
tau_cirrus_rep = _repeat_rows_and_columns(tau_cirrus_max.values, n_reps=n_reps)
|
|
1993
|
+
|
|
1994
|
+
# Enhance resolution of `tau_cirrus`
|
|
1995
|
+
rng = np.random.default_rng(random_state)
|
|
1996
|
+
rand_number = rng.uniform(0, 1, np.shape(tau_cirrus_rep))
|
|
1997
|
+
dx = 0.03 # Prevent division of small values: calibrated to match the original cirrus cover
|
|
1998
|
+
has_cirrus = rand_number > (1 + dx - cc_rep)
|
|
1999
|
+
|
|
2000
|
+
tau_cirrus_hi_res = np.zeros_like(tau_cirrus_rep)
|
|
2001
|
+
tau_cirrus_hi_res[has_cirrus] = tau_cirrus_rep[has_cirrus] / cc_rep[has_cirrus]
|
|
2002
|
+
|
|
2003
|
+
# Enhance resolution of `cirrus coverage`
|
|
2004
|
+
cirrus_cover_hi_res = np.where(tau_cirrus_hi_res > optical_depth_threshold, 1, 0)
|
|
2005
|
+
|
|
2006
|
+
# Package outputs
|
|
2007
|
+
ds_hi_res = xr.Dataset(
|
|
2008
|
+
data_vars=dict(
|
|
2009
|
+
tau_cirrus=(["longitude", "latitude"], tau_cirrus_hi_res),
|
|
2010
|
+
cc_natural_cirrus=(["longitude", "latitude"], cirrus_cover_hi_res),
|
|
2011
|
+
),
|
|
2012
|
+
coords=dict(longitude=lon_coords_hi_res, latitude=lat_coords_hi_res),
|
|
2013
|
+
)
|
|
2014
|
+
ds_hi_res = ds_hi_res.expand_dims({"level": np.array([-1])})
|
|
2015
|
+
ds_hi_res = ds_hi_res.expand_dims({"time": met["time"].values})
|
|
2016
|
+
return MetDataset(ds_hi_res)
|
|
2017
|
+
|
|
2018
|
+
|
|
2019
|
+
def _hi_res_grid_coordinates(
|
|
2020
|
+
lon_coords: npt.NDArray[np.floating],
|
|
2021
|
+
lat_coords: npt.NDArray[np.floating],
|
|
2022
|
+
*,
|
|
2023
|
+
spatial_grid_res: float = 0.05,
|
|
2024
|
+
) -> tuple[npt.NDArray[np.floating], npt.NDArray[np.floating]]:
|
|
2025
|
+
r"""
|
|
2026
|
+
Calculate longitude and latitude coordinates for the high resolution grid.
|
|
2027
|
+
|
|
2028
|
+
Parameters
|
|
2029
|
+
----------
|
|
2030
|
+
lon_coords : npt.NDArray[np.floating]
|
|
2031
|
+
Longitude coordinates provided by the original `MetDataset`.
|
|
2032
|
+
lat_coords : npt.NDArray[np.floating]
|
|
2033
|
+
Latitude coordinates provided by the original `MetDataset`.
|
|
2034
|
+
spatial_grid_res : float
|
|
2035
|
+
Spatial grid resolution for the output, [:math:`\deg`]
|
|
2036
|
+
|
|
2037
|
+
Returns
|
|
2038
|
+
-------
|
|
2039
|
+
tuple[npt.NDArray[np.floating], npt.NDArray[np.floating]
|
|
2040
|
+
Longitude and latitude coordinates for the high resolution grid.
|
|
2041
|
+
"""
|
|
2042
|
+
d_lon = np.abs(np.diff(lon_coords)[0])
|
|
2043
|
+
d_lat = np.abs(np.diff(lat_coords)[0])
|
|
2044
|
+
is_whole_number = (d_lon / spatial_grid_res) - int(d_lon / spatial_grid_res) == 0
|
|
2045
|
+
|
|
2046
|
+
if (d_lon <= spatial_grid_res) | (d_lat <= spatial_grid_res):
|
|
2047
|
+
raise ArithmeticError(
|
|
2048
|
+
"Spatial resolution of `met` is already higher than `spatial_grid_res`"
|
|
2049
|
+
)
|
|
2050
|
+
|
|
2051
|
+
if not is_whole_number:
|
|
2052
|
+
raise ArithmeticError(
|
|
2053
|
+
"Select a spatial grid resolution where `spatial_grid_res / existing_grid_res` is "
|
|
2054
|
+
"a whole number. "
|
|
2055
|
+
)
|
|
2056
|
+
|
|
2057
|
+
lon_coords_hi_res = np.arange(
|
|
2058
|
+
lon_coords[0], lon_coords[-1] + spatial_grid_res, spatial_grid_res, dtype=float
|
|
2059
|
+
)
|
|
2060
|
+
|
|
2061
|
+
lat_coords_hi_res = np.arange(
|
|
2062
|
+
lat_coords[0], lat_coords[-1] + spatial_grid_res, spatial_grid_res, dtype=float
|
|
2063
|
+
)
|
|
2064
|
+
|
|
2065
|
+
return (np.round(lon_coords_hi_res, decimals=3), np.round(lat_coords_hi_res, decimals=3))
|
|
2066
|
+
|
|
2067
|
+
|
|
2068
|
+
def _repeat_rows_and_columns(
|
|
2069
|
+
array_2d: npt.NDArray[np.floating], *, n_reps: int
|
|
2070
|
+
) -> npt.NDArray[np.floating]:
|
|
2071
|
+
"""
|
|
2072
|
+
Repeat the elements in `array_2d` along each row and column.
|
|
2073
|
+
|
|
2074
|
+
Parameters
|
|
2075
|
+
----------
|
|
2076
|
+
array_2d : npt.NDArray[np.float64, np.float64]
|
|
2077
|
+
2D array containing `tau_cirrus` or `cirrus_coverage` across longitude and latitude.
|
|
2078
|
+
n_reps : int
|
|
2079
|
+
Number of repetitions.
|
|
2080
|
+
|
|
2081
|
+
Returns
|
|
2082
|
+
-------
|
|
2083
|
+
npt.NDArray[np.float64, np.float64]
|
|
2084
|
+
2D array containing `tau_cirrus` or `cirrus_coverage` at a higher spatial resolution.
|
|
2085
|
+
See :func:`_hi_res_grid_coordinates`.
|
|
2086
|
+
"""
|
|
2087
|
+
dimension = np.shape(array_2d)
|
|
2088
|
+
|
|
2089
|
+
# Repeating elements along axis=1
|
|
2090
|
+
array_1d_rep = [np.repeat(array_2d[i, :], n_reps) for i in np.arange(dimension[0])]
|
|
2091
|
+
stacked = np.vstack(array_1d_rep)
|
|
2092
|
+
|
|
2093
|
+
# Repeating elements along axis=0
|
|
2094
|
+
array_2d_rep = np.repeat(stacked, n_reps, axis=0)
|
|
2095
|
+
|
|
2096
|
+
# Do not repeat final row and column as they are on the edge
|
|
2097
|
+
return array_2d_rep[: -(n_reps - 1), : -(n_reps - 1)]
|
|
2098
|
+
|
|
2099
|
+
|
|
2100
|
+
# -----------------------------------------
|
|
2101
|
+
# Compare CoCiP outputs with GOES satellite
|
|
2102
|
+
# -----------------------------------------
|
|
2103
|
+
|
|
2104
|
+
|
|
2105
|
+
def compare_cocip_with_goes(
|
|
2106
|
+
time: np.timedelta64 | pd.Timestamp,
|
|
2107
|
+
flight: GeoVectorDataset | pd.DataFrame,
|
|
2108
|
+
contrail: GeoVectorDataset | pd.DataFrame,
|
|
2109
|
+
*,
|
|
2110
|
+
spatial_bbox: tuple[float, float, float, float] = (-160.0, -80.0, 10.0, 80.0),
|
|
2111
|
+
region: str = "F",
|
|
2112
|
+
path_write_img: pathlib.Path | None = None,
|
|
2113
|
+
) -> None | pathlib.Path:
|
|
2114
|
+
r"""
|
|
2115
|
+
Compare simulated persistent contrails from CoCiP with GOES satellite imagery.
|
|
2116
|
+
|
|
2117
|
+
Parameters
|
|
2118
|
+
----------
|
|
2119
|
+
time : np.timedelta64 | pd.Timestamp
|
|
2120
|
+
Time of GOES satellite image.
|
|
2121
|
+
flight : GeoVectorDataset | pd.DataFrame
|
|
2122
|
+
Flight waypoints.
|
|
2123
|
+
Best to use the returned output :class:`Flight` from
|
|
2124
|
+
:meth:`pycontrails.models.cocip.Cocip.eval`.
|
|
2125
|
+
contrail : GeoVectorDataset | pd.DataFrame,
|
|
2126
|
+
Contrail evolution outputs (:attr:`pycontrails.models.cocip.Cocip.contrail`)
|
|
2127
|
+
set during :meth:`pycontrails.models.cocip.Cocip.eval`.
|
|
2128
|
+
spatial_bbox : tuple[float, float, float, float]
|
|
2129
|
+
Spatial bounding box, ``(lon_min, lat_min, lon_max, lat_max)``, [:math:`\deg`]
|
|
2130
|
+
region : str
|
|
2131
|
+
'F' for full disk (image provided every 10 m), and 'C' for CONUS (image provided every 5 m)
|
|
2132
|
+
path_write_img : None | pathlib.Path
|
|
2133
|
+
File path to save the CoCiP-GOES image.
|
|
2134
|
+
|
|
2135
|
+
Returns
|
|
2136
|
+
-------
|
|
2137
|
+
None | pathlib.Path
|
|
2138
|
+
File path of saved CoCiP-GOES image if ``path_write_img`` is provided.
|
|
2139
|
+
"""
|
|
2140
|
+
|
|
2141
|
+
# We'll get a nice error message if dependencies are not installed
|
|
2142
|
+
from pycontrails.datalib import goes
|
|
2143
|
+
|
|
2144
|
+
try:
|
|
2145
|
+
import cartopy.crs as ccrs
|
|
2146
|
+
from cartopy.mpl.ticker import LatitudeFormatter, LongitudeFormatter
|
|
2147
|
+
except ModuleNotFoundError as e:
|
|
2148
|
+
dependencies.raise_module_not_found_error(
|
|
2149
|
+
name="compare_cocip_with_goes function",
|
|
2150
|
+
package_name="cartopy",
|
|
2151
|
+
module_not_found_error=e,
|
|
2152
|
+
pycontrails_optional_package="sat",
|
|
2153
|
+
)
|
|
2154
|
+
|
|
2155
|
+
try:
|
|
2156
|
+
import matplotlib.pyplot as plt
|
|
2157
|
+
except ModuleNotFoundError as e:
|
|
2158
|
+
dependencies.raise_module_not_found_error(
|
|
2159
|
+
name="compare_cocip_with_goes function",
|
|
2160
|
+
package_name="matplotlib",
|
|
2161
|
+
module_not_found_error=e,
|
|
2162
|
+
pycontrails_optional_package="vis",
|
|
2163
|
+
)
|
|
2164
|
+
|
|
2165
|
+
# Round `time` to nearest GOES image time slice
|
|
2166
|
+
if isinstance(time, np.timedelta64):
|
|
2167
|
+
time = pd.to_datetime(time)
|
|
2168
|
+
|
|
2169
|
+
if region == "F":
|
|
2170
|
+
time = time.round("10min")
|
|
2171
|
+
elif region == "C":
|
|
2172
|
+
time = time.round("5min")
|
|
2173
|
+
else:
|
|
2174
|
+
raise AssertionError("`region` only accepts inputs of `F` (full disk) or `C` (CONUS)")
|
|
2175
|
+
|
|
2176
|
+
_flight = GeoVectorDataset(flight)
|
|
2177
|
+
_contrail = GeoVectorDataset(contrail)
|
|
2178
|
+
|
|
2179
|
+
# Ensure the required columns are included in `flight_waypoints` and `contrails`
|
|
2180
|
+
_flight.ensure_vars(["flight_id", "waypoint"])
|
|
2181
|
+
_contrail.ensure_vars(
|
|
2182
|
+
["flight_id", "waypoint", "sin_a", "cos_a", "width", "tau_contrail", "age_hours"]
|
|
2183
|
+
)
|
|
2184
|
+
|
|
2185
|
+
# Downselect `_flight` only to spatial domain covered by GOES full disk
|
|
2186
|
+
is_in_lon = _flight.dataframe["longitude"].between(spatial_bbox[0], spatial_bbox[2])
|
|
2187
|
+
is_in_lat = _flight.dataframe["latitude"].between(spatial_bbox[1], spatial_bbox[3])
|
|
2188
|
+
is_in_lon_lat = is_in_lon & is_in_lat
|
|
2189
|
+
|
|
2190
|
+
if not np.any(is_in_lon_lat):
|
|
2191
|
+
warnings.warn(
|
|
2192
|
+
"Flight trajectory does not intersect with the defined spatial bounding box or spatial "
|
|
2193
|
+
"domain covered by GOES."
|
|
2194
|
+
)
|
|
2195
|
+
|
|
2196
|
+
_flight = _flight.filter(is_in_lon_lat)
|
|
2197
|
+
|
|
2198
|
+
# Filter `_flight` if time bounds were previously defined.
|
|
2199
|
+
is_before_time = _flight["time"] < time
|
|
2200
|
+
|
|
2201
|
+
if not np.any(is_before_time):
|
|
2202
|
+
warnings.warn("No flight waypoints were recorded before the specified `time`.")
|
|
2203
|
+
|
|
2204
|
+
_flight = _flight.filter(is_before_time)
|
|
2205
|
+
|
|
2206
|
+
# Downselect `_contrail` only to include the filtered flight waypoints
|
|
2207
|
+
is_in_domain = _contrail.dataframe["waypoint"].isin(_flight["waypoint"])
|
|
2208
|
+
|
|
2209
|
+
if not np.any(is_in_domain):
|
|
2210
|
+
warnings.warn(
|
|
2211
|
+
"No persistent contrails were formed within the defined spatial bounding box."
|
|
2212
|
+
)
|
|
2213
|
+
|
|
2214
|
+
_contrail = _contrail.filter(is_in_domain)
|
|
2215
|
+
|
|
2216
|
+
# Download GOES image at `time`
|
|
2217
|
+
da = goes.GOES(region=region).get(time)
|
|
2218
|
+
rgb, transform, extent = goes.extract_goes_visualization(da)
|
|
2219
|
+
bbox = spatial_bbox[0], spatial_bbox[2], spatial_bbox[1], spatial_bbox[3]
|
|
2220
|
+
|
|
2221
|
+
# Calculate optimal figure dimensions
|
|
2222
|
+
d_lon = spatial_bbox[2] - spatial_bbox[0]
|
|
2223
|
+
d_lat = spatial_bbox[3] - spatial_bbox[1]
|
|
2224
|
+
x_dim = 9.99
|
|
2225
|
+
y_dim = x_dim * (d_lat / d_lon)
|
|
2226
|
+
|
|
2227
|
+
# Plot data
|
|
2228
|
+
fig = plt.figure(figsize=(1.2 * x_dim, y_dim))
|
|
2229
|
+
pc = ccrs.PlateCarree()
|
|
2230
|
+
ax = fig.add_subplot(projection=pc, extent=bbox)
|
|
2231
|
+
ax.coastlines()
|
|
2232
|
+
ax.imshow(rgb, extent=extent, transform=transform)
|
|
2233
|
+
|
|
2234
|
+
ax.set_xticks([spatial_bbox[0], spatial_bbox[2]], crs=ccrs.PlateCarree())
|
|
2235
|
+
ax.set_yticks([spatial_bbox[1], spatial_bbox[3]], crs=ccrs.PlateCarree())
|
|
2236
|
+
lon_formatter = LongitudeFormatter(zero_direction_label=True)
|
|
2237
|
+
lat_formatter = LatitudeFormatter()
|
|
2238
|
+
ax.xaxis.set_major_formatter(lon_formatter)
|
|
2239
|
+
ax.yaxis.set_major_formatter(lat_formatter)
|
|
2240
|
+
|
|
2241
|
+
# Plot flight trajectory up to `time`
|
|
2242
|
+
ax.plot(_flight["longitude"], _flight["latitude"], c="k", linewidth=2.5)
|
|
2243
|
+
plt.legend(["Flight trajectory"])
|
|
2244
|
+
|
|
2245
|
+
# Plot persistent contrails at `time`
|
|
2246
|
+
is_time = (_contrail["time"] == time) & (~np.isnan(_contrail["age_hours"]))
|
|
2247
|
+
im = ax.scatter(
|
|
2248
|
+
_contrail["longitude"][is_time],
|
|
2249
|
+
_contrail["latitude"][is_time],
|
|
2250
|
+
c=_contrail["tau_contrail"][is_time],
|
|
2251
|
+
s=4,
|
|
2252
|
+
cmap="YlOrRd_r",
|
|
2253
|
+
vmin=0,
|
|
2254
|
+
vmax=0.2,
|
|
2255
|
+
)
|
|
2256
|
+
cbar = plt.colorbar(im)
|
|
2257
|
+
cbar.set_label(r"$\tau_{\rm contrail}$")
|
|
2258
|
+
ax.set_title(f"{time}")
|
|
2259
|
+
plt.tight_layout()
|
|
2260
|
+
|
|
2261
|
+
# return output path if `path_write_img` is not None
|
|
2262
|
+
if path_write_img is not None:
|
|
2263
|
+
t_str = time.strftime("%Y%m%d_%H%M%S")
|
|
2264
|
+
file_name = f"goes_{t_str}.png"
|
|
2265
|
+
output_path = path_write_img.joinpath(file_name)
|
|
2266
|
+
plt.savefig(output_path, dpi=150, bbox_inches="tight")
|
|
2267
|
+
plt.close()
|
|
2268
|
+
|
|
2269
|
+
return output_path
|
|
2270
|
+
return None
|