pycontrails 0.58.0__cp314-cp314-macosx_10_13_x86_64.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,437 @@
|
|
|
1
|
+
"""APCEMM interface utility functions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import pathlib
|
|
6
|
+
import subprocess
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
import xarray as xr
|
|
11
|
+
|
|
12
|
+
from pycontrails.core import GeoVectorDataset, MetDataset, met_var, models
|
|
13
|
+
from pycontrails.models.apcemm.inputs import APCEMMInput
|
|
14
|
+
from pycontrails.models.humidity_scaling import HumidityScaling
|
|
15
|
+
from pycontrails.physics import constants, thermo, units
|
|
16
|
+
from pycontrails.utils.types import ArrayScalarLike
|
|
17
|
+
|
|
18
|
+
_path_to_static = pathlib.Path(__file__).parent / "static"
|
|
19
|
+
YAML_TEMPLATE = _path_to_static / "apcemm_yaml_template.yaml"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def generate_apcemm_input_yaml(params: APCEMMInput) -> str:
|
|
23
|
+
"""Generate YAML file from APCEMM input parameters.
|
|
24
|
+
|
|
25
|
+
Parameters
|
|
26
|
+
----------
|
|
27
|
+
params : APCEMMInput
|
|
28
|
+
:class:`APCEMMInput` instance with parameters for input YAML file.
|
|
29
|
+
|
|
30
|
+
Return
|
|
31
|
+
------
|
|
32
|
+
str
|
|
33
|
+
Contents of input YAML file generated from :param:`params`.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
with open(YAML_TEMPLATE) as f:
|
|
37
|
+
template = f.read()
|
|
38
|
+
|
|
39
|
+
return template.format(
|
|
40
|
+
n_threads_int=params.n_threads,
|
|
41
|
+
output_folder_str=params.output_directory,
|
|
42
|
+
overwrite_output_bool=_yaml_bool(params.overwrite_output),
|
|
43
|
+
input_background_conditions_str=params.input_background_conditions,
|
|
44
|
+
input_engine_emissions_str=params.input_engine_emissions,
|
|
45
|
+
max_age_hr=params.max_age / np.timedelta64(1, "h"),
|
|
46
|
+
temperature_k=params.air_temperature,
|
|
47
|
+
rhw_percent=params.rhw * 100,
|
|
48
|
+
horiz_diff_coeff_m2_per_s=params.horiz_diff,
|
|
49
|
+
vert_diff_coeff_m2_per_s=params.vert_diff,
|
|
50
|
+
pressure_hpa=params.air_pressure / 100,
|
|
51
|
+
wind_shear_per_s=params.normal_shear,
|
|
52
|
+
brunt_vaisala_frequency_per_s=params.brunt_vaisala_frequency,
|
|
53
|
+
longitude_deg=params.longitude,
|
|
54
|
+
latitude_deg=params.latitude,
|
|
55
|
+
emission_day_dayofyear=params.day_of_year,
|
|
56
|
+
emission_time_hourofday=params.hour_of_day,
|
|
57
|
+
nox_ppt=params.nox_vmr * 1e12,
|
|
58
|
+
hno3_ppt=params.hno3_vmr * 1e12,
|
|
59
|
+
o3_ppb=params.o3_vmr * 1e9,
|
|
60
|
+
co_ppb=params.co_vmr * 1e9,
|
|
61
|
+
ch4_ppm=params.ch4_vmr * 1e6,
|
|
62
|
+
so2_ppt=params.so2_vmr * 1e12,
|
|
63
|
+
nox_g_per_kg=params.nox_ei * 1000,
|
|
64
|
+
co_g_per_kg=params.co_ei * 1000,
|
|
65
|
+
uhc_g_per_kg=params.hc_ei * 1000,
|
|
66
|
+
so2_g_per_kg=params.so2_ei * 1000,
|
|
67
|
+
so2_to_so4_conv_percent=params.so2_to_so4_conversion * 100,
|
|
68
|
+
soot_g_per_kg=params.nvpm_ei_m * 1000,
|
|
69
|
+
soot_radius_m=params.soot_radius,
|
|
70
|
+
total_fuel_flow_kg_per_s=params.fuel_flow,
|
|
71
|
+
aircraft_mass_kg=params.aircraft_mass,
|
|
72
|
+
flight_speed_m_per_s=params.true_airspeed,
|
|
73
|
+
num_of_engines_int=params.n_engine,
|
|
74
|
+
wingspan_m=params.wingspan,
|
|
75
|
+
core_exit_temp_k=params.core_exit_temp,
|
|
76
|
+
exit_bypass_area_m2=params.core_exit_area,
|
|
77
|
+
transport_timestep_min=params.dt_apcemm_transport / np.timedelta64(1, "m"),
|
|
78
|
+
gravitational_settling_bool=_yaml_bool(params.do_gravitational_setting),
|
|
79
|
+
solid_coagulation_bool=_yaml_bool(params.do_solid_coagulation),
|
|
80
|
+
liquid_coagulation_bool=_yaml_bool(params.do_liquid_coagulation),
|
|
81
|
+
ice_growth_bool=_yaml_bool(params.do_ice_growth),
|
|
82
|
+
coag_timestep_min=params.dt_apcemm_coagulation / np.timedelta64(1, "m"),
|
|
83
|
+
ice_growth_timestep_min=params.dt_apcemm_ice_growth / np.timedelta64(1, "m"),
|
|
84
|
+
met_input_file_path_str=params.input_met_file,
|
|
85
|
+
time_series_data_timestep_hr=params.dt_input_met / np.timedelta64(1, "h"),
|
|
86
|
+
save_aerosol_timeseries_bool=_yaml_bool(params.do_apcemm_nc_output),
|
|
87
|
+
aerosol_indices_list_int=",".join(str(i) for i in params.apcemm_nc_output_species),
|
|
88
|
+
save_frequency_min=params.dt_apcemm_nc_output / np.timedelta64(1, "m"),
|
|
89
|
+
nx_pos_int=params.nx,
|
|
90
|
+
ny_pos_int=params.ny,
|
|
91
|
+
xlim_right_pos_m=params.xlim_right,
|
|
92
|
+
xlim_left_pos_m=params.xlim_left,
|
|
93
|
+
ylim_up_pos_m=params.ylim_up,
|
|
94
|
+
ylim_down_pos_m=params.ylim_down,
|
|
95
|
+
base_contrail_depth_m=params.initial_contrail_depth_offset,
|
|
96
|
+
contrail_depth_scaling_factor_nondim=params.initial_contrail_depth_scale_factor,
|
|
97
|
+
base_contrail_width_m=params.initial_contrail_width_offset,
|
|
98
|
+
contrail_width_scaling_factor_nondim=params.initial_contrail_width_scale_factor,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _yaml_bool(param: bool) -> str:
|
|
103
|
+
"""Convert boolean to T/F for YAML file."""
|
|
104
|
+
return "T" if param else "F"
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def generate_apcemm_input_met(
|
|
108
|
+
time: np.ndarray,
|
|
109
|
+
longitude: np.ndarray,
|
|
110
|
+
latitude: np.ndarray,
|
|
111
|
+
azimuth: np.ndarray,
|
|
112
|
+
altitude: np.ndarray,
|
|
113
|
+
met: MetDataset,
|
|
114
|
+
humidity_scaling: HumidityScaling,
|
|
115
|
+
dz_m: float,
|
|
116
|
+
interp_kwargs: dict[str, Any],
|
|
117
|
+
) -> xr.Dataset:
|
|
118
|
+
r"""Create xarray Dataset for APCEMM meteorology netCDF file.
|
|
119
|
+
|
|
120
|
+
This dataset contains a sequence of atmospheric profiles along the
|
|
121
|
+
Lagrangian trajectory of an advected flight segment. The along-trajectory
|
|
122
|
+
dimension is parameterized by time (rather than latitude and longitude),
|
|
123
|
+
so the dataset coordinates are air pressure and time.
|
|
124
|
+
|
|
125
|
+
Parameters
|
|
126
|
+
----------
|
|
127
|
+
time : np.ndarray
|
|
128
|
+
Time coordinates along the Lagrangian trajectory of the advected flight segment.
|
|
129
|
+
Values must be coercible to ``np.datetime64`` by :class:`GeoVectorDataset`.
|
|
130
|
+
Will be flattened before use if not 1-dimensional.
|
|
131
|
+
longitude : np.ndarray
|
|
132
|
+
Longitude [WGS84] along the Lagrangian trajectory of the advected flight segment.
|
|
133
|
+
Defines the longitude of the trajectory at each time and should have the
|
|
134
|
+
same shape as :param:`time`
|
|
135
|
+
Will be flattened before use if not 1-dimensional.
|
|
136
|
+
latitude : np.ndarray
|
|
137
|
+
Latitude [WGS84] along the Lagrangian trajectory of the advected flight segment.
|
|
138
|
+
Defines the longitude of the trajectory at each time and should have the
|
|
139
|
+
same shape as :param:`time`
|
|
140
|
+
Will be flattened before use if not 1-dimensional.
|
|
141
|
+
azimuth : np.ndarray
|
|
142
|
+
Azimuth [:math:`\deg`] of the advected flight segment at each point along its
|
|
143
|
+
Lagrangian trajectory. Note that the azimuth defines the orientation of the
|
|
144
|
+
advected segment itself, and not the direction in which advection is transporting
|
|
145
|
+
the segment. The azimuth is used to convert horizontal winds into segment-normal
|
|
146
|
+
wind shear. Must have the same shape as :param:`time`.
|
|
147
|
+
Will be flattened before use if not 1-dimensional.
|
|
148
|
+
altitude : np.ndarray
|
|
149
|
+
Defines altitudes [:math:`m`] on which atmospheric profiles are computed.
|
|
150
|
+
Profiles are defined using the same set of altitudes at every point
|
|
151
|
+
along the Lagrangian trajectory of the advected flight segment. Note that
|
|
152
|
+
this parameter does not have to have the same shape as :param:`time`.
|
|
153
|
+
met : MetDataset
|
|
154
|
+
Meteorology used to generate the sequence of atmospheric profiles. Must contain:
|
|
155
|
+
- air temperature [:math:`K`]
|
|
156
|
+
- specific humidity [:math:`kg/kg`]
|
|
157
|
+
- geopotential height [:math:`m`]
|
|
158
|
+
- eastward wind [:math:`m/s`]
|
|
159
|
+
- northward wind [:math:`m/s`]
|
|
160
|
+
- vertical velocity [:math:`Pa/s`]
|
|
161
|
+
humidity_scaling : HumidityScaling
|
|
162
|
+
Humidity scaling applied to specific humidity in :param:`met` before
|
|
163
|
+
generating atmospheric profiles.
|
|
164
|
+
dz_m : float
|
|
165
|
+
Altitude difference [:math:`m`] used to approximate vertical derivatives
|
|
166
|
+
when computing wind shear.
|
|
167
|
+
|
|
168
|
+
Returns
|
|
169
|
+
-------
|
|
170
|
+
xr.Dataset
|
|
171
|
+
Meteorology dataset in required format for APCEMM input.
|
|
172
|
+
"""
|
|
173
|
+
|
|
174
|
+
# Ensure that altitudes are sorted ascending
|
|
175
|
+
altitude = np.sort(altitude)
|
|
176
|
+
|
|
177
|
+
# Check for required fields in met
|
|
178
|
+
vars = (
|
|
179
|
+
met_var.AirTemperature,
|
|
180
|
+
met_var.SpecificHumidity,
|
|
181
|
+
met_var.GeopotentialHeight,
|
|
182
|
+
met_var.EastwardWind,
|
|
183
|
+
met_var.NorthwardWind,
|
|
184
|
+
met_var.VerticalVelocity,
|
|
185
|
+
)
|
|
186
|
+
met.ensure_vars(vars)
|
|
187
|
+
met.standardize_variables(vars)
|
|
188
|
+
|
|
189
|
+
# Flatten input arrays
|
|
190
|
+
time = time.ravel()
|
|
191
|
+
longitude = longitude.ravel()
|
|
192
|
+
latitude = latitude.ravel()
|
|
193
|
+
azimuth = azimuth.ravel()
|
|
194
|
+
altitude = altitude.ravel()
|
|
195
|
+
|
|
196
|
+
# Estimate pressure levels close to target altitudes
|
|
197
|
+
# (not exact because this assumes the ISA temperature profile)
|
|
198
|
+
pressure = units.m_to_pl(altitude) * 1e2
|
|
199
|
+
|
|
200
|
+
# Broadcast to required shape and create vector for initial interpolation
|
|
201
|
+
# onto original pressure levels at target horizontal location.
|
|
202
|
+
shape = (time.size, altitude.size)
|
|
203
|
+
time = np.broadcast_to(time[:, np.newaxis], shape).ravel()
|
|
204
|
+
longitude = np.broadcast_to(longitude[:, np.newaxis], shape).ravel()
|
|
205
|
+
latitude = np.broadcast_to(latitude[:, np.newaxis], shape).ravel()
|
|
206
|
+
azimuth = np.broadcast_to(azimuth[:, np.newaxis], shape).ravel()
|
|
207
|
+
level = np.broadcast_to(pressure[np.newaxis, :] / 1e2, shape).ravel()
|
|
208
|
+
vector = GeoVectorDataset(
|
|
209
|
+
data={"azimuth": azimuth},
|
|
210
|
+
longitude=longitude,
|
|
211
|
+
latitude=latitude,
|
|
212
|
+
level=level,
|
|
213
|
+
time=time,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
# Downselect met before interpolation
|
|
217
|
+
met = vector.downselect_met(met)
|
|
218
|
+
|
|
219
|
+
# Interpolate meteorology data onto vector
|
|
220
|
+
scale_humidity = humidity_scaling is not None and "specific_humidity" not in vector
|
|
221
|
+
for met_key in (
|
|
222
|
+
"air_temperature",
|
|
223
|
+
"eastward_wind",
|
|
224
|
+
"geopotential_height",
|
|
225
|
+
"northward_wind",
|
|
226
|
+
"specific_humidity",
|
|
227
|
+
"lagrangian_tendency_of_air_pressure",
|
|
228
|
+
):
|
|
229
|
+
models.interpolate_met(met, vector, met_key, **interp_kwargs)
|
|
230
|
+
|
|
231
|
+
# Interpolate winds at lower level for shear calculation
|
|
232
|
+
air_pressure_lower = thermo.pressure_dz(vector["air_temperature"], vector.air_pressure, dz_m)
|
|
233
|
+
lower_level = air_pressure_lower / 100.0
|
|
234
|
+
for met_key in ("eastward_wind", "northward_wind"):
|
|
235
|
+
vector_key = f"{met_key}_lower"
|
|
236
|
+
models.interpolate_met(met, vector, met_key, vector_key, **interp_kwargs, level=lower_level)
|
|
237
|
+
|
|
238
|
+
# Apply humidity scaling
|
|
239
|
+
if scale_humidity and humidity_scaling is not None:
|
|
240
|
+
humidity_scaling.eval(vector, copy_source=False)
|
|
241
|
+
|
|
242
|
+
# Compute RHi and segment-normal shear
|
|
243
|
+
vector.setdefault(
|
|
244
|
+
"rhi",
|
|
245
|
+
thermo.rhi(vector["specific_humidity"], vector["air_temperature"], vector.air_pressure),
|
|
246
|
+
)
|
|
247
|
+
vector.setdefault(
|
|
248
|
+
"normal_shear",
|
|
249
|
+
normal_wind_shear(
|
|
250
|
+
vector["eastward_wind"],
|
|
251
|
+
vector["eastward_wind_lower"],
|
|
252
|
+
vector["northward_wind"],
|
|
253
|
+
vector["northward_wind_lower"],
|
|
254
|
+
vector["azimuth"],
|
|
255
|
+
dz_m,
|
|
256
|
+
),
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
# Reshape interpolated fields to (time, level).
|
|
260
|
+
nlev = altitude.size
|
|
261
|
+
ntime = len(vector) // nlev
|
|
262
|
+
shape = (ntime, nlev)
|
|
263
|
+
time = np.unique(vector["time"])
|
|
264
|
+
time = (time - time[0]) / np.timedelta64(1, "h")
|
|
265
|
+
temperature = vector["air_temperature"].reshape(shape)
|
|
266
|
+
qv = vector["specific_humidity"].reshape(shape)
|
|
267
|
+
z = vector["geopotential_height"].reshape(shape)
|
|
268
|
+
rhi = vector["rhi"].reshape(shape)
|
|
269
|
+
shear = vector["normal_shear"].reshape(shape)
|
|
270
|
+
shear[:, -1] = shear[:, -2] # lowest level will be nan
|
|
271
|
+
omega = vector["lagrangian_tendency_of_air_pressure"].reshape(shape)
|
|
272
|
+
virtual_temperature = temperature * (1 + qv / constants.epsilon) / (1 + qv)
|
|
273
|
+
density = pressure[np.newaxis, :] / (constants.R_d * virtual_temperature)
|
|
274
|
+
w = -omega / (density * constants.g)
|
|
275
|
+
|
|
276
|
+
# Interpolate fields to target altitudes profile-by-profile
|
|
277
|
+
# to obtain 2D arrays with dimensions (time, altitude).
|
|
278
|
+
temperature_on_z = np.zeros(shape, dtype=temperature.dtype)
|
|
279
|
+
rhi_on_z = np.zeros(shape, dtype=rhi.dtype)
|
|
280
|
+
shear_on_z = np.zeros(shape, dtype=shear.dtype)
|
|
281
|
+
w_on_z = np.zeros(shape, dtype=w.dtype)
|
|
282
|
+
|
|
283
|
+
# Fields should already be on pressure levels close to target
|
|
284
|
+
# altitudes, so this just uses linear interpolation and constant
|
|
285
|
+
# extrapolation on fields expected by APCEMM.
|
|
286
|
+
# NaNs are preserved at the start and end of interpolated profiles
|
|
287
|
+
# but removed in interiors.
|
|
288
|
+
def interp(z: np.ndarray, z0: np.ndarray, f0: np.ndarray) -> np.ndarray:
|
|
289
|
+
# mask nans
|
|
290
|
+
mask = np.isnan(z0) | np.isnan(f0)
|
|
291
|
+
if np.all(mask):
|
|
292
|
+
msg = (
|
|
293
|
+
"Found all-NaN profile during APCEMM meterology input file creation. "
|
|
294
|
+
"MetDataset may have insufficient spatiotemporal coverage."
|
|
295
|
+
)
|
|
296
|
+
raise ValueError(msg)
|
|
297
|
+
z0 = z0[~mask]
|
|
298
|
+
f0 = f0[~mask]
|
|
299
|
+
|
|
300
|
+
# interpolate
|
|
301
|
+
assert np.all(np.diff(z0) > 0) # expect increasing altitudes
|
|
302
|
+
fi = np.interp(z, z0, f0, left=f0[0], right=f0[-1])
|
|
303
|
+
|
|
304
|
+
# restore nans at start and end of profile
|
|
305
|
+
if mask[0]: # nans at top of profile
|
|
306
|
+
fi[z > z0.max()] = np.nan
|
|
307
|
+
if mask[-1]: # nans at end of profile
|
|
308
|
+
fi[z < z0.min()] = np.nan
|
|
309
|
+
return fi
|
|
310
|
+
|
|
311
|
+
# The manual for loop is unlikely to be a bottleneck since a
|
|
312
|
+
# substantial amount of work is done within each iteration.
|
|
313
|
+
for i in range(ntime):
|
|
314
|
+
temperature_on_z[i, :] = interp(altitude, z[i, :], temperature[i, :])
|
|
315
|
+
rhi_on_z[i, :] = interp(altitude, z[i, :], rhi[i, :])
|
|
316
|
+
shear_on_z[i, :] = interp(altitude, z[i, :], shear[i, :])
|
|
317
|
+
w_on_z[i, :] = interp(altitude, z[i, :], w[i, :])
|
|
318
|
+
|
|
319
|
+
# APCEMM also requires initial pressure profile
|
|
320
|
+
pressure_on_z = interp(altitude, z[0, :], pressure)
|
|
321
|
+
|
|
322
|
+
# Create APCEMM input dataset.
|
|
323
|
+
# Transpose require because APCEMM expects (altitude, time) arrays.
|
|
324
|
+
return xr.Dataset(
|
|
325
|
+
data_vars={
|
|
326
|
+
"pressure": (("altitude",), pressure_on_z.astype("float32") / 1e2, {"units": "hPa"}),
|
|
327
|
+
"temperature": (
|
|
328
|
+
("altitude", "time"),
|
|
329
|
+
temperature_on_z.astype("float32").T,
|
|
330
|
+
{"units": "K"},
|
|
331
|
+
),
|
|
332
|
+
"relative_humidity_ice": (
|
|
333
|
+
("altitude", "time"),
|
|
334
|
+
1e2 * rhi_on_z.astype("float32").T,
|
|
335
|
+
{"units": "percent"},
|
|
336
|
+
),
|
|
337
|
+
"shear": (("altitude", "time"), shear_on_z.astype("float32").T, {"units": "s**-1"}),
|
|
338
|
+
"w": (("altitude", "time"), w_on_z.astype("float32").T, {"units": "m s**-1"}),
|
|
339
|
+
},
|
|
340
|
+
coords={
|
|
341
|
+
"altitude": ("altitude", altitude.astype("float32") / 1e3, {"units": "km"}),
|
|
342
|
+
"time": ("time", time, {"units": "hours"}),
|
|
343
|
+
},
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def run(
|
|
348
|
+
apcemm_path: pathlib.Path | str, input_yaml: str, rundir: str, stdout_log: str, stderr_log: str
|
|
349
|
+
) -> None:
|
|
350
|
+
"""
|
|
351
|
+
Run APCEMM executable.
|
|
352
|
+
|
|
353
|
+
Parameters
|
|
354
|
+
----------
|
|
355
|
+
apcemm_path : pathlib.Path | str
|
|
356
|
+
Path to APCEMM executable.
|
|
357
|
+
input_yaml : str
|
|
358
|
+
Path to APCEMM input yaml file.
|
|
359
|
+
rundir : str
|
|
360
|
+
Path to APCEMM simulation directory.
|
|
361
|
+
stdout_log : str
|
|
362
|
+
Path to file used to log APCEMM stdout
|
|
363
|
+
stderr_log : str
|
|
364
|
+
Path to file used to log APCEMM stderr
|
|
365
|
+
|
|
366
|
+
Raises
|
|
367
|
+
------
|
|
368
|
+
ChildProcessError
|
|
369
|
+
APCEMM exits with a non-zero return code.
|
|
370
|
+
"""
|
|
371
|
+
|
|
372
|
+
with open(stdout_log, "w") as stdout, open(stderr_log, "w") as stderr:
|
|
373
|
+
result = subprocess.run(
|
|
374
|
+
[apcemm_path, input_yaml], stdout=stdout, stderr=stderr, cwd=rundir, check=False
|
|
375
|
+
)
|
|
376
|
+
if result.returncode != 0:
|
|
377
|
+
msg = (
|
|
378
|
+
f"APCEMM simulation in {rundir} "
|
|
379
|
+
f"exited with return code {result.returncode}. "
|
|
380
|
+
f"Check logs at {stdout_log} and {stderr_log}."
|
|
381
|
+
)
|
|
382
|
+
raise ChildProcessError(msg)
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def normal_wind_shear(
|
|
386
|
+
u_hi: ArrayScalarLike,
|
|
387
|
+
u_lo: ArrayScalarLike,
|
|
388
|
+
v_hi: ArrayScalarLike,
|
|
389
|
+
v_lo: ArrayScalarLike,
|
|
390
|
+
azimuth: ArrayScalarLike,
|
|
391
|
+
dz: float,
|
|
392
|
+
) -> ArrayScalarLike:
|
|
393
|
+
r"""Compute segment-normal wind shear from wind speeds at lower and upper levels.
|
|
394
|
+
|
|
395
|
+
Parameters
|
|
396
|
+
----------
|
|
397
|
+
u_hi : ArrayScalarLike
|
|
398
|
+
Eastward wind at upper level [:math:`m/s`]
|
|
399
|
+
u_lo : ArrayScalarLike
|
|
400
|
+
Eastward wind at lower level [:math:`m/s`]
|
|
401
|
+
v_hi : ArrayScalarLike
|
|
402
|
+
Northward wind at upper level [:math:`m/s`]
|
|
403
|
+
v_lo : ArrayScalarLike
|
|
404
|
+
Northward wind at lower level [:math:`m/s`]
|
|
405
|
+
azimuth : ArrayScalarLike
|
|
406
|
+
Segment azimuth [:math:`\deg`]
|
|
407
|
+
dz : float
|
|
408
|
+
Distance between upper and lower level [:math:`m`]
|
|
409
|
+
|
|
410
|
+
Returns
|
|
411
|
+
-------
|
|
412
|
+
ArrayScalarLike
|
|
413
|
+
Segment-normal wind shear [:math:`1/s`]
|
|
414
|
+
"""
|
|
415
|
+
du_dz = (u_hi - u_lo) / dz
|
|
416
|
+
dv_dz = (v_hi - v_lo) / dz
|
|
417
|
+
az_radians = units.degrees_to_radians(azimuth)
|
|
418
|
+
sin_az = np.sin(az_radians)
|
|
419
|
+
cos_az = np.cos(az_radians)
|
|
420
|
+
return sin_az * dv_dz - cos_az * du_dz
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def soot_radius(
|
|
424
|
+
nvpm_ei_m: ArrayScalarLike, nvpm_ei_n: ArrayScalarLike, rho_bc: float = 1770.0
|
|
425
|
+
) -> ArrayScalarLike:
|
|
426
|
+
"""Calculate mean soot radius from mass and number emissions indices.
|
|
427
|
+
|
|
428
|
+
Parameters
|
|
429
|
+
----------
|
|
430
|
+
nvpm_ei_m : ArrayScalarLike
|
|
431
|
+
Soot mass emissions index [:math:`kg/kg`]
|
|
432
|
+
nvpm_ei_n : ArrayScalarLike
|
|
433
|
+
Soot number emissions index [:math:`1/kg`]
|
|
434
|
+
rho_bc : float, optional
|
|
435
|
+
Density of black carbon [:math:`kg/m^3`]. By default, 1770.
|
|
436
|
+
"""
|
|
437
|
+
return ((3.0 * nvpm_ei_m) / (4.0 * np.pi * rho_bc * nvpm_ei_n)) ** (1.0 / 3.0)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Contrail Cirrus Prediction (CoCiP) modeling support."""
|
|
2
|
+
|
|
3
|
+
from pycontrails.models.cocip.cocip import Cocip
|
|
4
|
+
from pycontrails.models.cocip.cocip_params import CocipFlightParams, CocipParams
|
|
5
|
+
from pycontrails.models.cocip.cocip_uncertainty import CocipUncertaintyParams, habit_dirichlet
|
|
6
|
+
from pycontrails.models.cocip.output_formats import (
|
|
7
|
+
compare_cocip_with_goes,
|
|
8
|
+
contrail_flight_summary_statistics,
|
|
9
|
+
contrails_to_hi_res_grid,
|
|
10
|
+
flight_waypoint_summary_statistics,
|
|
11
|
+
longitude_latitude_grid,
|
|
12
|
+
natural_cirrus_properties_to_hi_res_grid,
|
|
13
|
+
time_slice_statistics,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"Cocip",
|
|
18
|
+
"CocipFlightParams",
|
|
19
|
+
"CocipParams",
|
|
20
|
+
"CocipUncertaintyParams",
|
|
21
|
+
"compare_cocip_with_goes",
|
|
22
|
+
"contrail_flight_summary_statistics",
|
|
23
|
+
"contrails_to_hi_res_grid",
|
|
24
|
+
"flight_waypoint_summary_statistics",
|
|
25
|
+
"habit_dirichlet",
|
|
26
|
+
"longitude_latitude_grid",
|
|
27
|
+
"natural_cirrus_properties_to_hi_res_grid",
|
|
28
|
+
"time_slice_statistics",
|
|
29
|
+
]
|