pycontrails 0.51.2__cp311-cp311-win_amd64.whl → 0.52.1__cp311-cp311-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of pycontrails might be problematic. Click here for more details.
- pycontrails/__init__.py +1 -1
- pycontrails/_version.py +2 -2
- pycontrails/core/__init__.py +1 -1
- pycontrails/core/cache.py +1 -1
- pycontrails/core/coordinates.py +5 -5
- pycontrails/core/flight.py +36 -32
- pycontrails/core/interpolation.py +2 -2
- pycontrails/core/met.py +23 -14
- pycontrails/core/polygon.py +1 -1
- pycontrails/core/rgi_cython.cp311-win_amd64.pyd +0 -0
- pycontrails/core/vector.py +1 -1
- pycontrails/datalib/__init__.py +4 -1
- pycontrails/datalib/_leo_utils/search.py +250 -0
- pycontrails/datalib/_leo_utils/static/bq_roi_query.sql +6 -0
- pycontrails/datalib/_leo_utils/vis.py +60 -0
- pycontrails/{core/datalib.py → datalib/_met_utils/metsource.py} +1 -5
- pycontrails/datalib/ecmwf/arco_era5.py +8 -7
- pycontrails/datalib/ecmwf/common.py +3 -2
- pycontrails/datalib/ecmwf/era5.py +12 -11
- pycontrails/datalib/ecmwf/era5_model_level.py +12 -11
- pycontrails/datalib/ecmwf/hres.py +14 -13
- pycontrails/datalib/ecmwf/hres_model_level.py +15 -14
- pycontrails/datalib/ecmwf/ifs.py +14 -13
- pycontrails/datalib/gfs/gfs.py +15 -14
- pycontrails/datalib/goes.py +12 -5
- pycontrails/datalib/landsat.py +567 -0
- pycontrails/datalib/sentinel.py +512 -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/cocip_uncertainty.py +9 -9
- pycontrails/models/cocip/output_formats.py +3 -2
- pycontrails/models/cocipgrid/cocip_grid.py +7 -6
- pycontrails/models/dry_advection.py +14 -5
- pycontrails/physics/geo.py +1 -1
- {pycontrails-0.51.2.dist-info → pycontrails-0.52.1.dist-info}/METADATA +31 -12
- {pycontrails-0.51.2.dist-info → pycontrails-0.52.1.dist-info}/RECORD +44 -35
- {pycontrails-0.51.2.dist-info → pycontrails-0.52.1.dist-info}/WHEEL +1 -1
- pycontrails/datalib/spire/__init__.py +0 -19
- /pycontrails/datalib/{spire/spire.py → spire.py} +0 -0
- {pycontrails-0.51.2.dist-info → pycontrails-0.52.1.dist-info}/LICENSE +0 -0
- {pycontrails-0.51.2.dist-info → pycontrails-0.52.1.dist-info}/NOTICE +0 -0
- {pycontrails-0.51.2.dist-info → pycontrails-0.52.1.dist-info}/top_level.txt +0 -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, copy=False)
|
|
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)
|
|
@@ -92,12 +92,12 @@ class CocipUncertaintyParams(CocipParams):
|
|
|
92
92
|
>>> distr = scipy.stats.uniform(loc=0.4, scale=0.2)
|
|
93
93
|
>>> params = CocipUncertaintyParams(seed=123, initial_wake_vortex_depth_uncertainty=distr)
|
|
94
94
|
>>> params.initial_wake_vortex_depth
|
|
95
|
-
0.41076420
|
|
95
|
+
np.float64(0.41076420...)
|
|
96
96
|
|
|
97
97
|
>>> # Once seeded, calling the class again gives a new value
|
|
98
98
|
>>> params = CocipUncertaintyParams(initial_wake_vortex_depth=distr)
|
|
99
99
|
>>> params.initial_wake_vortex_depth
|
|
100
|
-
0.43526372
|
|
100
|
+
np.float64(0.43526372...)
|
|
101
101
|
|
|
102
102
|
>>> # To retain the default value, set the uncertainty to None
|
|
103
103
|
>>> params = CocipUncertaintyParams(rf_lw_enhancement_factor_uncertainty=None)
|
|
@@ -212,7 +212,7 @@ class CocipUncertaintyParams(CocipParams):
|
|
|
212
212
|
|
|
213
213
|
return out
|
|
214
214
|
|
|
215
|
-
def rvs(self, size: None | int = None) -> dict[str,
|
|
215
|
+
def rvs(self, size: None | int = None) -> dict[str, np.float64 | npt.NDArray[np.float64]]:
|
|
216
216
|
"""Call each distribution's `rvs` method to generate random parameters.
|
|
217
217
|
|
|
218
218
|
Seed calls to `rvs` with class variable `rng`.
|
|
@@ -247,12 +247,12 @@ class CocipUncertaintyParams(CocipParams):
|
|
|
247
247
|
[7.9063961e-04, 3.0336906e-03, 7.7571563e-04, 2.0577813e-02,
|
|
248
248
|
9.4205803e-01, 4.3379897e-03, 3.6786550e-03, 2.4747452e-02]],
|
|
249
249
|
dtype=float32),
|
|
250
|
-
'initial_wake_vortex_depth': 0.39805019708566847,
|
|
251
|
-
'nvpm_ei_n_enhancement_factor': 0.9371878437312526,
|
|
252
|
-
'rf_lw_enhancement_factor': 1.1017491252832377,
|
|
253
|
-
'rf_sw_enhancement_factor': 0.99721639115012,
|
|
254
|
-
'sedimentation_impact_factor': 0.5071779847244678,
|
|
255
|
-
'wind_shear_enhancement_exponent': 0.34100931239701004}
|
|
250
|
+
'initial_wake_vortex_depth': np.float64(0.39805019708566847),
|
|
251
|
+
'nvpm_ei_n_enhancement_factor': np.float64(0.9371878437312526),
|
|
252
|
+
'rf_lw_enhancement_factor': np.float64(1.1017491252832377),
|
|
253
|
+
'rf_sw_enhancement_factor': np.float64(0.99721639115012),
|
|
254
|
+
'sedimentation_impact_factor': np.float64(0.5071779847244678),
|
|
255
|
+
'wind_shear_enhancement_exponent': np.float64(0.34100931239701004)}
|
|
256
256
|
"""
|
|
257
257
|
return {
|
|
258
258
|
param: distr.rvs(size=size, random_state=self.rng)
|
|
@@ -32,7 +32,6 @@ import xarray as xr
|
|
|
32
32
|
|
|
33
33
|
from pycontrails.core.met import MetDataArray, MetDataset
|
|
34
34
|
from pycontrails.core.vector import GeoVectorDataset, vector_to_lon_lat_grid
|
|
35
|
-
from pycontrails.datalib.goes import GOES, extract_goes_visualization
|
|
36
35
|
from pycontrails.models.cocip.contrail_properties import contrail_edges, plume_mass_per_distance
|
|
37
36
|
from pycontrails.models.cocip.radiative_forcing import albedo
|
|
38
37
|
from pycontrails.models.humidity_scaling import HumidityScaling
|
|
@@ -217,7 +216,7 @@ def contrail_flight_summary_statistics(flight_waypoints: GeoVectorDataset) -> pd
|
|
|
217
216
|
)
|
|
218
217
|
|
|
219
218
|
flight_waypoints["persistent_contrail_length"] = np.where(
|
|
220
|
-
np.
|
|
219
|
+
np.nan_to_num(flight_waypoints["ef"]) == 0.0, 0.0, flight_waypoints["segment_length"]
|
|
221
220
|
)
|
|
222
221
|
|
|
223
222
|
# Calculate contrail statistics for each flight
|
|
@@ -2125,6 +2124,8 @@ def compare_cocip_with_goes(
|
|
|
2125
2124
|
File path of saved CoCiP-GOES image if ``path_write_img`` is provided.
|
|
2126
2125
|
"""
|
|
2127
2126
|
|
|
2127
|
+
from pycontrails.datalib.goes import GOES, extract_goes_visualization
|
|
2128
|
+
|
|
2128
2129
|
try:
|
|
2129
2130
|
import cartopy.crs as ccrs
|
|
2130
2131
|
from cartopy.mpl.ticker import LatitudeFormatter, LongitudeFormatter
|
|
@@ -749,12 +749,13 @@ class CocipGrid(models.Model):
|
|
|
749
749
|
|
|
750
750
|
# These should probably not be included in model input ... so
|
|
751
751
|
# we'll get a warning if they get overwritten
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
)
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
)
|
|
752
|
+
lon_head, lat_head = geo.forward_azimuth(lons=lons, lats=lats, az=azimuth, dist=dist)
|
|
753
|
+
vector["longitude_head"] = lon_head.astype(self._target_dtype, copy=False)
|
|
754
|
+
vector["latitude_head"] = lat_head.astype(self._target_dtype, copy=False)
|
|
755
|
+
|
|
756
|
+
lon_tail, lat_tail = geo.forward_azimuth(lons=lons, lats=lats, az=azimuth, dist=-dist)
|
|
757
|
+
vector["longitude_tail"] = lon_tail.astype(self._target_dtype, copy=False)
|
|
758
|
+
vector["latitude_tail"] = lat_tail.astype(self._target_dtype, copy=False)
|
|
758
759
|
|
|
759
760
|
return vector
|
|
760
761
|
|
|
@@ -27,6 +27,9 @@ class DryAdvectionParams(models.ModelParams):
|
|
|
27
27
|
#: Max age of plume evolution.
|
|
28
28
|
max_age: np.timedelta64 = np.timedelta64(20, "h")
|
|
29
29
|
|
|
30
|
+
#: Rate of change of pressure due to sedimentation [:math:`Pa/s`]
|
|
31
|
+
sedimentation_rate: float = 0.0
|
|
32
|
+
|
|
30
33
|
#: Difference in altitude between top and bottom layer for stratification calculations,
|
|
31
34
|
#: [:math:`m`]. Used to approximate derivative of "lagrangian_tendency_of_air_pressure"
|
|
32
35
|
#: (upward component of air velocity) with respect to altitude.
|
|
@@ -124,6 +127,7 @@ class DryAdvection(models.Model):
|
|
|
124
127
|
|
|
125
128
|
dt_integration = self.params["dt_integration"]
|
|
126
129
|
max_age = self.params["max_age"]
|
|
130
|
+
sedimentation_rate = self.params["sedimentation_rate"]
|
|
127
131
|
dz_m = self.params["dz_m"]
|
|
128
132
|
max_depth = self.params["max_depth"]
|
|
129
133
|
|
|
@@ -142,6 +146,7 @@ class DryAdvection(models.Model):
|
|
|
142
146
|
self.met,
|
|
143
147
|
vector,
|
|
144
148
|
t,
|
|
149
|
+
sedimentation_rate=sedimentation_rate,
|
|
145
150
|
dz_m=dz_m,
|
|
146
151
|
max_depth=max_depth,
|
|
147
152
|
**interp_kwargs,
|
|
@@ -171,9 +176,11 @@ class DryAdvection(models.Model):
|
|
|
171
176
|
"""
|
|
172
177
|
|
|
173
178
|
self.source.setdefault("level", self.source.level)
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
179
|
+
|
|
180
|
+
columns: tuple[str, ...] = ("longitude", "latitude", "level", "time")
|
|
181
|
+
if "azimuth" in self.source:
|
|
182
|
+
columns += ("azimuth",)
|
|
183
|
+
self.source = GeoVectorDataset(self.source.select(columns, copy=False))
|
|
177
184
|
|
|
178
185
|
# Get waypoint index if not already set
|
|
179
186
|
self.source.setdefault("waypoint", np.arange(self.source.size))
|
|
@@ -212,7 +219,8 @@ class DryAdvection(models.Model):
|
|
|
212
219
|
if val is not None and pointwise_only:
|
|
213
220
|
raise ValueError(f"Cannot specify '{key}' without specifying 'azimuth'.")
|
|
214
221
|
|
|
215
|
-
|
|
222
|
+
if not pointwise_only:
|
|
223
|
+
self.source[key] = np.full_like(self.source["longitude"], val)
|
|
216
224
|
|
|
217
225
|
if pointwise_only:
|
|
218
226
|
return
|
|
@@ -428,6 +436,7 @@ def _evolve_one_step(
|
|
|
428
436
|
vector: GeoVectorDataset,
|
|
429
437
|
t: np.datetime64,
|
|
430
438
|
*,
|
|
439
|
+
sedimentation_rate: float,
|
|
431
440
|
dz_m: float,
|
|
432
441
|
max_depth: float | None,
|
|
433
442
|
**interp_kwargs: Any,
|
|
@@ -437,7 +446,7 @@ def _evolve_one_step(
|
|
|
437
446
|
_perform_interp_for_step(met, vector, dz_m, **interp_kwargs)
|
|
438
447
|
u_wind = vector["u_wind"]
|
|
439
448
|
v_wind = vector["v_wind"]
|
|
440
|
-
vertical_velocity = vector["vertical_velocity"]
|
|
449
|
+
vertical_velocity = vector["vertical_velocity"] + sedimentation_rate
|
|
441
450
|
|
|
442
451
|
latitude = vector["latitude"]
|
|
443
452
|
longitude = vector["longitude"]
|
pycontrails/physics/geo.py
CHANGED
|
@@ -886,7 +886,7 @@ def spatial_bounding_box(
|
|
|
886
886
|
>>> lon = rng.uniform(-180, 180, size=30)
|
|
887
887
|
>>> lat = rng.uniform(-90, 90, size=30)
|
|
888
888
|
>>> spatial_bounding_box(lon, lat)
|
|
889
|
-
(-168.0, -77.0, 155.0, 82.0)
|
|
889
|
+
(np.float64(-168.0), np.float64(-77.0), np.float64(155.0), np.float64(82.0))
|
|
890
890
|
"""
|
|
891
891
|
lon_min = max(np.floor(np.min(longitude) - buffer), -180.0)
|
|
892
892
|
lon_max = min(np.ceil(np.max(longitude) + buffer), 179.99)
|