pycontrails 0.58.0__cp314-cp314-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.

Files changed (122) hide show
  1. pycontrails/__init__.py +70 -0
  2. pycontrails/_version.py +34 -0
  3. pycontrails/core/__init__.py +30 -0
  4. pycontrails/core/aircraft_performance.py +679 -0
  5. pycontrails/core/airports.py +228 -0
  6. pycontrails/core/cache.py +889 -0
  7. pycontrails/core/coordinates.py +174 -0
  8. pycontrails/core/fleet.py +483 -0
  9. pycontrails/core/flight.py +2185 -0
  10. pycontrails/core/flightplan.py +228 -0
  11. pycontrails/core/fuel.py +140 -0
  12. pycontrails/core/interpolation.py +702 -0
  13. pycontrails/core/met.py +2931 -0
  14. pycontrails/core/met_var.py +387 -0
  15. pycontrails/core/models.py +1321 -0
  16. pycontrails/core/polygon.py +549 -0
  17. pycontrails/core/rgi_cython.cp314-win_amd64.pyd +0 -0
  18. pycontrails/core/vector.py +2249 -0
  19. pycontrails/datalib/__init__.py +12 -0
  20. pycontrails/datalib/_met_utils/metsource.py +746 -0
  21. pycontrails/datalib/ecmwf/__init__.py +73 -0
  22. pycontrails/datalib/ecmwf/arco_era5.py +345 -0
  23. pycontrails/datalib/ecmwf/common.py +114 -0
  24. pycontrails/datalib/ecmwf/era5.py +554 -0
  25. pycontrails/datalib/ecmwf/era5_model_level.py +490 -0
  26. pycontrails/datalib/ecmwf/hres.py +804 -0
  27. pycontrails/datalib/ecmwf/hres_model_level.py +466 -0
  28. pycontrails/datalib/ecmwf/ifs.py +287 -0
  29. pycontrails/datalib/ecmwf/model_levels.py +435 -0
  30. pycontrails/datalib/ecmwf/static/model_level_dataframe_v20240418.csv +139 -0
  31. pycontrails/datalib/ecmwf/variables.py +268 -0
  32. pycontrails/datalib/geo_utils.py +261 -0
  33. pycontrails/datalib/gfs/__init__.py +28 -0
  34. pycontrails/datalib/gfs/gfs.py +656 -0
  35. pycontrails/datalib/gfs/variables.py +104 -0
  36. pycontrails/datalib/goes.py +757 -0
  37. pycontrails/datalib/himawari/__init__.py +27 -0
  38. pycontrails/datalib/himawari/header_struct.py +266 -0
  39. pycontrails/datalib/himawari/himawari.py +667 -0
  40. pycontrails/datalib/landsat.py +589 -0
  41. pycontrails/datalib/leo_utils/__init__.py +5 -0
  42. pycontrails/datalib/leo_utils/correction.py +266 -0
  43. pycontrails/datalib/leo_utils/landsat_metadata.py +300 -0
  44. pycontrails/datalib/leo_utils/search.py +250 -0
  45. pycontrails/datalib/leo_utils/sentinel_metadata.py +748 -0
  46. pycontrails/datalib/leo_utils/static/bq_roi_query.sql +6 -0
  47. pycontrails/datalib/leo_utils/vis.py +59 -0
  48. pycontrails/datalib/sentinel.py +650 -0
  49. pycontrails/datalib/spire/__init__.py +5 -0
  50. pycontrails/datalib/spire/exceptions.py +62 -0
  51. pycontrails/datalib/spire/spire.py +604 -0
  52. pycontrails/ext/bada.py +42 -0
  53. pycontrails/ext/cirium.py +14 -0
  54. pycontrails/ext/empirical_grid.py +140 -0
  55. pycontrails/ext/synthetic_flight.py +431 -0
  56. pycontrails/models/__init__.py +1 -0
  57. pycontrails/models/accf.py +425 -0
  58. pycontrails/models/apcemm/__init__.py +8 -0
  59. pycontrails/models/apcemm/apcemm.py +983 -0
  60. pycontrails/models/apcemm/inputs.py +226 -0
  61. pycontrails/models/apcemm/static/apcemm_yaml_template.yaml +183 -0
  62. pycontrails/models/apcemm/utils.py +437 -0
  63. pycontrails/models/cocip/__init__.py +29 -0
  64. pycontrails/models/cocip/cocip.py +2742 -0
  65. pycontrails/models/cocip/cocip_params.py +305 -0
  66. pycontrails/models/cocip/cocip_uncertainty.py +291 -0
  67. pycontrails/models/cocip/contrail_properties.py +1530 -0
  68. pycontrails/models/cocip/output_formats.py +2270 -0
  69. pycontrails/models/cocip/radiative_forcing.py +1260 -0
  70. pycontrails/models/cocip/radiative_heating.py +520 -0
  71. pycontrails/models/cocip/unterstrasser_wake_vortex.py +508 -0
  72. pycontrails/models/cocip/wake_vortex.py +396 -0
  73. pycontrails/models/cocip/wind_shear.py +120 -0
  74. pycontrails/models/cocipgrid/__init__.py +9 -0
  75. pycontrails/models/cocipgrid/cocip_grid.py +2552 -0
  76. pycontrails/models/cocipgrid/cocip_grid_params.py +138 -0
  77. pycontrails/models/dry_advection.py +602 -0
  78. pycontrails/models/emissions/__init__.py +21 -0
  79. pycontrails/models/emissions/black_carbon.py +599 -0
  80. pycontrails/models/emissions/emissions.py +1353 -0
  81. pycontrails/models/emissions/ffm2.py +336 -0
  82. pycontrails/models/emissions/static/default-engine-uids.csv +239 -0
  83. pycontrails/models/emissions/static/edb-gaseous-v29b-engines.csv +596 -0
  84. pycontrails/models/emissions/static/edb-nvpm-v29b-engines.csv +215 -0
  85. pycontrails/models/extended_k15.py +1327 -0
  86. pycontrails/models/humidity_scaling/__init__.py +37 -0
  87. pycontrails/models/humidity_scaling/humidity_scaling.py +1075 -0
  88. pycontrails/models/humidity_scaling/quantiles/era5-model-level-quantiles.pq +0 -0
  89. pycontrails/models/humidity_scaling/quantiles/era5-pressure-level-quantiles.pq +0 -0
  90. pycontrails/models/issr.py +210 -0
  91. pycontrails/models/pcc.py +326 -0
  92. pycontrails/models/pcr.py +154 -0
  93. pycontrails/models/ps_model/__init__.py +18 -0
  94. pycontrails/models/ps_model/ps_aircraft_params.py +381 -0
  95. pycontrails/models/ps_model/ps_grid.py +701 -0
  96. pycontrails/models/ps_model/ps_model.py +1000 -0
  97. pycontrails/models/ps_model/ps_operational_limits.py +525 -0
  98. pycontrails/models/ps_model/static/ps-aircraft-params-20250328.csv +69 -0
  99. pycontrails/models/ps_model/static/ps-synonym-list-20250328.csv +104 -0
  100. pycontrails/models/sac.py +442 -0
  101. pycontrails/models/tau_cirrus.py +183 -0
  102. pycontrails/physics/__init__.py +1 -0
  103. pycontrails/physics/constants.py +117 -0
  104. pycontrails/physics/geo.py +1138 -0
  105. pycontrails/physics/jet.py +968 -0
  106. pycontrails/physics/static/iata-cargo-load-factors-20250221.csv +74 -0
  107. pycontrails/physics/static/iata-passenger-load-factors-20250221.csv +74 -0
  108. pycontrails/physics/thermo.py +551 -0
  109. pycontrails/physics/units.py +472 -0
  110. pycontrails/py.typed +0 -0
  111. pycontrails/utils/__init__.py +1 -0
  112. pycontrails/utils/dependencies.py +66 -0
  113. pycontrails/utils/iteration.py +13 -0
  114. pycontrails/utils/json.py +187 -0
  115. pycontrails/utils/temp.py +50 -0
  116. pycontrails/utils/types.py +163 -0
  117. pycontrails-0.58.0.dist-info/METADATA +180 -0
  118. pycontrails-0.58.0.dist-info/RECORD +122 -0
  119. pycontrails-0.58.0.dist-info/WHEEL +5 -0
  120. pycontrails-0.58.0.dist-info/licenses/LICENSE +178 -0
  121. pycontrails-0.58.0.dist-info/licenses/NOTICE +43 -0
  122. 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
+ ]