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,983 @@
|
|
|
1
|
+
"""Pycontrails :class:`Model` interface to APCEMM."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import dataclasses
|
|
6
|
+
import glob
|
|
7
|
+
import logging
|
|
8
|
+
import multiprocessing as mp
|
|
9
|
+
import os
|
|
10
|
+
import pathlib
|
|
11
|
+
import shutil
|
|
12
|
+
from typing import Any, NoReturn, overload
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
import pandas as pd
|
|
16
|
+
|
|
17
|
+
from pycontrails.core import cache, models
|
|
18
|
+
from pycontrails.core.aircraft_performance import AircraftPerformance
|
|
19
|
+
from pycontrails.core.flight import Flight
|
|
20
|
+
from pycontrails.core.fuel import Fuel, JetA
|
|
21
|
+
from pycontrails.core.met import MetDataset
|
|
22
|
+
from pycontrails.core.met_var import (
|
|
23
|
+
AirTemperature,
|
|
24
|
+
EastwardWind,
|
|
25
|
+
Geopotential,
|
|
26
|
+
GeopotentialHeight,
|
|
27
|
+
NorthwardWind,
|
|
28
|
+
SpecificHumidity,
|
|
29
|
+
VerticalVelocity,
|
|
30
|
+
)
|
|
31
|
+
from pycontrails.core.vector import GeoVectorDataset
|
|
32
|
+
from pycontrails.models.apcemm import utils
|
|
33
|
+
from pycontrails.models.apcemm.inputs import APCEMMInput
|
|
34
|
+
from pycontrails.models.dry_advection import DryAdvection
|
|
35
|
+
from pycontrails.models.emissions import Emissions
|
|
36
|
+
from pycontrails.models.humidity_scaling import HumidityScaling
|
|
37
|
+
from pycontrails.models.ps_model import PSFlight
|
|
38
|
+
from pycontrails.physics import constants, geo, thermo
|
|
39
|
+
|
|
40
|
+
logger = logging.getLogger(__name__)
|
|
41
|
+
|
|
42
|
+
#: Minimum altitude
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclasses.dataclass
|
|
46
|
+
class APCEMMParams(models.ModelParams):
|
|
47
|
+
"""Default parameters for the pycontrails :class:`APCEMM` interface."""
|
|
48
|
+
|
|
49
|
+
#: Maximum contrail age
|
|
50
|
+
max_age: np.timedelta64 = np.timedelta64(20, "h")
|
|
51
|
+
|
|
52
|
+
#: Longitude buffer for Lagrangian trajectory calculation [WGS84]
|
|
53
|
+
met_longitude_buffer: tuple[float, float] = (10.0, 10.0)
|
|
54
|
+
|
|
55
|
+
#: Latitude buffer for Lagrangian trajectory calculation [WGS84]
|
|
56
|
+
met_latitude_buffer: tuple[float, float] = (10.0, 10.0)
|
|
57
|
+
|
|
58
|
+
#: Level buffer for Lagrangian trajectory calculation [:math:`hPa`]
|
|
59
|
+
met_level_buffer: tuple[float, float] = (40.0, 40.0)
|
|
60
|
+
|
|
61
|
+
#: Timestep for Lagrangian trajectory calculation
|
|
62
|
+
dt_lagrangian: np.timedelta64 = np.timedelta64(30, "m")
|
|
63
|
+
|
|
64
|
+
#: Sedimentation rate for Lagrangian trajectories [:math:`Pa/s`]
|
|
65
|
+
lagrangian_sedimentation_rate: float = 0.0
|
|
66
|
+
|
|
67
|
+
#: Time step of meteorology in generated APCEMM input file.
|
|
68
|
+
dt_input_met: np.timedelta64 = np.timedelta64(1, "h")
|
|
69
|
+
|
|
70
|
+
#: Altitude coordinates [:math:`m`] for meteorology in generated APCEMM input file.
|
|
71
|
+
#: If not provided, uses estimated altitudes for levels in input :class:`Metdataset`.
|
|
72
|
+
altitude_input_met: list[float] | None = None
|
|
73
|
+
|
|
74
|
+
#: Humidity scaling
|
|
75
|
+
humidity_scaling: HumidityScaling | None = None
|
|
76
|
+
|
|
77
|
+
#: Altitude difference for vertical derivative calculations [:math:`m`]
|
|
78
|
+
dz_m: float = 200.0
|
|
79
|
+
|
|
80
|
+
#: ICAO aircraft identifier
|
|
81
|
+
aircraft_type: str = "B738"
|
|
82
|
+
|
|
83
|
+
#: Engine UID. If not provided, uses the default engine UID
|
|
84
|
+
#: for the :attr:`aircraft_type`.
|
|
85
|
+
engine_uid: str | None = None
|
|
86
|
+
|
|
87
|
+
#: Aircraft performance model
|
|
88
|
+
aircraft_performance: AircraftPerformance = dataclasses.field(default_factory=PSFlight)
|
|
89
|
+
|
|
90
|
+
#: Fuel type
|
|
91
|
+
fuel: Fuel = dataclasses.field(default_factory=JetA)
|
|
92
|
+
|
|
93
|
+
#: List of flight waypoints to simulate in APCEMM.
|
|
94
|
+
#: By default, runs a simulation for every waypoint.
|
|
95
|
+
waypoints: list[int] | None = None
|
|
96
|
+
|
|
97
|
+
#: If defined, use to override ``input_background_conditions`` and
|
|
98
|
+
#: ``input_engine_emissions`` in :class:`APCEMMInput` assuming that
|
|
99
|
+
#: ``apcemm_root`` points to the root of the APCEMM git repository.
|
|
100
|
+
apcemm_root: pathlib.Path | str | None = None
|
|
101
|
+
|
|
102
|
+
#: If True, delete existing run directories before running APCEMM simulations.
|
|
103
|
+
#: If False (default), raise an exception if a run directory already exists.
|
|
104
|
+
overwrite: bool = False
|
|
105
|
+
|
|
106
|
+
#: Name of output directory within run directory
|
|
107
|
+
output_directory: str = "out"
|
|
108
|
+
|
|
109
|
+
#: Number of threads to use within individual APCEMM simulations.
|
|
110
|
+
apcemm_threads: int = 1
|
|
111
|
+
|
|
112
|
+
#: Number of individual APCEMM simulations to run in parallel.
|
|
113
|
+
n_jobs: int = 1
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class APCEMM(models.Model):
|
|
117
|
+
"""Run APCEMM as a pycontrails :class:`Model`.
|
|
118
|
+
|
|
119
|
+
This class acts as an adapter between the pycontrails :class:`Model` interface
|
|
120
|
+
(shared with other contrail models) and APCEMM's native interface.
|
|
121
|
+
|
|
122
|
+
`APCEMM <https://github.com/MIT-LAE/APCEMM>`__ was developed at the
|
|
123
|
+
`MIT Laboratory for Aviation and the Environment <https://lae.mit.edu/>`__
|
|
124
|
+
and is described in :cite:`fritzRolePlumescaleProcesses2020`.
|
|
125
|
+
|
|
126
|
+
Parameters
|
|
127
|
+
----------
|
|
128
|
+
met : MetDataset
|
|
129
|
+
Pressure level dataset containing :attr:`met_variables` variables.
|
|
130
|
+
See *Notes* for variable names by data source.
|
|
131
|
+
apcemm_path : pathlib.Path | str
|
|
132
|
+
Path to APCEMM executable. See *Notes* for information about
|
|
133
|
+
acquiring and compiling APCEMM.
|
|
134
|
+
apcemm_root : pathlib.Path | str, optional
|
|
135
|
+
Path to APCEMM root directory, used to set ``input_background_conditions`` and
|
|
136
|
+
``input_engine_emissions`` based on the structure of the
|
|
137
|
+
`APCEMM GitHub repository <https://github.com/MIT-LAE/APCEMM>`__.
|
|
138
|
+
If not provided, pycontrails will use the default paths defined in :class:`APCEMMInput`.
|
|
139
|
+
apcemm_input_params : APCEMMInput, optional
|
|
140
|
+
Value for APCEMM input parameters defined in :class:`APCEMMInput`. If provided, values
|
|
141
|
+
for ``input_background_condition`` or ``input_engine_emissions`` will override values
|
|
142
|
+
set based on ``apcemm_root``. Attempting to provide values for input parameters
|
|
143
|
+
that are determined automatically by this interface will result in an error.
|
|
144
|
+
See *Notes* for detailed information about YAML file generation.
|
|
145
|
+
cachestore : CacheStore, optional
|
|
146
|
+
:class:`CacheStore` used to store APCEMM run directories.
|
|
147
|
+
If not provided, uses a :class:`DiskCacheStore`.
|
|
148
|
+
See *Notes* for detailed information about the file structure for APCEMM
|
|
149
|
+
simulations.
|
|
150
|
+
params : dict[str,Any], optional
|
|
151
|
+
Override APCEMM model parameters with dictionary.
|
|
152
|
+
See :class:`APCEMMParams` for model parameters.
|
|
153
|
+
**params_kwargs : Any
|
|
154
|
+
Override Cocip model parameters with keyword arguments.
|
|
155
|
+
See :class:`APCEMMParams` for model parameters.
|
|
156
|
+
|
|
157
|
+
Notes
|
|
158
|
+
-----
|
|
159
|
+
**Meteorology**
|
|
160
|
+
|
|
161
|
+
APCEMM requires temperature, humidity, gepotential height, and winds.
|
|
162
|
+
Geopotential height is required because APCEMM expects meteorological fields
|
|
163
|
+
on height rather than pressure surfaces. See :attr:`met_variables` for the
|
|
164
|
+
list of required variables.
|
|
165
|
+
|
|
166
|
+
.. list-table:: Variable keys for pressure level data
|
|
167
|
+
:header-rows: 1
|
|
168
|
+
|
|
169
|
+
* - Parameter
|
|
170
|
+
- ECMWF
|
|
171
|
+
- GFS
|
|
172
|
+
* - Air Temperature
|
|
173
|
+
- ``air_temperature``
|
|
174
|
+
- ``air_temperature``
|
|
175
|
+
* - Specific Humidity
|
|
176
|
+
- ``specific_humidity``
|
|
177
|
+
- ``specific_humidity``
|
|
178
|
+
* - Geopotential/Geopotential Height
|
|
179
|
+
- ``geopotential``
|
|
180
|
+
- ``geopotential_height``
|
|
181
|
+
* - Eastward wind
|
|
182
|
+
- ``eastward_wind``
|
|
183
|
+
- ``eastward_wind``
|
|
184
|
+
* - Northward wind
|
|
185
|
+
- ``northward_wind``
|
|
186
|
+
- ``northward_wind``
|
|
187
|
+
* - Vertical velocity
|
|
188
|
+
- ``lagrangian_tendency_of_air_pressure``
|
|
189
|
+
- ``lagrangian_tendency_of_air_pressure``
|
|
190
|
+
|
|
191
|
+
**Acquiring and compiling APCEMM**
|
|
192
|
+
|
|
193
|
+
Users are responsible for acquiring and compiling APCEMM. The APCEMM source code is
|
|
194
|
+
available through `GitHub <https://github.com/MIT-LAE/APCEMM>`__, and instructions
|
|
195
|
+
for compiling APCEMM are available in the repository.
|
|
196
|
+
|
|
197
|
+
Note that APCEMM does not provide versioned releases, and future updates may break
|
|
198
|
+
this interface. To guarantee compatibility between this interface and APCEMM,
|
|
199
|
+
users should use commit
|
|
200
|
+
`9d8e1ee <https://github.com/MIT-LAE/APCEMM/commit/9d8e1eeaa61cbdee1b1d03c65b5b033ded9159e4>`__
|
|
201
|
+
from the APCEMM repository.
|
|
202
|
+
|
|
203
|
+
**Configuring APCEMM YAML files**
|
|
204
|
+
|
|
205
|
+
:class:`APCEMMInput` provides low-level control over the contents of YAML files used
|
|
206
|
+
as APCEMM input. YAML file contents can be controlled by passing custom parameters
|
|
207
|
+
in a dictionary through the ``apcemm_input_params`` parameter. Note, however, that
|
|
208
|
+
:class:`APCEMM` sets a number of APCEMM input parameters automatically, and attempting
|
|
209
|
+
to override any automatically-determined parameters using ``apcemm_input_params``
|
|
210
|
+
will result in an error. A list of automatically-determined parameters is available in
|
|
211
|
+
:attr:`dynamic_yaml_params`.
|
|
212
|
+
|
|
213
|
+
**Simulation initialization, execution, and postprocessing**
|
|
214
|
+
|
|
215
|
+
This interface initializes, runs, and postprocesses APCEMM simulations in four stages:
|
|
216
|
+
|
|
217
|
+
1. A :class:`DryAdvection` model is used to generate trajectories for contrails
|
|
218
|
+
initialized at each flight waypoint. This is a necessary preprocessing step because
|
|
219
|
+
APCEMM is a Lagrangian model and does not explicitly track changes in plume
|
|
220
|
+
location over time. This step also provides time-dependent azimuths that define the
|
|
221
|
+
orientation of advected contrails, which is required to compute contrail-normal
|
|
222
|
+
wind shear from horizontal winds.
|
|
223
|
+
Results from the trajectory calculation are stored in :attr:`trajectories`.
|
|
224
|
+
2. Model parameters and results from the trajectory calculation are used to generate
|
|
225
|
+
YAML files with APCEMM input parameters and netCDF files with meteorology data
|
|
226
|
+
used by APCEMM simulations. A separate pair of files is generated for each
|
|
227
|
+
waypoint processed by APCEMM. Files are saved as ``apcemm_waypoint_<i>/input.yaml``
|
|
228
|
+
and ``apcemm_waypoint_<i>/input.nc`` in the model :attr:`cachestore`,
|
|
229
|
+
where ``<i>`` is replaced by the index of each simulated flight waypoint.
|
|
230
|
+
3. A separate APCEMM simulation is run in each run directory inside the model
|
|
231
|
+
:attr:`cachestore`. Simulations are independent and can be run in parallel
|
|
232
|
+
(controlled by the ``n_jobs`` parameter in :class:`APCEMMParams`). Standard output
|
|
233
|
+
and error streams from each simulation are saved in ``apcemm_waypoint_<i>/stdout.log``
|
|
234
|
+
and ``apcemm_waypoint_<i>/stderr.log``, and APCEMM output is saved
|
|
235
|
+
in a subdirectory specified by the ``output_directory`` model parameter ("out" by default).
|
|
236
|
+
4. APCEMM simulation output is postprocessed. After postprocessing:
|
|
237
|
+
|
|
238
|
+
- A ``status`` column is attached to the ``Flight`` returned by :meth:`eval`.
|
|
239
|
+
This column contains ``"NoSimulation"`` for waypoints where no simulation
|
|
240
|
+
was run and the contents of the APCEMM ``status_case0`` output file for
|
|
241
|
+
other waypoints.
|
|
242
|
+
- A :class:`pd.DataFrame` is created and stored in :attr:`vortex`. This dataframe
|
|
243
|
+
contains time series output from the APCEMM "early plume model" of the aircraft
|
|
244
|
+
exhaust plume and downwash vortex, read from ``Micro_000000.out`` output files
|
|
245
|
+
saved by APCEMM.
|
|
246
|
+
- If APCEMM simulated at least one persistent contrail, A :class:`pd.DataFrame` is
|
|
247
|
+
created and stored in :attr:`contrail`. This dataframe contains paths to netCDF
|
|
248
|
+
files, saved at prescribed time intervals during the APCEMM simulation, and can be
|
|
249
|
+
used to open APCEMM output (e.g., using :func:`xr.open_dataset`) for further analysis.
|
|
250
|
+
|
|
251
|
+
**Numerics**
|
|
252
|
+
|
|
253
|
+
APCEMM simulations are initialized at flight waypoints and represent the evolution of the
|
|
254
|
+
cross-section of contrails formed at each waypoint. APCEMM does not explicitly model the length
|
|
255
|
+
of contrail segments and does not include any representation of deformation by divergent flow.
|
|
256
|
+
APCEMM output represents properties of cross-sections of contrails formed at flight waypoints,
|
|
257
|
+
not properties of contrail segments that form between flight waypoints. Unlike :class:`Cocip`,
|
|
258
|
+
output produced by this interface does not include trailing NaN values.
|
|
259
|
+
|
|
260
|
+
**Known limitations**
|
|
261
|
+
|
|
262
|
+
- Engine core exit temperature and bypass area are not provided as output by pycontrails
|
|
263
|
+
aircraft performance models and are currently set to static values in APCEMM input files.
|
|
264
|
+
These parameters will be computed dynamically in a future release.
|
|
265
|
+
- APCEMM does not compute contrail radiative forcing internally. Radiative forcing must be
|
|
266
|
+
computed offline by the user. Tools for radiative forcing calculations may be included
|
|
267
|
+
in a future version of the interface.
|
|
268
|
+
- APCEMM currently produces different results in simulations that do not read vertical
|
|
269
|
+
velocity data from netCDF input files and in simulations that read vertical velocities
|
|
270
|
+
that are set to 0 everywhere (see https://github.com/MIT-LAE/APCEMM/issues/17).
|
|
271
|
+
Reading of vertical velocity data from netCDF input files will be disabled in this
|
|
272
|
+
interface until this issue is resolved.
|
|
273
|
+
|
|
274
|
+
References
|
|
275
|
+
----------
|
|
276
|
+
- :cite:`fritzRolePlumescaleProcesses2020`
|
|
277
|
+
"""
|
|
278
|
+
|
|
279
|
+
__slots__ = (
|
|
280
|
+
"_trajectory_downsampling",
|
|
281
|
+
"apcemm_input_params",
|
|
282
|
+
"apcemm_path",
|
|
283
|
+
"cachestore",
|
|
284
|
+
"contrail",
|
|
285
|
+
"trajectories",
|
|
286
|
+
"vortex",
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
name = "apcemm"
|
|
290
|
+
long_name = "Interface to APCEMM plume model"
|
|
291
|
+
met_variables = (
|
|
292
|
+
AirTemperature,
|
|
293
|
+
SpecificHumidity,
|
|
294
|
+
(Geopotential, GeopotentialHeight),
|
|
295
|
+
EastwardWind,
|
|
296
|
+
NorthwardWind,
|
|
297
|
+
VerticalVelocity,
|
|
298
|
+
)
|
|
299
|
+
default_params = APCEMMParams
|
|
300
|
+
|
|
301
|
+
#: Met data is not optional
|
|
302
|
+
met: MetDataset
|
|
303
|
+
met_required = True
|
|
304
|
+
|
|
305
|
+
#: Path to APCEMM executable
|
|
306
|
+
apcemm_path: pathlib.Path
|
|
307
|
+
|
|
308
|
+
#: Overridden APCEMM input parameters
|
|
309
|
+
apcemm_input_params: dict[str, Any]
|
|
310
|
+
|
|
311
|
+
#: CacheStore for APCEMM run directories
|
|
312
|
+
cachestore: cache.CacheStore
|
|
313
|
+
|
|
314
|
+
#: Last flight processed in :meth:`eval`
|
|
315
|
+
source: Flight
|
|
316
|
+
|
|
317
|
+
#: Output from trajectory calculation
|
|
318
|
+
trajectories: GeoVectorDataset | None
|
|
319
|
+
|
|
320
|
+
#: Time series output from the APCEMM early plume model
|
|
321
|
+
vortex: pd.DataFrame | None
|
|
322
|
+
|
|
323
|
+
#: Paths to APCEMM netCDF output at prescribed time intervals
|
|
324
|
+
contrail: pd.DataFrame | None
|
|
325
|
+
|
|
326
|
+
#: Downsampling factor from trajectory time resolution to
|
|
327
|
+
#: APCEMM met input file resolution
|
|
328
|
+
_trajectory_downsampling: int
|
|
329
|
+
|
|
330
|
+
def __init__(
|
|
331
|
+
self,
|
|
332
|
+
met: MetDataset,
|
|
333
|
+
apcemm_path: pathlib.Path | str,
|
|
334
|
+
apcemm_root: pathlib.Path | str | None = None,
|
|
335
|
+
apcemm_input_params: dict[str, Any] | None = None,
|
|
336
|
+
cachestore: cache.CacheStore | None = None,
|
|
337
|
+
params: dict[str, Any] | None = None,
|
|
338
|
+
**params_kwargs: Any,
|
|
339
|
+
) -> None:
|
|
340
|
+
super().__init__(met, params=params, **params_kwargs)
|
|
341
|
+
self._ensure_geopotential_height()
|
|
342
|
+
|
|
343
|
+
if isinstance(apcemm_path, str):
|
|
344
|
+
apcemm_path = pathlib.Path(apcemm_path)
|
|
345
|
+
self.apcemm_path = apcemm_path
|
|
346
|
+
|
|
347
|
+
if cachestore is None:
|
|
348
|
+
cache_root = cache._get_user_cache_dir()
|
|
349
|
+
cache_dir = f"{cache_root}/apcemm"
|
|
350
|
+
cachestore = cache.DiskCacheStore(cache_dir=cache_dir)
|
|
351
|
+
self.cachestore = cachestore
|
|
352
|
+
|
|
353
|
+
# Validate overridden input parameters
|
|
354
|
+
apcemm_input_params = apcemm_input_params or {}
|
|
355
|
+
if apcemm_root is not None:
|
|
356
|
+
apcemm_input_params = {
|
|
357
|
+
"input_background_conditions": os.path.join(apcemm_root, "input_data", "init.txt"),
|
|
358
|
+
"input_engine_emissions": os.path.join(apcemm_root, "input_data", "ENG_EI.txt"),
|
|
359
|
+
} | apcemm_input_params
|
|
360
|
+
cannot_override = set(apcemm_input_params.keys()).intersection(self.dynamic_yaml_params)
|
|
361
|
+
if len(cannot_override) > 0:
|
|
362
|
+
msg = (
|
|
363
|
+
f"Cannot override APCEMM input parameters {cannot_override}, "
|
|
364
|
+
"as these parameters are set automatically by the APCEMM interface."
|
|
365
|
+
)
|
|
366
|
+
raise ValueError(msg)
|
|
367
|
+
self.apcemm_input_params = apcemm_input_params
|
|
368
|
+
|
|
369
|
+
@overload
|
|
370
|
+
def eval(self, source: Flight, **params: Any) -> Flight: ...
|
|
371
|
+
|
|
372
|
+
@overload
|
|
373
|
+
def eval(self, source: None = ..., **params: Any) -> NoReturn: ...
|
|
374
|
+
|
|
375
|
+
def eval(self, source: Flight | None = None, **params: Any) -> Flight:
|
|
376
|
+
"""Set up and run APCEMM simulations initialized at flight waypoints.
|
|
377
|
+
|
|
378
|
+
Simulates the formation and evolution of contrails from a Flight
|
|
379
|
+
using the APCEMM plume model described in Fritz et. al. (2020)
|
|
380
|
+
:cite:`fritzRolePlumescaleProcesses2020`.
|
|
381
|
+
|
|
382
|
+
Parameters
|
|
383
|
+
----------
|
|
384
|
+
source : Flight | None
|
|
385
|
+
Input Flight to model.
|
|
386
|
+
**params : Any
|
|
387
|
+
Overwrite model parameters before eval.
|
|
388
|
+
|
|
389
|
+
Returns
|
|
390
|
+
-------
|
|
391
|
+
Flight | NoReturn
|
|
392
|
+
Flight with exit status of APCEMM simulations. Detailed APCEMM outputs are attached
|
|
393
|
+
to model :attr:`vortex` and :attr:`contrail` attributes (see :class:`APCEMM` notes
|
|
394
|
+
for details).
|
|
395
|
+
|
|
396
|
+
References
|
|
397
|
+
----------
|
|
398
|
+
- :cite:`fritzRolePlumescaleProcesses2020`
|
|
399
|
+
"""
|
|
400
|
+
|
|
401
|
+
self.update_params(params)
|
|
402
|
+
self.set_source(source)
|
|
403
|
+
self.source = self.require_source_type(Flight)
|
|
404
|
+
|
|
405
|
+
# Assign waypoints to flight if not already present
|
|
406
|
+
if "waypoint" not in self.source:
|
|
407
|
+
self.source["waypoint"] = np.arange(self.source.size)
|
|
408
|
+
|
|
409
|
+
logger.info("Attaching APCEMM initial conditions to source")
|
|
410
|
+
self.attach_apcemm_initial_conditions()
|
|
411
|
+
|
|
412
|
+
logger.info("Computing Lagrangian trajectories")
|
|
413
|
+
self.compute_lagrangian_trajectories()
|
|
414
|
+
|
|
415
|
+
# Select waypoints to run in APCEMM
|
|
416
|
+
# Defaults to all waypoints, but allows user to select a subset
|
|
417
|
+
waypoints = self.params["waypoints"]
|
|
418
|
+
if waypoints is None:
|
|
419
|
+
waypoints = list(self.source["waypoints"])
|
|
420
|
+
|
|
421
|
+
# Generate input files (serial)
|
|
422
|
+
logger.info("Generating APCEMM input files") # serial
|
|
423
|
+
for waypoint in waypoints:
|
|
424
|
+
rundir = self.apcemm_file(waypoint)
|
|
425
|
+
if self.cachestore.exists(rundir) and not self.params["overwrite"]:
|
|
426
|
+
msg = f"APCEMM run directory already exists at {rundir}"
|
|
427
|
+
raise ValueError(msg)
|
|
428
|
+
if self.cachestore.exists(rundir) and self.params["overwrite"]:
|
|
429
|
+
shutil.rmtree(rundir)
|
|
430
|
+
self.generate_apcemm_input(waypoint)
|
|
431
|
+
|
|
432
|
+
# Run APCEMM (parallelizable)
|
|
433
|
+
logger.info("Running APCEMM")
|
|
434
|
+
self.run_apcemm(waypoints)
|
|
435
|
+
|
|
436
|
+
# Process output (serial)
|
|
437
|
+
logger.info("Postprocessing APCEMM output")
|
|
438
|
+
self.process_apcemm_output()
|
|
439
|
+
|
|
440
|
+
return self.source
|
|
441
|
+
|
|
442
|
+
def attach_apcemm_initial_conditions(self) -> None:
|
|
443
|
+
"""Compute fields required for APCEMM initial conditions and attach to :attr:`source`.
|
|
444
|
+
|
|
445
|
+
This modifies :attr:`source` by attaching quantities derived from meterology
|
|
446
|
+
data and aircraft performance calculations.
|
|
447
|
+
"""
|
|
448
|
+
|
|
449
|
+
self._attach_apcemm_time()
|
|
450
|
+
self._attach_initial_met()
|
|
451
|
+
self._attach_aircraft_performance()
|
|
452
|
+
|
|
453
|
+
def compute_lagrangian_trajectories(self) -> None:
|
|
454
|
+
"""Calculate Lagrangian trajectories using a :class:`DryAdvection` model.
|
|
455
|
+
|
|
456
|
+
Lagrangian trajectories provide the expected time-dependent location
|
|
457
|
+
(longitude, latitude, and altitude) and orientation (azimuth) of
|
|
458
|
+
contrails formed by the input source. This information is used to
|
|
459
|
+
extract time series of meteorological profiles at the contrail location
|
|
460
|
+
from input meteorology data, and to compute contrail-normal horizontal shear
|
|
461
|
+
from horizontal winds.
|
|
462
|
+
|
|
463
|
+
The length of Lagrangian trajectories is set by the ``max_age`` parameter,
|
|
464
|
+
and trajectories are integrated using a time step set by the ``dt_lagrangian``
|
|
465
|
+
parameter. Contrails are advected both horizontally and vertically, and a
|
|
466
|
+
fixed sedimentation velocity (set by the ``sedimentation_rate`` parameter)
|
|
467
|
+
can be included to represent contrail sedimentation.
|
|
468
|
+
|
|
469
|
+
Results of the trajectory calculation are attached to :attr:`trajectories`.
|
|
470
|
+
"""
|
|
471
|
+
|
|
472
|
+
buffers = {
|
|
473
|
+
f"{coord}_buffer": self.params[f"met_{coord}_buffer"]
|
|
474
|
+
for coord in ("longitude", "latitude", "level")
|
|
475
|
+
}
|
|
476
|
+
buffers["time_buffer"] = (0, self.params["max_age"] + self.params["dt_lagrangian"])
|
|
477
|
+
met = self.source.downselect_met(self.met, **buffers)
|
|
478
|
+
model = DryAdvection(
|
|
479
|
+
met=met,
|
|
480
|
+
dt_integration=self.params["dt_lagrangian"],
|
|
481
|
+
max_age=self.params["max_age"],
|
|
482
|
+
sedimentation_rate=self.params["lagrangian_sedimentation_rate"],
|
|
483
|
+
)
|
|
484
|
+
self.trajectories = model.eval(self.source)
|
|
485
|
+
|
|
486
|
+
def generate_apcemm_input(self, waypoint: int) -> None:
|
|
487
|
+
"""Generate APCEMM yaml and netCDF input files for a single waypoint.
|
|
488
|
+
|
|
489
|
+
For details about generated input files, see :class:`APCEMM` notes.
|
|
490
|
+
|
|
491
|
+
Parameters
|
|
492
|
+
----------
|
|
493
|
+
waypoint : int
|
|
494
|
+
Waypoint for which to generate input files.
|
|
495
|
+
"""
|
|
496
|
+
|
|
497
|
+
self._gen_apcemm_yaml(waypoint)
|
|
498
|
+
self._gen_apcemm_nc(waypoint)
|
|
499
|
+
|
|
500
|
+
def run_apcemm(self, waypoints: list[int]) -> None:
|
|
501
|
+
"""Run APCEMM over multiple waypoints.
|
|
502
|
+
|
|
503
|
+
Multiple waypoints will be processed in parallel if the :class:`APCEMM`
|
|
504
|
+
``n_jobs`` parameter is set to a value larger than 1.
|
|
505
|
+
|
|
506
|
+
Parameters
|
|
507
|
+
----------
|
|
508
|
+
waypoints : list[int]
|
|
509
|
+
List of waypoints at which to initialize simulations.
|
|
510
|
+
"""
|
|
511
|
+
|
|
512
|
+
# run in series
|
|
513
|
+
if self.params["n_jobs"] == 1:
|
|
514
|
+
for waypoint in waypoints:
|
|
515
|
+
utils.run(
|
|
516
|
+
apcemm_path=self.apcemm_path,
|
|
517
|
+
input_yaml=self.apcemm_file(waypoint, "input.yaml"),
|
|
518
|
+
rundir=self.apcemm_file(waypoint),
|
|
519
|
+
stdout_log=self.apcemm_file(waypoint, "stdout.log"),
|
|
520
|
+
stderr_log=self.apcemm_file(waypoint, "stderr.log"),
|
|
521
|
+
)
|
|
522
|
+
|
|
523
|
+
# run in parallel
|
|
524
|
+
else:
|
|
525
|
+
with mp.Pool(self.params["n_jobs"]) as p:
|
|
526
|
+
args = (
|
|
527
|
+
(
|
|
528
|
+
self.apcemm_path,
|
|
529
|
+
self.apcemm_file(waypoint, "input.yaml"),
|
|
530
|
+
self.apcemm_file(waypoint),
|
|
531
|
+
self.apcemm_file(waypoint, "stdout.log"),
|
|
532
|
+
self.apcemm_file(waypoint, "stderr.log"),
|
|
533
|
+
)
|
|
534
|
+
for waypoint in waypoints
|
|
535
|
+
)
|
|
536
|
+
p.starmap(utils.run, args)
|
|
537
|
+
|
|
538
|
+
def process_apcemm_output(self) -> None:
|
|
539
|
+
"""Process APCEMM output.
|
|
540
|
+
|
|
541
|
+
After processing, a ``status`` column will be attached to
|
|
542
|
+
:attr:`source`, and additional output data will be attached
|
|
543
|
+
to :attr:`vortex` and :attr:`contrail`. For details about
|
|
544
|
+
contents of APCEMM output files, see :class:`APCEMM` notes.
|
|
545
|
+
"""
|
|
546
|
+
|
|
547
|
+
output_directory = self.params["output_directory"]
|
|
548
|
+
|
|
549
|
+
statuses: list[str] = []
|
|
550
|
+
vortexes: list[pd.DataFrame] = []
|
|
551
|
+
contrails: list[pd.DataFrame] = []
|
|
552
|
+
|
|
553
|
+
for _, row in self.source.dataframe.iterrows():
|
|
554
|
+
waypoint = row["waypoint"]
|
|
555
|
+
|
|
556
|
+
# Mark waypoint as skipped if no APCEMM simulation ran
|
|
557
|
+
if waypoint not in self.params["waypoints"]:
|
|
558
|
+
statuses.append("NoSimulation")
|
|
559
|
+
continue
|
|
560
|
+
|
|
561
|
+
# Otherwise, record status of APCEMM simulation
|
|
562
|
+
with open(
|
|
563
|
+
self.apcemm_file(waypoint, os.path.join(output_directory, "status_case0"))
|
|
564
|
+
) as f:
|
|
565
|
+
status = f.read().strip()
|
|
566
|
+
statuses.append(status)
|
|
567
|
+
|
|
568
|
+
# Get waypoint initialization time
|
|
569
|
+
base_time = row["time"]
|
|
570
|
+
|
|
571
|
+
# Convert contents of wake vortex output to pandas dataframe
|
|
572
|
+
# with elapsed times converted to absolute times
|
|
573
|
+
vortex = pd.read_csv(
|
|
574
|
+
self.apcemm_file(waypoint, os.path.join(output_directory, "Micro000000.out")),
|
|
575
|
+
skiprows=[1],
|
|
576
|
+
).rename(columns=lambda x: x.strip())
|
|
577
|
+
time = (base_time + pd.to_timedelta(vortex["Time [s]"], unit="s")).rename("time")
|
|
578
|
+
waypoint_col = pd.Series(np.full((len(vortex),), waypoint), name="waypoint")
|
|
579
|
+
vortex = pd.concat(
|
|
580
|
+
(waypoint_col, time, vortex.drop(columns="Time [s]")), axis="columns"
|
|
581
|
+
)
|
|
582
|
+
vortexes.append(vortex)
|
|
583
|
+
|
|
584
|
+
# Record paths to contrail output (netCDF files) in pandas dataframe
|
|
585
|
+
# get paths to contrail output
|
|
586
|
+
files = sorted(
|
|
587
|
+
glob.glob(
|
|
588
|
+
self.apcemm_file(
|
|
589
|
+
waypoint, os.path.join(output_directory, "ts_aerosol_case0_*.nc")
|
|
590
|
+
)
|
|
591
|
+
)
|
|
592
|
+
)
|
|
593
|
+
if len(files) == 0:
|
|
594
|
+
continue
|
|
595
|
+
time = []
|
|
596
|
+
path = []
|
|
597
|
+
for file in files:
|
|
598
|
+
elapsed_hours = pd.to_timedelta(file[-7:-5] + "h")
|
|
599
|
+
elapsed_minutes = pd.to_timedelta(file[-5:-3] + "m")
|
|
600
|
+
elapsed_time = elapsed_hours + elapsed_minutes
|
|
601
|
+
time.append(base_time + elapsed_time)
|
|
602
|
+
path.append(file)
|
|
603
|
+
waypoint_col = pd.Series(np.full((len(time),), waypoint), name="waypoint")
|
|
604
|
+
contrail = pd.DataFrame.from_dict(
|
|
605
|
+
{
|
|
606
|
+
"waypoint": waypoint_col,
|
|
607
|
+
"time": time,
|
|
608
|
+
"path": path,
|
|
609
|
+
}
|
|
610
|
+
)
|
|
611
|
+
contrails.append(contrail)
|
|
612
|
+
|
|
613
|
+
# Attach status to self
|
|
614
|
+
self.source["status"] = statuses
|
|
615
|
+
|
|
616
|
+
# Attach wake vortex and contrail outputs to model
|
|
617
|
+
self.vortex = pd.concat(vortexes, axis="index", ignore_index=True)
|
|
618
|
+
if len(contrails) > 0: # only present if APCEMM simulates persistent contrails
|
|
619
|
+
self.contrail = pd.concat(contrails, axis="index", ignore_index=True)
|
|
620
|
+
|
|
621
|
+
@property
|
|
622
|
+
def dynamic_yaml_params(self) -> set[str]:
|
|
623
|
+
"""Set of :class:`APCEMMInput` attributes set dynamically by this model.
|
|
624
|
+
|
|
625
|
+
Other :class:`APCEMMInput` attributes can be set statically by passing
|
|
626
|
+
parameters in ``apcemm_input_params`` to the :class:`APCEMM` constructor.
|
|
627
|
+
"""
|
|
628
|
+
return {
|
|
629
|
+
"max_age",
|
|
630
|
+
"day_of_year",
|
|
631
|
+
"hour_of_day",
|
|
632
|
+
"longitude",
|
|
633
|
+
"latitude",
|
|
634
|
+
"air_pressure",
|
|
635
|
+
"air_temperature",
|
|
636
|
+
"rhw",
|
|
637
|
+
"normal_shear",
|
|
638
|
+
"brunt_vaisala_frequency",
|
|
639
|
+
"dt_input_met",
|
|
640
|
+
"nox_ei",
|
|
641
|
+
"co_ei",
|
|
642
|
+
"hc_ei",
|
|
643
|
+
"so2_ei",
|
|
644
|
+
"nvpm_ei_m",
|
|
645
|
+
"soot_radius",
|
|
646
|
+
"fuel_flow",
|
|
647
|
+
"aircraft_mass",
|
|
648
|
+
"true_airspeed",
|
|
649
|
+
"n_engine",
|
|
650
|
+
"wingspan",
|
|
651
|
+
"output_directory",
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
def apcemm_file(self, waypoint: int, name: str | None = None) -> str:
|
|
655
|
+
"""Get path to file from an APCEMM simulation initialized at a specific waypoint.
|
|
656
|
+
|
|
657
|
+
Parameters
|
|
658
|
+
----------
|
|
659
|
+
waypoint : int
|
|
660
|
+
Segment index
|
|
661
|
+
name : str, optional
|
|
662
|
+
If provided, the path to the file relative to the APCEMM simulation
|
|
663
|
+
root directory.
|
|
664
|
+
|
|
665
|
+
Returns
|
|
666
|
+
-------
|
|
667
|
+
str
|
|
668
|
+
Path to a file in the APCEMM simulation root directory, if ``name``
|
|
669
|
+
is provided, or the path to the APCEMM simulation root directory otherwise.
|
|
670
|
+
"""
|
|
671
|
+
rpath = f"apcemm_waypoint_{waypoint}"
|
|
672
|
+
if name is not None:
|
|
673
|
+
rpath = os.path.join(rpath, name)
|
|
674
|
+
return self.cachestore.path(rpath)
|
|
675
|
+
|
|
676
|
+
def _ensure_geopotential_height(self) -> None:
|
|
677
|
+
"""Ensure that :attr:`self.met` contains geopotential height."""
|
|
678
|
+
geopotential = Geopotential.standard_name
|
|
679
|
+
geopotential_height = GeopotentialHeight.standard_name
|
|
680
|
+
|
|
681
|
+
if geopotential not in self.met and geopotential_height not in self.met:
|
|
682
|
+
msg = f"APCEMM MetDataset must contain either {geopotential} or {geopotential_height}."
|
|
683
|
+
raise ValueError(msg)
|
|
684
|
+
|
|
685
|
+
if geopotential_height not in self.met:
|
|
686
|
+
self.met.update({geopotential_height: self.met[geopotential].data / constants.g})
|
|
687
|
+
|
|
688
|
+
def _attach_apcemm_time(self) -> None:
|
|
689
|
+
"""Attach day of year and fractional hour of day.
|
|
690
|
+
|
|
691
|
+
Mutates :attr:`self.source` by adding the following keys if not already present:
|
|
692
|
+
- ``day_of_year``
|
|
693
|
+
- ``hour_of_day``
|
|
694
|
+
"""
|
|
695
|
+
|
|
696
|
+
self.source.setdefault(
|
|
697
|
+
"day_of_year",
|
|
698
|
+
# APCEMM doesn't accept 366 on leap years
|
|
699
|
+
self.source.dataframe["time"].dt.dayofyear.clip(upper=365),
|
|
700
|
+
)
|
|
701
|
+
self.source.setdefault(
|
|
702
|
+
"hour_of_day",
|
|
703
|
+
self.source.dataframe["time"].dt.hour
|
|
704
|
+
+ self.source.dataframe["time"].dt.minute / 60
|
|
705
|
+
+ self.source.dataframe["time"].dt.second / 3600,
|
|
706
|
+
)
|
|
707
|
+
|
|
708
|
+
def _attach_initial_met(self) -> None:
|
|
709
|
+
"""Attach meteorological fields for APCEMM initialization.
|
|
710
|
+
|
|
711
|
+
Mutates :attr:`source` by adding the following keys if not already present:
|
|
712
|
+
- ``air_temperature``
|
|
713
|
+
- ``eastward_wind``
|
|
714
|
+
- ``northward_wind``
|
|
715
|
+
- ``specific_humidity``
|
|
716
|
+
- ``air_temperature_lower``
|
|
717
|
+
- ``eastward_wind_lower``
|
|
718
|
+
- ``northward_wind_lower``
|
|
719
|
+
- ``rhw``
|
|
720
|
+
- ``brunt_vaisala_frequency``
|
|
721
|
+
- ``normal_shear``
|
|
722
|
+
"""
|
|
723
|
+
humidity_scaling = self.params["humidity_scaling"]
|
|
724
|
+
scale_humidity = humidity_scaling is not None and "specific_humidity" not in self.source
|
|
725
|
+
|
|
726
|
+
# Downselect met before interpolation.
|
|
727
|
+
# Need buffer in downward direction for calculation of vertical derivatives,
|
|
728
|
+
# but not in any other directions.
|
|
729
|
+
level_buffer = 0, self.params["met_level_buffer"][1]
|
|
730
|
+
met = self.source.downselect_met(self.met, level_buffer=level_buffer)
|
|
731
|
+
|
|
732
|
+
# Interpolate meteorology data onto vector
|
|
733
|
+
for met_key in ("air_temperature", "eastward_wind", "northward_wind", "specific_humidity"):
|
|
734
|
+
models.interpolate_met(met, self.source, met_key, **self.interp_kwargs)
|
|
735
|
+
|
|
736
|
+
# Interpolate fields at lower levels for vertical derivative calculation
|
|
737
|
+
air_pressure_lower = thermo.pressure_dz(
|
|
738
|
+
self.source["air_temperature"], self.source.air_pressure, self.params["dz_m"]
|
|
739
|
+
)
|
|
740
|
+
lower_level = air_pressure_lower / 100.0
|
|
741
|
+
for met_key in ("air_temperature", "eastward_wind", "northward_wind"):
|
|
742
|
+
source_key = f"{met_key}_lower"
|
|
743
|
+
models.interpolate_met(
|
|
744
|
+
met, self.source, met_key, source_key, **self.interp_kwargs, level=lower_level
|
|
745
|
+
)
|
|
746
|
+
|
|
747
|
+
# Apply humidity scaling
|
|
748
|
+
if scale_humidity:
|
|
749
|
+
humidity_scaling.eval(self.source, copy_source=False)
|
|
750
|
+
|
|
751
|
+
# Compute RH over liquid water
|
|
752
|
+
self.source.setdefault(
|
|
753
|
+
"rhw",
|
|
754
|
+
thermo.rh(
|
|
755
|
+
self.source["specific_humidity"],
|
|
756
|
+
self.source["air_temperature"],
|
|
757
|
+
self.source.air_pressure,
|
|
758
|
+
),
|
|
759
|
+
)
|
|
760
|
+
|
|
761
|
+
# Compute Brunt-Vaisala frequency
|
|
762
|
+
dT_dz = thermo.T_potential_gradient(
|
|
763
|
+
self.source["air_temperature"],
|
|
764
|
+
self.source.air_pressure,
|
|
765
|
+
self.source["air_temperature_lower"],
|
|
766
|
+
air_pressure_lower,
|
|
767
|
+
self.params["dz_m"],
|
|
768
|
+
)
|
|
769
|
+
self.source.setdefault(
|
|
770
|
+
"brunt_vaisala_frequency",
|
|
771
|
+
thermo.brunt_vaisala_frequency(
|
|
772
|
+
self.source.air_pressure, self.source["air_temperature"], dT_dz
|
|
773
|
+
),
|
|
774
|
+
)
|
|
775
|
+
|
|
776
|
+
# Compute azimuth
|
|
777
|
+
# Use forward and backward differences for first and last waypoints
|
|
778
|
+
# and centered differences elsewhere
|
|
779
|
+
ileft = [0, *range(self.source.size - 1)]
|
|
780
|
+
iright = [*range(1, self.source.size), self.source.size - 1]
|
|
781
|
+
lon0 = self.source["longitude"][ileft]
|
|
782
|
+
lat0 = self.source["latitude"][ileft]
|
|
783
|
+
lon1 = self.source["longitude"][iright]
|
|
784
|
+
lat1 = self.source["latitude"][iright]
|
|
785
|
+
self.source.setdefault("azimuth", geo.azimuth(lon0, lat0, lon1, lat1))
|
|
786
|
+
|
|
787
|
+
# Compute normal shear
|
|
788
|
+
self.source.setdefault(
|
|
789
|
+
"normal_shear",
|
|
790
|
+
utils.normal_wind_shear(
|
|
791
|
+
self.source["eastward_wind"],
|
|
792
|
+
self.source["eastward_wind_lower"],
|
|
793
|
+
self.source["northward_wind"],
|
|
794
|
+
self.source["northward_wind_lower"],
|
|
795
|
+
self.source["azimuth"],
|
|
796
|
+
self.params["dz_m"],
|
|
797
|
+
),
|
|
798
|
+
)
|
|
799
|
+
|
|
800
|
+
def _attach_aircraft_performance(self) -> None:
|
|
801
|
+
"""Attach aircraft performance and emissions parameters.
|
|
802
|
+
|
|
803
|
+
Mutates :attr:`source evaluating the aircraft performance model provided by
|
|
804
|
+
the ``aircraft_performance`` model parameter and a :class:`Emissions` models. In addition:
|
|
805
|
+
- MetDatasetutates :attr:`source` by adding the following keys if not already present:
|
|
806
|
+
- ``soot_radius``
|
|
807
|
+
- Mutates :attr:`source.attrs` by adding the following keys if not already present:
|
|
808
|
+
- ``so2_ei``
|
|
809
|
+
"""
|
|
810
|
+
|
|
811
|
+
ap_model = self.params["aircraft_performance"]
|
|
812
|
+
emissions = Emissions()
|
|
813
|
+
humidity_scaling = self.params["humidity_scaling"]
|
|
814
|
+
scale_humidity = humidity_scaling is not None and "specific_humidity" not in self.source
|
|
815
|
+
|
|
816
|
+
# Ensure required met data is present.
|
|
817
|
+
# No buffers needed for interpolation!
|
|
818
|
+
vars = ap_model.met_variables + ap_model.optional_met_variables + emissions.met_variables
|
|
819
|
+
met = self.source.downselect_met(self.met)
|
|
820
|
+
met.ensure_vars(vars)
|
|
821
|
+
met.standardize_variables(vars)
|
|
822
|
+
for var in vars:
|
|
823
|
+
models.interpolate_met(met, self.source, var.standard_name, **self.interp_kwargs)
|
|
824
|
+
|
|
825
|
+
# Apply humidity scaling
|
|
826
|
+
if scale_humidity:
|
|
827
|
+
humidity_scaling.eval(self.source, copy_source=False)
|
|
828
|
+
|
|
829
|
+
# Ensure flight has aircraft type, fuel, and engine UID if defined
|
|
830
|
+
self.source.attrs.setdefault("aircraft_type", self.params["aircraft_type"])
|
|
831
|
+
self.source.attrs.setdefault("fuel", self.params["fuel"])
|
|
832
|
+
if self.params["engine_uid"]:
|
|
833
|
+
self.source.attrs.setdefault("engine_uid", self.params["engine_uid"])
|
|
834
|
+
|
|
835
|
+
# Run performance and emissions calculations
|
|
836
|
+
ap_model.eval(self.source, copy_source=False)
|
|
837
|
+
emissions.eval(self.source, copy_source=False)
|
|
838
|
+
|
|
839
|
+
# Attach additional required quantities
|
|
840
|
+
soot_radius = utils.soot_radius(self.source["nvpm_ei_m"], self.source["nvpm_ei_n"])
|
|
841
|
+
self.source.setdefault("soot_radius", soot_radius)
|
|
842
|
+
self.source.attrs.setdefault("so2_ei", self.source.attrs["fuel"].ei_so2)
|
|
843
|
+
|
|
844
|
+
def _gen_apcemm_yaml(self, waypoint: int) -> None:
|
|
845
|
+
"""Generate APCEMM yaml file.
|
|
846
|
+
|
|
847
|
+
Parameters
|
|
848
|
+
----------
|
|
849
|
+
waypoint : int
|
|
850
|
+
Waypoint for which to generate the yaml file.
|
|
851
|
+
"""
|
|
852
|
+
|
|
853
|
+
# Collect parameters determined by this interface
|
|
854
|
+
dyn_params = _combine_prioritized(
|
|
855
|
+
self.dynamic_yaml_params,
|
|
856
|
+
[
|
|
857
|
+
self.source.dataframe.loc[waypoint], # flight waypoint
|
|
858
|
+
self.source.attrs, # flight attributes
|
|
859
|
+
self.params, # class parameters
|
|
860
|
+
],
|
|
861
|
+
)
|
|
862
|
+
|
|
863
|
+
# Combine with other overridden parameters
|
|
864
|
+
params = self.apcemm_input_params | dyn_params
|
|
865
|
+
|
|
866
|
+
# We should be setting these parameters based on aircraft data,
|
|
867
|
+
# but we don't currently have an easy way to do this.
|
|
868
|
+
# For now, stubbing in static values.
|
|
869
|
+
params = params | {"core_exit_temp": 550.0, "core_exit_area": 1.0}
|
|
870
|
+
|
|
871
|
+
# Generate and write YAML file
|
|
872
|
+
yaml = APCEMMInput(**params)
|
|
873
|
+
yaml_contents = utils.generate_apcemm_input_yaml(yaml)
|
|
874
|
+
path = self.apcemm_file(waypoint, "input.yaml")
|
|
875
|
+
with open(path, "w") as f:
|
|
876
|
+
f.write(yaml_contents)
|
|
877
|
+
|
|
878
|
+
def _gen_apcemm_nc(self, waypoint: int) -> None:
|
|
879
|
+
"""Generate APCEMM meteorology netCDF file.
|
|
880
|
+
|
|
881
|
+
Parameters
|
|
882
|
+
----------
|
|
883
|
+
waypoint : int
|
|
884
|
+
Waypoint for which to generate the meteorology file.
|
|
885
|
+
"""
|
|
886
|
+
# Extract trajectories of advected contrails, include initial position
|
|
887
|
+
columns = ["longitude", "latitude", "time", "azimuth"]
|
|
888
|
+
if self.trajectories is None:
|
|
889
|
+
msg = (
|
|
890
|
+
"APCEMM meteorology input generation requires precomputed trajectories. "
|
|
891
|
+
"To compute trajectories, call `compute_lagrangian_trajectories`."
|
|
892
|
+
)
|
|
893
|
+
raise ValueError(msg)
|
|
894
|
+
tail = self.trajectories.dataframe
|
|
895
|
+
tail = tail[tail["waypoint"] == waypoint][columns]
|
|
896
|
+
head = self.source.dataframe
|
|
897
|
+
head = head[head["waypoint"] == waypoint][columns]
|
|
898
|
+
traj = pd.concat((head, tail), axis="index").reset_index()
|
|
899
|
+
|
|
900
|
+
# APCEMM requires atmospheric profiles at even time intervals,
|
|
901
|
+
# but the time coordinates of the initial position plus subsequent
|
|
902
|
+
# trajectory may not be evenly spaced. To fix this, we interpolate
|
|
903
|
+
# horizontal location and azimuth to an evenly-spaced set of time
|
|
904
|
+
# coordinates.
|
|
905
|
+
time = traj["time"].values
|
|
906
|
+
n_profiles = int(self.params["max_age"] / self.params["dt_input_met"]) + 1
|
|
907
|
+
tick = np.timedelta64(1, "s")
|
|
908
|
+
target_elapsed = np.linspace(
|
|
909
|
+
0, (n_profiles - 1) * self.params["dt_input_met"] / tick, n_profiles
|
|
910
|
+
)
|
|
911
|
+
target_time = time[0] + target_elapsed * tick
|
|
912
|
+
elapsed = (traj["time"] - traj["time"][0]) / tick
|
|
913
|
+
|
|
914
|
+
# Need to deal with antimeridian crossing.
|
|
915
|
+
# Detecting antimeridian crossing follows Flight.resample_and_fill,
|
|
916
|
+
# but rather than applying a variable shift we just convert longitudes to
|
|
917
|
+
# [0, 360) before interpolating flights that cross the antimeridian,
|
|
918
|
+
# and un-convert any longitudes above 180 degree after interpolation.
|
|
919
|
+
lon = traj["longitude"].values
|
|
920
|
+
min_pos = np.min(lon[lon > 0], initial=np.inf)
|
|
921
|
+
max_neg = np.max(lon[lon < 0], initial=-np.inf)
|
|
922
|
+
if (180 - min_pos) + (180 + max_neg) < 180 and min_pos < np.inf and max_neg > -np.inf:
|
|
923
|
+
lon = np.where(lon < 0, lon + 360, lon)
|
|
924
|
+
interp_lon = np.interp(target_elapsed, elapsed, lon)
|
|
925
|
+
interp_lon = np.where(interp_lon > 180, interp_lon - 360, interp_lon)
|
|
926
|
+
|
|
927
|
+
interp_lat = np.interp(target_elapsed, elapsed, traj["latitude"].values)
|
|
928
|
+
interp_az = np.interp(target_elapsed, elapsed, traj["azimuth"].values)
|
|
929
|
+
|
|
930
|
+
if self.params["altitude_input_met"] is None:
|
|
931
|
+
altitude = self.met["altitude"].values
|
|
932
|
+
else:
|
|
933
|
+
altitude = np.array(self.params["altitude_input_met"])
|
|
934
|
+
|
|
935
|
+
ds = utils.generate_apcemm_input_met(
|
|
936
|
+
time=target_time,
|
|
937
|
+
longitude=interp_lon,
|
|
938
|
+
latitude=interp_lat,
|
|
939
|
+
azimuth=interp_az,
|
|
940
|
+
altitude=altitude,
|
|
941
|
+
met=self.met,
|
|
942
|
+
humidity_scaling=self.params["humidity_scaling"],
|
|
943
|
+
dz_m=self.params["dz_m"],
|
|
944
|
+
interp_kwargs=self.interp_kwargs,
|
|
945
|
+
)
|
|
946
|
+
|
|
947
|
+
path = self.apcemm_file(waypoint, "input.nc")
|
|
948
|
+
ds.to_netcdf(path)
|
|
949
|
+
|
|
950
|
+
|
|
951
|
+
def _combine_prioritized(keys: set[str], sources: list[dict[str, Any]]) -> dict[str, Any]:
|
|
952
|
+
"""Combine dictionary keys from prioritized list of source dictionaries.
|
|
953
|
+
|
|
954
|
+
Parameters
|
|
955
|
+
----------
|
|
956
|
+
keys : set[str]
|
|
957
|
+
Set of keys to attempt to extract from source dictionary.
|
|
958
|
+
sources : list[dict[str, Any]]
|
|
959
|
+
List of dictionaries from which to attempt to extract key-value pairs.
|
|
960
|
+
If the key is in the first dictionary, it will be set in the returned dictionary
|
|
961
|
+
with the corresponding value. Otherwise, the method will fall on the remaining
|
|
962
|
+
dictionaries in the order provided.
|
|
963
|
+
|
|
964
|
+
Returns
|
|
965
|
+
-------
|
|
966
|
+
dict[str, Any]
|
|
967
|
+
Dictionary containing key-value pairs from :param:`sources`.
|
|
968
|
+
|
|
969
|
+
Raises
|
|
970
|
+
------
|
|
971
|
+
ValueError
|
|
972
|
+
Any key is not found in any dictionary in ``sources``.
|
|
973
|
+
"""
|
|
974
|
+
dest = {}
|
|
975
|
+
for key in keys:
|
|
976
|
+
for source in sources:
|
|
977
|
+
if key in source:
|
|
978
|
+
dest[key] = source[key]
|
|
979
|
+
break
|
|
980
|
+
else:
|
|
981
|
+
msg = f"Key {key} not found in any source dictionary."
|
|
982
|
+
raise ValueError(msg)
|
|
983
|
+
return dest
|