pycontrails 0.53.0__cp313-cp313-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.

Files changed (109) hide show
  1. pycontrails/__init__.py +70 -0
  2. pycontrails/_version.py +16 -0
  3. pycontrails/core/__init__.py +30 -0
  4. pycontrails/core/aircraft_performance.py +641 -0
  5. pycontrails/core/airports.py +226 -0
  6. pycontrails/core/cache.py +881 -0
  7. pycontrails/core/coordinates.py +174 -0
  8. pycontrails/core/fleet.py +470 -0
  9. pycontrails/core/flight.py +2312 -0
  10. pycontrails/core/flightplan.py +220 -0
  11. pycontrails/core/fuel.py +140 -0
  12. pycontrails/core/interpolation.py +721 -0
  13. pycontrails/core/met.py +2833 -0
  14. pycontrails/core/met_var.py +307 -0
  15. pycontrails/core/models.py +1181 -0
  16. pycontrails/core/polygon.py +549 -0
  17. pycontrails/core/rgi_cython.cpython-313-darwin.so +0 -0
  18. pycontrails/core/vector.py +2191 -0
  19. pycontrails/datalib/__init__.py +12 -0
  20. pycontrails/datalib/_leo_utils/search.py +250 -0
  21. pycontrails/datalib/_leo_utils/static/bq_roi_query.sql +6 -0
  22. pycontrails/datalib/_leo_utils/vis.py +59 -0
  23. pycontrails/datalib/_met_utils/metsource.py +743 -0
  24. pycontrails/datalib/ecmwf/__init__.py +53 -0
  25. pycontrails/datalib/ecmwf/arco_era5.py +527 -0
  26. pycontrails/datalib/ecmwf/common.py +109 -0
  27. pycontrails/datalib/ecmwf/era5.py +538 -0
  28. pycontrails/datalib/ecmwf/era5_model_level.py +482 -0
  29. pycontrails/datalib/ecmwf/hres.py +782 -0
  30. pycontrails/datalib/ecmwf/hres_model_level.py +495 -0
  31. pycontrails/datalib/ecmwf/ifs.py +284 -0
  32. pycontrails/datalib/ecmwf/model_levels.py +79 -0
  33. pycontrails/datalib/ecmwf/static/model_level_dataframe_v20240418.csv +139 -0
  34. pycontrails/datalib/ecmwf/variables.py +256 -0
  35. pycontrails/datalib/gfs/__init__.py +28 -0
  36. pycontrails/datalib/gfs/gfs.py +646 -0
  37. pycontrails/datalib/gfs/variables.py +100 -0
  38. pycontrails/datalib/goes.py +772 -0
  39. pycontrails/datalib/landsat.py +568 -0
  40. pycontrails/datalib/sentinel.py +512 -0
  41. pycontrails/datalib/spire.py +739 -0
  42. pycontrails/ext/bada.py +41 -0
  43. pycontrails/ext/cirium.py +14 -0
  44. pycontrails/ext/empirical_grid.py +140 -0
  45. pycontrails/ext/synthetic_flight.py +426 -0
  46. pycontrails/models/__init__.py +1 -0
  47. pycontrails/models/accf.py +406 -0
  48. pycontrails/models/apcemm/__init__.py +8 -0
  49. pycontrails/models/apcemm/apcemm.py +983 -0
  50. pycontrails/models/apcemm/inputs.py +226 -0
  51. pycontrails/models/apcemm/static/apcemm_yaml_template.yaml +183 -0
  52. pycontrails/models/apcemm/utils.py +437 -0
  53. pycontrails/models/cocip/__init__.py +29 -0
  54. pycontrails/models/cocip/cocip.py +2617 -0
  55. pycontrails/models/cocip/cocip_params.py +299 -0
  56. pycontrails/models/cocip/cocip_uncertainty.py +285 -0
  57. pycontrails/models/cocip/contrail_properties.py +1517 -0
  58. pycontrails/models/cocip/output_formats.py +2261 -0
  59. pycontrails/models/cocip/radiative_forcing.py +1262 -0
  60. pycontrails/models/cocip/radiative_heating.py +520 -0
  61. pycontrails/models/cocip/unterstrasser_wake_vortex.py +403 -0
  62. pycontrails/models/cocip/wake_vortex.py +396 -0
  63. pycontrails/models/cocip/wind_shear.py +120 -0
  64. pycontrails/models/cocipgrid/__init__.py +9 -0
  65. pycontrails/models/cocipgrid/cocip_grid.py +2573 -0
  66. pycontrails/models/cocipgrid/cocip_grid_params.py +138 -0
  67. pycontrails/models/dry_advection.py +486 -0
  68. pycontrails/models/emissions/__init__.py +21 -0
  69. pycontrails/models/emissions/black_carbon.py +594 -0
  70. pycontrails/models/emissions/emissions.py +1353 -0
  71. pycontrails/models/emissions/ffm2.py +336 -0
  72. pycontrails/models/emissions/static/default-engine-uids.csv +239 -0
  73. pycontrails/models/emissions/static/edb-gaseous-v29b-engines.csv +596 -0
  74. pycontrails/models/emissions/static/edb-nvpm-v29b-engines.csv +215 -0
  75. pycontrails/models/humidity_scaling/__init__.py +37 -0
  76. pycontrails/models/humidity_scaling/humidity_scaling.py +1025 -0
  77. pycontrails/models/humidity_scaling/quantiles/era5-model-level-quantiles.pq +0 -0
  78. pycontrails/models/humidity_scaling/quantiles/era5-pressure-level-quantiles.pq +0 -0
  79. pycontrails/models/issr.py +210 -0
  80. pycontrails/models/pcc.py +327 -0
  81. pycontrails/models/pcr.py +154 -0
  82. pycontrails/models/ps_model/__init__.py +17 -0
  83. pycontrails/models/ps_model/ps_aircraft_params.py +376 -0
  84. pycontrails/models/ps_model/ps_grid.py +505 -0
  85. pycontrails/models/ps_model/ps_model.py +1017 -0
  86. pycontrails/models/ps_model/ps_operational_limits.py +540 -0
  87. pycontrails/models/ps_model/static/ps-aircraft-params-20240524.csv +68 -0
  88. pycontrails/models/ps_model/static/ps-synonym-list-20240524.csv +103 -0
  89. pycontrails/models/sac.py +459 -0
  90. pycontrails/models/tau_cirrus.py +168 -0
  91. pycontrails/physics/__init__.py +1 -0
  92. pycontrails/physics/constants.py +116 -0
  93. pycontrails/physics/geo.py +989 -0
  94. pycontrails/physics/jet.py +837 -0
  95. pycontrails/physics/thermo.py +451 -0
  96. pycontrails/physics/units.py +472 -0
  97. pycontrails/py.typed +0 -0
  98. pycontrails/utils/__init__.py +1 -0
  99. pycontrails/utils/dependencies.py +66 -0
  100. pycontrails/utils/iteration.py +13 -0
  101. pycontrails/utils/json.py +188 -0
  102. pycontrails/utils/temp.py +50 -0
  103. pycontrails/utils/types.py +165 -0
  104. pycontrails-0.53.0.dist-info/LICENSE +178 -0
  105. pycontrails-0.53.0.dist-info/METADATA +181 -0
  106. pycontrails-0.53.0.dist-info/NOTICE +43 -0
  107. pycontrails-0.53.0.dist-info/RECORD +109 -0
  108. pycontrails-0.53.0.dist-info/WHEEL +5 -0
  109. pycontrails-0.53.0.dist-info/top_level.txt +3 -0
@@ -0,0 +1,299 @@
1
+ """Default parameters for CoCiP models.
2
+
3
+ Used by :class:`Cocip` and :class:`CocipGrid`.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import dataclasses
9
+
10
+ import numpy as np
11
+ import numpy.typing as npt
12
+
13
+ from pycontrails.core.aircraft_performance import AircraftPerformance
14
+ from pycontrails.core.models import ModelParams
15
+ from pycontrails.models.emissions.emissions import EmissionsParams
16
+ from pycontrails.models.humidity_scaling import HumidityScaling
17
+
18
+
19
+ def _radius_threshold_um() -> npt.NDArray[np.float32]:
20
+ return np.array([5.0, 9.5, 23.0, 190.0, 310.0], dtype=np.float32)
21
+
22
+
23
+ def _habit_distributions() -> npt.NDArray[np.float32]:
24
+ return np.array(
25
+ [
26
+ [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0],
27
+ [0.0, 0.3, 0.0, 0.0, 0.0, 0.0, 0.7, 0.0],
28
+ [0.0, 0.3, 0.0, 0.0, 0.3, 0.0, 0.4, 0.0],
29
+ [0.0, 0.5, 0.0, 0.0, 0.15, 0.35, 0.0, 0.0],
30
+ [0.0, 0.45, 0.45, 0.1, 0.0, 0.0, 0.0, 0.0],
31
+ [0.0, 0.0, 0.0, 0.03, 0.97, 0.0, 0.0, 0.0],
32
+ ],
33
+ dtype=np.float32,
34
+ )
35
+
36
+
37
+ def _habits() -> npt.NDArray[np.str_]:
38
+ return np.array(
39
+ [
40
+ "Sphere",
41
+ "Solid column",
42
+ "Hollow column",
43
+ "Rough aggregate",
44
+ "Rosette-6",
45
+ "Plate",
46
+ "Droxtal",
47
+ "Myhre",
48
+ ]
49
+ )
50
+
51
+
52
+ @dataclasses.dataclass
53
+ class CocipParams(ModelParams):
54
+ """Model parameters required by the CoCiP models."""
55
+
56
+ # -------------------------
57
+ # Implementation parameters
58
+ # -------------------------
59
+
60
+ #: Determines whether :meth:`Cocip.process_emissions` runs on model :meth:`Cocip.eval`
61
+ #: Set to ``False`` when input Flight includes emissions data.
62
+ process_emissions: bool = True
63
+
64
+ #: Apply Euler's method with a fixed step size of ``dt_integration``. Advected waypoints
65
+ #: are interpolated against met data once each ``dt_integration``.
66
+ dt_integration: np.timedelta64 = np.timedelta64(30, "m")
67
+
68
+ #: Difference in altitude between top and bottom layer for stratification calculations,
69
+ #: [:math:`m`]. Used to approximate derivative of "lagrangian_tendency_of_air_pressure" layer.
70
+ dz_m: float = 200.0
71
+
72
+ #: Vertical resolution (m) associated to met data.
73
+ #: Constant below applies to ECMWF data.
74
+ effective_vertical_resolution: float = 2000.0
75
+
76
+ #: Smoothing parameters for true airspeed.
77
+ #: Only used for Flight models.
78
+ #: Passed directly to :func:`scipy.signal.savgol_filter`.
79
+ #: See :meth:`pycontrails.Flight.segment_true_airspeed` for details.
80
+ smooth_true_airspeed: bool = True
81
+ smooth_true_airspeed_window_length: int = 7
82
+ smooth_true_airspeed_polyorder: int = 1
83
+
84
+ #: Humidity scaling
85
+ humidity_scaling: HumidityScaling | None = None
86
+
87
+ #: Experimental. If ``True``, attempt to reduce memory consumption during
88
+ #: aircraft performance and initial contrail formation/persistent calculations
89
+ #: by calling :meth:`MetDataArray.interpolate` with ``lowmem=True``.
90
+ #:
91
+ #: **IMPORTANT**:
92
+ #:
93
+ #: * Memory optimizations used when ``proprocess_lowmem=True`` are designed for
94
+ #: meteorology backed by dask arrays with a chunk size of 1 along
95
+ #: the time dimension. This option may degrade performance if dask if not used
96
+ #: or if chunks contain more than a single time step.
97
+ #: * The impact on runtime of setting ``preprocess_lowmem=True`` depends on how
98
+ #: meteorology data is chunked. Runtime is likely to increase if meteorology
99
+ #: data is chunked in time only, but may decrease if meteorology data is also
100
+ #: chunked in longitude, latitude, and level.
101
+ #: * Changes to data access patterns with ``preprocess_lowmem=True`` alter locations
102
+ #: where interpolation is in- vs out-of-bounds. As a consequence,
103
+ #: Cocip output with ``preprocess_lowmem=True`` is only guaranteed to match output
104
+ #: with ``preprocess_lowmem=False`` when run with ``interpolation_bounds_error=True``
105
+ #: to ensure no out-of-bounds interpolation occurs.
106
+ preprocess_lowmem: bool = False
107
+
108
+ # --------------
109
+ # Downselect met
110
+ # --------------
111
+
112
+ #: Compute ``"tau_cirrus"`` variable in pressure-level met data during model
113
+ #: initialization. Must be one of ``"auto"``, ``True``, or ``False``. If set to
114
+ #: ``"auto"``, ``"tau_cirrus"`` will be computed during model initialization
115
+ #: iff the met data is dask-backed. Otherwise, it will be computed during model
116
+ #: evaluation after the met data is downselected.
117
+ compute_tau_cirrus_in_model_init: bool | str = "auto"
118
+
119
+ #: Met longitude [WGS84] buffer for Cocip evolution.
120
+ met_longitude_buffer: tuple[float, float] = (10.0, 10.0)
121
+
122
+ #: Met latitude buffer [WGS84] for Cocip evolution.
123
+ met_latitude_buffer: tuple[float, float] = (10.0, 10.0)
124
+
125
+ #: Met level buffer [:math:`hPa`] for Cocip initialization and evolution.
126
+ met_level_buffer: tuple[float, float] = (40.0, 40.0)
127
+
128
+ # ---------
129
+ # Filtering
130
+ # ---------
131
+
132
+ #: Filter out waypoints if the don't satisfy the SAC criteria
133
+ #: Note that the SAC algorithm will still be run to calculate
134
+ #: ``T_critical_sac`` for use estimating initial ice particle number.
135
+ #: Passing in a non-default value is unusual, but is included
136
+ #: to allow for false negative calibration and model uncertainty studies.
137
+ filter_sac: bool = True
138
+
139
+ #: Filter out waypoints if they don't satisfy the initial persistent criteria
140
+ #: Passing in a non-default value is unusual, but is included
141
+ #: to allow for false negative calibration and model uncertainty studies.
142
+ filter_initially_persistent: bool = True
143
+
144
+ #: Continue evolving contrail waypoints ``persistent_buffer`` beyond
145
+ #: end of contrail life.
146
+ #: Passing in a non-default value is unusual, but is included
147
+ #: to allow for false negative calibration and model uncertainty studies.
148
+ persistent_buffer: np.timedelta64 | None = None
149
+
150
+ # -------
151
+ # Outputs
152
+ # -------
153
+
154
+ #: Add additional values to the flight and contrail that are not explicitly
155
+ #: necessary for calculation.
156
+ #: See also :attr:`CocipGridParams.verbose_outputs_formation` and
157
+ #: :attr:`CocipGridParams.verbose_outputs_evolution`.
158
+ verbose_outputs: bool = False
159
+
160
+ #: Add additional metric of ATR20 and global yearly mean RF to model output.
161
+ #: These are not standard CoCiP outputs but based on the derivation used
162
+ #: in the first supplement to :cite:`yinPredictingClimateImpact2023`. ATR20 is defined
163
+ #: as the average temperature response over a 20 year horizon.
164
+ compute_atr20: bool = False
165
+
166
+ #: Constant factor used to convert global- and year-mean RF, [:math:`W m^{-2}`],
167
+ #: to ATR20, [:math:`K`], given by :cite:`yinPredictingClimateImpact2023`.
168
+ global_rf_to_atr20_factor: float = 0.0151
169
+
170
+ # ----------------
171
+ # Model parameters
172
+ # ----------------
173
+
174
+ #: Initial wake vortex depth scaling factor.
175
+ #: This factor scales max contrail downward displacement after the wake vortex phase
176
+ #: to set the initial contrail depth.
177
+ #: Denoted :math:`C_{D0}` in eq (14) in :cite:`schumannContrailCirrusPrediction2012`.
178
+ initial_wake_vortex_depth: float = 0.5
179
+
180
+ #: Sedimentation impact factor. Denoted by :math:`f_{T}` in eq. (35) of
181
+ #: :cite:`schumannContrailCirrusPrediction2012`.
182
+ #: Schumann describes this as "an important adjustable parameter", and sets
183
+ #: it to 0.1 in the original publication. In :cite:`schumannAviationinducedCirrusRadiation2013`,
184
+ #: a value of 0.5 is referenced after comparing CoCiP predictions to observations.
185
+ sedimentation_impact_factor: float = 0.5
186
+
187
+ #: Default ``nvpm_ei_n`` value if no data provided and emissions calculations fails.
188
+ default_nvpm_ei_n: float = EmissionsParams.default_nvpm_ei_n
189
+
190
+ #: Parameter denoted by :math:`n` in eq. (39) of :cite:`schumannContrailCirrusPrediction2012`.
191
+ wind_shear_enhancement_exponent: float = 0.5
192
+
193
+ #: Multiply flight black carbon number by enhancement factor.
194
+ #: A value of 1.0 provides no scaling.
195
+ #: Primarily used to support uncertainty estimation.
196
+ nvpm_ei_n_enhancement_factor: float = 1.0
197
+
198
+ #: Lower bound for ``nvpm_ei_n`` to account for ambient aerosol
199
+ #: particles for newer engines, [:math:`kg^{-1}`]
200
+ min_ice_particle_number_nvpm_ei_n: float = 1e13
201
+
202
+ #: Upper bound for contrail plume depth, constraining it to realistic values.
203
+ #: CoCiP only uses the ambient conditions at the mid-point of the Gaussian plume,
204
+ #: and the edges could be in subsaturated conditions and sublimate. Important when
205
+ #: :attr:`radiative_heating_effects` is enabled.
206
+ max_depth: float = 1500.0
207
+
208
+ #: Experimental. Improved ice crystal number survival fraction in the wake vortex phase.
209
+ #: Implement :cite:`unterstrasserPropertiesYoungContrails2016`, who developed a
210
+ #: parametric model that estimates the survival fraction of the contrail ice crystal
211
+ #: number after the wake vortex phase based on the results from large eddy simulations.
212
+ #: This replicates Fig. 4 of :cite:`karcherFormationRadiativeForcing2018`.
213
+ #:
214
+ #: .. versionadded:: 0.50.1
215
+ unterstrasser_ice_survival_fraction: bool = False
216
+
217
+ #: Experimental. Radiative heating effects on contrail cirrus properties.
218
+ #: Terrestrial and solar radiances warm the contrail ice particles and cause
219
+ #: convective turbulence. This effect is expected to enhance vertical mixing
220
+ #: and reduce the lifetime of contrail cirrus. This parameter is experimental,
221
+ #: and the CoCiP implementation of this parameter may change.
222
+ #:
223
+ #: .. versionadded:: 0.28.9
224
+ radiative_heating_effects: bool = False
225
+
226
+ #: Experimental. Radiative effects due to contrail-contrail overlapping
227
+ #: Account for change in local contrail shortwave and longwave radiative forcing
228
+ #: due to contrail-contrail overlapping.
229
+ #:
230
+ #: .. versionadded:: 0.45
231
+ contrail_contrail_overlapping: bool = False
232
+
233
+ #: Experimental. Contrail-contrail overlapping altitude interval
234
+ #: If :attr:`contrail_contrail_overlapping` is set to True, contrails will be grouped into
235
+ #: altitude intervals, and all contrails within each altitude interval are treated as one
236
+ #: contrail layer where they do not overlap.
237
+ dz_overlap_m: float = 500.0
238
+
239
+ #: Radius threshold for regime bins, [:math:`\mu m`]
240
+ #: This is the row index label for ``habit_distributions``.
241
+ #: See Table 2 in :cite:`schumannEffectiveRadiusIce2011`.
242
+ radius_threshold_um: np.ndarray = dataclasses.field(default_factory=_radius_threshold_um)
243
+
244
+ #: Particle habit (shape) types.
245
+ #: This is the column index label for ``habit_distributions``.
246
+ #: See Table 2 in :cite:`schumannEffectiveRadiusIce2011`.
247
+ habits: np.ndarray = dataclasses.field(default_factory=_habits)
248
+
249
+ #: Mix of ice particle habits in each radius regime.
250
+ #: Rows indexes are ``radius_threshold_um`` elements.
251
+ #: Columns indexes are ``habits`` particle habit type.
252
+ #: See Table 2 from :cite:`schumannEffectiveRadiusIce2011`.
253
+ habit_distributions: np.ndarray = dataclasses.field(default_factory=_habit_distributions)
254
+
255
+ #: Scale shortwave radiative forcing.
256
+ #: Primarily used to support uncertainty estimation.
257
+ rf_sw_enhancement_factor: float = 1.0
258
+
259
+ #: Scale longwave radiative forcing.
260
+ #: Primarily used to support uncertainty estimation.
261
+ rf_lw_enhancement_factor: float = 1.0
262
+
263
+ # ---------------------------------------
264
+ # Conditions for end of contrail lifetime
265
+ # ---------------------------------------
266
+
267
+ #: Minimum altitude domain in simulation, [:math:`m`]
268
+ #: If set to ``None``, this check is disabled.
269
+ min_altitude_m: float | None = 6000.0
270
+
271
+ #: Maximum altitude domain in simulation, [:math:`m`]
272
+ #: If set to ``None``, this check is disabled.
273
+ max_altitude_m: float | None = 13000.0
274
+
275
+ #: Maximum contrail segment length in simulation to prevent unrealistic values, [:math:`m`].
276
+ max_seg_length_m: float = 40000.0
277
+
278
+ #: Max age of contrail evolution.
279
+ max_age: np.timedelta64 = np.timedelta64(20, "h")
280
+
281
+ #: Minimum contrail optical depth.
282
+ min_tau: float = 1e-6
283
+
284
+ #: Maximum contrail optical depth to prevent unrealistic values.
285
+ max_tau: float = 1e10
286
+
287
+ #: Minimum contrail ice particle number per volume of air.
288
+ min_n_ice_per_m3: float = 1e3
289
+
290
+ #: Maximum contrail ice particle number per volume of air to prevent unrealistic values.
291
+ max_n_ice_per_m3: float = 1e20
292
+
293
+
294
+ @dataclasses.dataclass
295
+ class CocipFlightParams(CocipParams):
296
+ """Flight specific CoCiP parameters."""
297
+
298
+ #: Aircraft performance model
299
+ aircraft_performance: AircraftPerformance | None = None
@@ -0,0 +1,285 @@
1
+ """Parameters for CoCiP uncertainty calculations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from dataclasses import asdict, dataclass
7
+ from typing import Any, ClassVar
8
+
9
+ import numpy as np
10
+ import numpy.typing as npt
11
+ from scipy import stats
12
+ from scipy.stats.distributions import rv_frozen
13
+
14
+ from pycontrails.models.cocip import cocip_params
15
+ from pycontrails.models.cocip.cocip_params import CocipParams
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class habit_dirichlet(rv_frozen):
21
+ r"""Custom dirichlet distribution for habit weight distribution uncertainty.
22
+
23
+ Scale the habit distributions by a dirichlet distribution
24
+ with alpha parameter :math:`\alpha_{i} = 0.5 + \text{C} \text{G}_{i}`
25
+ where :math:`\text{G}_{i}` is the approximate habit weight distributions
26
+ defined in :attr:`CocipParams().habit_distributions`.
27
+
28
+ References
29
+ ----------
30
+ - Table 2 in :cite:`schumannEffectiveRadiusIce2011`
31
+ """
32
+
33
+ def __init__(self, C: float = 96.0):
34
+ self.C = C
35
+
36
+ def rvs(self, *args: Any, **kwds: Any) -> npt.NDArray[np.float32]:
37
+ """Generate sample set of habit distributions.
38
+
39
+ Sampled using dirichlet distribution.
40
+
41
+ Parameters
42
+ ----------
43
+ *args
44
+ Used to create a number of habit distributions
45
+ **kwds
46
+ Passed through to :func:`scipy.stats.dirichlet.rvs()`
47
+
48
+ Returns
49
+ -------
50
+ npt.NDArray[np.float32]
51
+ Sampled habit weight distribution with the same shape
52
+ as :attr:`CocipParams().habit_distributions`
53
+ """
54
+ if args or (kwds.get("size") is not None and kwds["size"] > 1):
55
+ raise ValueError("habit_dirichlet distribution only supports creating one rv at a time")
56
+
57
+ default_distributions = cocip_params._habit_distributions()
58
+ alpha_i = 0.5 + self.C * default_distributions
59
+
60
+ # In the first distribution, we assume all ice particles are droxtals
61
+ # There is no way to quantify the uncertainty in this assumption
62
+ # Consequently, we leave the first distribution in default_distributions
63
+ # alone, and only perturb the rest
64
+ distr_list = [stats.dirichlet(a) for a in alpha_i[1:]]
65
+
66
+ habit_weights = default_distributions.copy()
67
+ for i, distr in enumerate(distr_list, start=1):
68
+ habit_weights[i] = distr.rvs(**kwds)
69
+
70
+ return habit_weights
71
+
72
+
73
+ @dataclass
74
+ class CocipUncertaintyParams(CocipParams):
75
+ """Model parameters for CoCiP epistemic uncertainty.
76
+
77
+ Any uncertainty parameters should end with the suffix `uncertainty`. See `__post_init__`.
78
+
79
+ Uncertainty parameters take a `scipy.stats.distributions.rv_frozen` distribution.
80
+
81
+ Default distributions have mean at CocipParam default values.
82
+
83
+ To retain specific parameters as defaults, set the value of the key to None.
84
+
85
+ Examples
86
+ --------
87
+ >>> import scipy.stats
88
+ >>> from pycontrails.models.cocip import CocipUncertaintyParams
89
+
90
+ >>> # Override the 'initial_wake_vortex_depth' field from
91
+ >>> # CocipParams with a uniform value in [0.4, 0.6]
92
+ >>> distr = scipy.stats.uniform(loc=0.4, scale=0.2)
93
+ >>> params = CocipUncertaintyParams(seed=123, initial_wake_vortex_depth_uncertainty=distr)
94
+ >>> params.initial_wake_vortex_depth
95
+ np.float64(0.41076420...)
96
+
97
+ >>> # Once seeded, calling the class again gives a new value
98
+ >>> params = CocipUncertaintyParams(initial_wake_vortex_depth=distr)
99
+ >>> params.initial_wake_vortex_depth
100
+ np.float64(0.43526372...)
101
+
102
+ >>> # To retain the default value, set the uncertainty to None
103
+ >>> params = CocipUncertaintyParams(rf_lw_enhancement_factor_uncertainty=None)
104
+ >>> params.rf_lw_enhancement_factor
105
+ 1.0
106
+ """
107
+
108
+ #: The random number generator is attached to the class (as opposed to an instance).
109
+ #: If many instances of :class:`CocipUncertaintyParams` are needed, it is best to seed
110
+ #: the random number generator once initially. Reseeding the generator will also
111
+ #: reset it, giving rise to duplicate random numbers.
112
+ rng: ClassVar[np.random.Generator] = np.random.default_rng(None)
113
+
114
+ #: Reseed the random generator defined in ``__post_init__``
115
+ seed: int | None = None
116
+
117
+ #: Schumann takes ``wind_shear_enhancement_exponent`` = 0.5 and discusses the case of 0 and 2/3
118
+ #: as possibilities.
119
+ #: With a value of 0, wind shear is not enhanced.
120
+ wind_shear_enhancement_exponent_uncertainty: rv_frozen | None = stats.triang(
121
+ loc=0.0, c=CocipParams.wind_shear_enhancement_exponent, scale=1.0
122
+ )
123
+
124
+ #: Schumann takes ``initial_wake_vortex_depth`` = 0.5 and discusses some
125
+ #: uncertainty in this value. This parameter should be non-negative.
126
+ initial_wake_vortex_depth_uncertainty: rv_frozen | None = stats.triang(
127
+ loc=0.3, c=CocipParams.initial_wake_vortex_depth, scale=0.4
128
+ )
129
+
130
+ #: Schumann takes a default value of 0.1 and describes it as an "important adjustable parameter"
131
+ #: Currently, `CocipParams` uses a default value of 0.5
132
+ sedimentation_impact_factor_uncertainty: rv_frozen | None = stats.norm(
133
+ loc=CocipParams.sedimentation_impact_factor, scale=0.1
134
+ )
135
+
136
+ #: Teoh 2022 (to appear) takes values between 70% decrease and 100% increase.
137
+ #: This coincides with the log normal distribution defined below.
138
+ nvpm_ei_n_enhancement_factor_uncertainty: rv_frozen | None = stats.lognorm(
139
+ s=0.15, scale=1 / stats.lognorm(s=0.15).mean()
140
+ )
141
+
142
+ #: Scale shortwave radiative forcing.
143
+ #: Table 2 in :cite:`schumannParametricRadiativeForcing2012`
144
+ #: provides relative RMS error for SW/LW fit to the data generated
145
+ #: by `libRadTran <http://www.libradtran.org/doku.php>`_.
146
+ #: We use the average RMS error across all habit types (pg 1397) as the standard deviation
147
+ #: of a normally distributed scaling factor for SW forcing
148
+ rf_sw_enhancement_factor_uncertainty: rv_frozen | None = stats.norm(
149
+ loc=CocipParams.rf_sw_enhancement_factor, scale=0.106
150
+ )
151
+
152
+ #: Scale longwave radiative forcing.
153
+ #: Table 2 in :cite:`schumannParametricRadiativeForcing2012` provides relative error for SW/LW
154
+ #: fit to the data generated by `libRadTran <http://www.libradtran.org/doku.php>`_.
155
+ #: We use the average RMS error across all habit types (pg 1397) as the standard deviation
156
+ #: of a normally distributed scaling factor for LW forcing.
157
+ rf_lw_enhancement_factor_uncertainty: rv_frozen | None = stats.norm(
158
+ loc=CocipParams.rf_lw_enhancement_factor, scale=0.071
159
+ )
160
+
161
+ #: Scale the habit distributions by a dirichlet distribution
162
+ #: with alpha parameter :math:`\alpha_{i} = 0.5 + \text{C} \text{G}_{i}`
163
+ #: where :math:`\text{G}_{i}` is the approximate habit weight distributions
164
+ #: defined in :attr:`CocipParams().habit_distributions`.
165
+ #: Higher values of :math:`\text{C}` correspond to higher confidence in initial estimates.
166
+ habit_distributions_uncertainty: rv_frozen | None = habit_dirichlet(C=96.0)
167
+
168
+ def __post_init__(self) -> None:
169
+ """Override values of model parameters according to ranges."""
170
+ if self.seed is not None:
171
+ # Reset the class variable `rng`
172
+ logger.info("Reset %s random seed to %s", self.__class__.__name__, self.seed)
173
+ self.__class__.rng = np.random.default_rng(self.seed)
174
+
175
+ # Override defaults value on `CocipParams` with random parameters
176
+ for param, value in self.rvs().items():
177
+ setattr(self, param, value)
178
+
179
+ @property
180
+ def uncertainty_params(self) -> dict[str, rv_frozen]:
181
+ """Get dictionary of uncertainty parameters.
182
+
183
+ Method checks for attributes ending in `"_uncertainty"`.
184
+
185
+ Returns
186
+ -------
187
+ dict[str, dict[str, Any]]
188
+ Uncertainty parameters and values
189
+ """
190
+ # handle these differently starting in version 0.27.0
191
+ exclude = {"rhi_adj", "rhi_boost_exponent"}
192
+
193
+ out = {}
194
+
195
+ param_dict = asdict(self)
196
+ for uncertainty_param, dist in param_dict.items():
197
+ if uncertainty_param.endswith("_uncertainty") and dist is not None:
198
+ param = uncertainty_param.split("_uncertainty")[0]
199
+ if param not in exclude and param not in param_dict:
200
+ raise AttributeError(
201
+ f"Parameter {param} corresponding to uncertainty parameter "
202
+ f"{uncertainty_param} does not exist"
203
+ )
204
+
205
+ if not isinstance(dist, rv_frozen):
206
+ raise AttributeError(
207
+ f"Uncertainty parameter '{uncertainty_param}' must be instance of "
208
+ "'scipy.stats.distributions.rv_frozen'"
209
+ )
210
+
211
+ out[param] = dist
212
+
213
+ return out
214
+
215
+ def rvs(self, size: None | int = None) -> dict[str, np.float64 | npt.NDArray[np.float64]]:
216
+ """Call each distribution's `rvs` method to generate random parameters.
217
+
218
+ Seed calls to `rvs` with class variable `rng`.
219
+
220
+ Parameters
221
+ ----------
222
+ size : None | int, optional
223
+ If specified, an `array` of values is generated for each uncertainty parameter.
224
+
225
+ Returns
226
+ -------
227
+ dict[str, float | npt.NDArray[np.float64]]
228
+ Dictionary of random parameters. Dictionary keys consists of names of parameters in
229
+ `CocipParams` to be overridden by random value.
230
+
231
+ Examples
232
+ --------
233
+ >>> from pprint import pprint
234
+ >>> from pycontrails.models.cocip import CocipUncertaintyParams
235
+ >>> params = CocipUncertaintyParams(seed=456)
236
+ >>> pprint(params.rvs())
237
+ {'habit_distributions': array([[0.0000000e+00, 0.0000000e+00, 0.0000000e+00, 0.0000000e+00,
238
+ 0.0000000e+00, 0.0000000e+00, 1.0000000e+00, 0.0000000e+00],
239
+ [1.5554131e-02, 2.1363135e-01, 7.7715185e-03, 1.7690966e-02,
240
+ 3.1576434e-03, 3.2992734e-06, 7.4009895e-01, 2.0921326e-03],
241
+ [3.8193921e-03, 2.1235342e-01, 3.3554080e-04, 5.2846869e-04,
242
+ 3.1945917e-01, 4.8709914e-04, 4.6250960e-01, 5.0730183e-04],
243
+ [5.7327619e-04, 4.7781631e-01, 4.2596990e-03, 6.7235163e-04,
244
+ 1.4447135e-01, 3.6184600e-01, 1.0150939e-02, 2.1006212e-04],
245
+ [1.5397545e-02, 4.0522218e-01, 4.2781001e-01, 1.4331797e-01,
246
+ 7.1088417e-04, 9.4511814e-04, 3.3900745e-03, 3.2062260e-03],
247
+ [7.9063961e-04, 3.0336906e-03, 7.7571563e-04, 2.0577813e-02,
248
+ 9.4205803e-01, 4.3379897e-03, 3.6786550e-03, 2.4747452e-02]],
249
+ dtype=float32),
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
+ """
257
+ return {
258
+ param: distr.rvs(size=size, random_state=self.rng)
259
+ for param, distr in self.uncertainty_params.items()
260
+ }
261
+
262
+ def as_dict(self) -> dict[str, Any]:
263
+ """Convert object to dictionary.
264
+
265
+ Wrapper around :meth:`ModelBase.as_dict` that removes
266
+ uncertainty specific parameters.
267
+
268
+ Returns
269
+ -------
270
+ dict[str, Any]
271
+ Dictionary version of self.
272
+ """
273
+ obj = super().as_dict()
274
+
275
+ # remove seed and _uncertainty attributes
276
+ # these will throw an error in `ModelBase._load_params()`
277
+ keys_to_remove = ["seed"]
278
+ for key in obj:
279
+ if key.endswith("_uncertainty"):
280
+ keys_to_remove.append(key)
281
+
282
+ for key in keys_to_remove:
283
+ obj.pop(key, None)
284
+
285
+ return obj