pycontrails 0.59.0__cp314-cp314-macosx_10_15_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 (123) 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 +2936 -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.cpython-314-darwin.so +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 +764 -0
  37. pycontrails/datalib/gruan.py +343 -0
  38. pycontrails/datalib/himawari/__init__.py +27 -0
  39. pycontrails/datalib/himawari/header_struct.py +266 -0
  40. pycontrails/datalib/himawari/himawari.py +671 -0
  41. pycontrails/datalib/landsat.py +589 -0
  42. pycontrails/datalib/leo_utils/__init__.py +5 -0
  43. pycontrails/datalib/leo_utils/correction.py +266 -0
  44. pycontrails/datalib/leo_utils/landsat_metadata.py +300 -0
  45. pycontrails/datalib/leo_utils/search.py +250 -0
  46. pycontrails/datalib/leo_utils/sentinel_metadata.py +748 -0
  47. pycontrails/datalib/leo_utils/static/bq_roi_query.sql +6 -0
  48. pycontrails/datalib/leo_utils/vis.py +59 -0
  49. pycontrails/datalib/sentinel.py +650 -0
  50. pycontrails/datalib/spire/__init__.py +5 -0
  51. pycontrails/datalib/spire/exceptions.py +62 -0
  52. pycontrails/datalib/spire/spire.py +604 -0
  53. pycontrails/ext/bada.py +42 -0
  54. pycontrails/ext/cirium.py +14 -0
  55. pycontrails/ext/empirical_grid.py +140 -0
  56. pycontrails/ext/synthetic_flight.py +431 -0
  57. pycontrails/models/__init__.py +1 -0
  58. pycontrails/models/accf.py +425 -0
  59. pycontrails/models/apcemm/__init__.py +8 -0
  60. pycontrails/models/apcemm/apcemm.py +983 -0
  61. pycontrails/models/apcemm/inputs.py +226 -0
  62. pycontrails/models/apcemm/static/apcemm_yaml_template.yaml +183 -0
  63. pycontrails/models/apcemm/utils.py +437 -0
  64. pycontrails/models/cocip/__init__.py +29 -0
  65. pycontrails/models/cocip/cocip.py +2742 -0
  66. pycontrails/models/cocip/cocip_params.py +305 -0
  67. pycontrails/models/cocip/cocip_uncertainty.py +291 -0
  68. pycontrails/models/cocip/contrail_properties.py +1530 -0
  69. pycontrails/models/cocip/output_formats.py +2270 -0
  70. pycontrails/models/cocip/radiative_forcing.py +1260 -0
  71. pycontrails/models/cocip/radiative_heating.py +520 -0
  72. pycontrails/models/cocip/unterstrasser_wake_vortex.py +508 -0
  73. pycontrails/models/cocip/wake_vortex.py +396 -0
  74. pycontrails/models/cocip/wind_shear.py +120 -0
  75. pycontrails/models/cocipgrid/__init__.py +9 -0
  76. pycontrails/models/cocipgrid/cocip_grid.py +2552 -0
  77. pycontrails/models/cocipgrid/cocip_grid_params.py +138 -0
  78. pycontrails/models/dry_advection.py +602 -0
  79. pycontrails/models/emissions/__init__.py +21 -0
  80. pycontrails/models/emissions/black_carbon.py +599 -0
  81. pycontrails/models/emissions/emissions.py +1353 -0
  82. pycontrails/models/emissions/ffm2.py +336 -0
  83. pycontrails/models/emissions/static/default-engine-uids.csv +239 -0
  84. pycontrails/models/emissions/static/edb-gaseous-v29b-engines.csv +596 -0
  85. pycontrails/models/emissions/static/edb-nvpm-v29b-engines.csv +215 -0
  86. pycontrails/models/extended_k15.py +1327 -0
  87. pycontrails/models/humidity_scaling/__init__.py +37 -0
  88. pycontrails/models/humidity_scaling/humidity_scaling.py +1075 -0
  89. pycontrails/models/humidity_scaling/quantiles/era5-model-level-quantiles.pq +0 -0
  90. pycontrails/models/humidity_scaling/quantiles/era5-pressure-level-quantiles.pq +0 -0
  91. pycontrails/models/issr.py +210 -0
  92. pycontrails/models/pcc.py +326 -0
  93. pycontrails/models/pcr.py +154 -0
  94. pycontrails/models/ps_model/__init__.py +18 -0
  95. pycontrails/models/ps_model/ps_aircraft_params.py +381 -0
  96. pycontrails/models/ps_model/ps_grid.py +701 -0
  97. pycontrails/models/ps_model/ps_model.py +1000 -0
  98. pycontrails/models/ps_model/ps_operational_limits.py +525 -0
  99. pycontrails/models/ps_model/static/ps-aircraft-params-20250328.csv +69 -0
  100. pycontrails/models/ps_model/static/ps-synonym-list-20250328.csv +104 -0
  101. pycontrails/models/sac.py +442 -0
  102. pycontrails/models/tau_cirrus.py +183 -0
  103. pycontrails/physics/__init__.py +1 -0
  104. pycontrails/physics/constants.py +117 -0
  105. pycontrails/physics/geo.py +1138 -0
  106. pycontrails/physics/jet.py +968 -0
  107. pycontrails/physics/static/iata-cargo-load-factors-20250221.csv +74 -0
  108. pycontrails/physics/static/iata-passenger-load-factors-20250221.csv +74 -0
  109. pycontrails/physics/thermo.py +551 -0
  110. pycontrails/physics/units.py +472 -0
  111. pycontrails/py.typed +0 -0
  112. pycontrails/utils/__init__.py +1 -0
  113. pycontrails/utils/dependencies.py +66 -0
  114. pycontrails/utils/iteration.py +13 -0
  115. pycontrails/utils/json.py +187 -0
  116. pycontrails/utils/temp.py +50 -0
  117. pycontrails/utils/types.py +163 -0
  118. pycontrails-0.59.0.dist-info/METADATA +179 -0
  119. pycontrails-0.59.0.dist-info/RECORD +123 -0
  120. pycontrails-0.59.0.dist-info/WHEEL +6 -0
  121. pycontrails-0.59.0.dist-info/licenses/LICENSE +178 -0
  122. pycontrails-0.59.0.dist-info/licenses/NOTICE +43 -0
  123. pycontrails-0.59.0.dist-info/top_level.txt +3 -0
@@ -0,0 +1,305 @@
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 AdvectionBuffers
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(AdvectionBuffers):
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
+ #:
107
+ #: .. versionadded:: 0.52.3
108
+ preprocess_lowmem: bool = False
109
+
110
+ # --------------
111
+ # Downselect met
112
+ # --------------
113
+
114
+ #: Compute ``"tau_cirrus"`` variable in pressure-level met data during model
115
+ #: initialization. Must be one of ``"auto"``, ``True``, or ``False``. If set to
116
+ #: ``"auto"``, ``"tau_cirrus"`` will be computed during model initialization
117
+ #: iff the met data is dask-backed. Otherwise, it will be computed during model
118
+ #: evaluation after the met data is downselected.
119
+ #:
120
+ #: .. versionadded:: 0.47.1
121
+ compute_tau_cirrus_in_model_init: bool | str = "auto"
122
+
123
+ # ---------
124
+ # Filtering
125
+ # ---------
126
+
127
+ #: Filter out waypoints if the don't satisfy the SAC criteria
128
+ #: Note that the SAC algorithm will still be run to calculate
129
+ #: ``T_critical_sac`` for use estimating initial ice particle number.
130
+ #: Passing in a non-default value is unusual, but is included
131
+ #: to allow for false negative calibration and model uncertainty studies.
132
+ filter_sac: bool = True
133
+
134
+ #: Filter out waypoints if they don't satisfy the initial persistent criteria
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_initially_persistent: bool = True
138
+
139
+ #: Continue evolving contrail waypoints ``persistent_buffer`` beyond
140
+ #: end of contrail life.
141
+ #: Passing in a non-default value is unusual, but is included
142
+ #: to allow for false negative calibration and model uncertainty studies.
143
+ persistent_buffer: np.timedelta64 | None = None
144
+
145
+ # -------
146
+ # Outputs
147
+ # -------
148
+
149
+ #: Add additional values to the flight and contrail that are not explicitly
150
+ #: necessary for calculation.
151
+ #: See also :attr:`CocipGridParams.verbose_outputs_formation` and
152
+ #: :attr:`CocipGridParams.verbose_outputs_evolution`.
153
+ verbose_outputs: bool = False
154
+
155
+ #: Add additional metric of ATR20 and global yearly mean RF to model output.
156
+ #: These are not standard CoCiP outputs but based on the derivation used
157
+ #: in the first supplement to :cite:`yinPredictingClimateImpact2023`. ATR20 is defined
158
+ #: as the average temperature response over a 20 year horizon.
159
+ #:
160
+ #: .. versionadded:: 0.50.0
161
+ compute_atr20: bool = False
162
+
163
+ #: Constant factor used to convert global- and year-mean RF, [:math:`W m^{-2}`],
164
+ #: to ATR20, [:math:`K`], given by :cite:`yinPredictingClimateImpact2023`.
165
+ #:
166
+ #: .. versionadded:: 0.50.0
167
+ global_rf_to_atr20_factor: float = 0.0151
168
+
169
+ # ----------------
170
+ # Model parameters
171
+ # ----------------
172
+
173
+ #: Initial wake vortex depth scaling factor.
174
+ #: This factor scales max contrail downward displacement after the wake vortex phase
175
+ #: to set the initial contrail depth.
176
+ #: Denoted :math:`C_{D0}` in eq (14) in :cite:`schumannContrailCirrusPrediction2012`.
177
+ initial_wake_vortex_depth: float = 0.5
178
+
179
+ #: Sedimentation impact factor. Denoted by :math:`f_{T}` in eq. (35) of
180
+ #: :cite:`schumannContrailCirrusPrediction2012`.
181
+ #: Schumann describes this as "an important adjustable parameter", and sets
182
+ #: it to 0.1 in the original publication. In :cite:`schumannAviationinducedCirrusRadiation2013`,
183
+ #: a value of 0.5 is referenced after comparing CoCiP predictions to observations.
184
+ sedimentation_impact_factor: float = 0.5
185
+
186
+ #: Default ``nvpm_ei_n`` value if no data provided and emissions calculations fails.
187
+ default_nvpm_ei_n: float = EmissionsParams.default_nvpm_ei_n
188
+
189
+ #: Parameter denoted by :math:`n` in eq. (39) of :cite:`schumannContrailCirrusPrediction2012`.
190
+ wind_shear_enhancement_exponent: float = 0.5
191
+
192
+ #: Multiply flight black carbon number by enhancement factor.
193
+ #: A value of 1.0 provides no scaling.
194
+ #: Primarily used to support uncertainty estimation.
195
+ nvpm_ei_n_enhancement_factor: float = 1.0
196
+
197
+ #: Lower bound for ``nvpm_ei_n`` to account for ambient aerosol
198
+ #: particles for newer engines, [:math:`kg^{-1}`]. Set to None to disable.
199
+ min_ice_particle_number_nvpm_ei_n: float | None = 1e13
200
+
201
+ #: Upper bound for contrail plume depth, constraining it to realistic values.
202
+ #: CoCiP only uses the ambient conditions at the mid-point of the Gaussian plume,
203
+ #: and the edges could be in subsaturated conditions and sublimate. Important when
204
+ #: :attr:`radiative_heating_effects` is enabled.
205
+ max_depth: float = 1500.0
206
+
207
+ #: Experimental. Improved ice crystal number survival fraction in the wake vortex phase.
208
+ #: Implement :cite:`lottermoserHighResolutionEarlyContrails2025`, who developed a
209
+ #: parametric model that estimates the survival fraction of the contrail ice crystal
210
+ #: number after the wake vortex phase based on the results from large eddy simulations.
211
+ #: This replicates Fig. 4 of :cite:`karcherFormationRadiativeForcing2018`.
212
+ #:
213
+ #: .. versionadded:: 0.50.1
214
+ #: .. versionchanged:: 0.54.7
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. Apply the extended K15 model to account for vPM activation.
227
+ #: See the preprint `<https://doi.org/10.5194/egusphere-2025-1717>`_ for details.
228
+ #:
229
+ #: .. versionadded:: 0.55.0
230
+ vpm_activation: bool = False
231
+
232
+ #: Experimental. Radiative effects due to contrail-contrail overlapping
233
+ #: Account for change in local contrail shortwave and longwave radiative forcing
234
+ #: due to contrail-contrail overlapping.
235
+ #:
236
+ #: .. versionadded:: 0.45.0
237
+ contrail_contrail_overlapping: bool = False
238
+
239
+ #: Experimental. Contrail-contrail overlapping altitude interval
240
+ #: If :attr:`contrail_contrail_overlapping` is set to True, contrails will be grouped into
241
+ #: altitude intervals, and all contrails within each altitude interval are treated as one
242
+ #: contrail layer where they do not overlap.
243
+ dz_overlap_m: float = 500.0
244
+
245
+ #: Radius threshold for regime bins, [:math:`\mu m`]
246
+ #: This is the row index label for ``habit_distributions``.
247
+ #: See Table 2 in :cite:`schumannEffectiveRadiusIce2011`.
248
+ radius_threshold_um: np.ndarray = dataclasses.field(default_factory=_radius_threshold_um)
249
+
250
+ #: Particle habit (shape) types.
251
+ #: This is the column index label for ``habit_distributions``.
252
+ #: See Table 2 in :cite:`schumannEffectiveRadiusIce2011`.
253
+ habits: np.ndarray = dataclasses.field(default_factory=_habits)
254
+
255
+ #: Mix of ice particle habits in each radius regime.
256
+ #: Rows indexes are ``radius_threshold_um`` elements.
257
+ #: Columns indexes are ``habits`` particle habit type.
258
+ #: See Table 2 from :cite:`schumannEffectiveRadiusIce2011`.
259
+ habit_distributions: np.ndarray = dataclasses.field(default_factory=_habit_distributions)
260
+
261
+ #: Scale shortwave radiative forcing.
262
+ #: Primarily used to support uncertainty estimation.
263
+ rf_sw_enhancement_factor: float = 1.0
264
+
265
+ #: Scale longwave radiative forcing.
266
+ #: Primarily used to support uncertainty estimation.
267
+ rf_lw_enhancement_factor: float = 1.0
268
+
269
+ # ---------------------------------------
270
+ # Conditions for end of contrail lifetime
271
+ # ---------------------------------------
272
+
273
+ #: Minimum altitude domain in simulation, [:math:`m`]
274
+ #: If set to ``None``, this check is disabled.
275
+ min_altitude_m: float | None = 6000.0
276
+
277
+ #: Maximum altitude domain in simulation, [:math:`m`]
278
+ #: If set to ``None``, this check is disabled.
279
+ max_altitude_m: float | None = 13000.0
280
+
281
+ #: Maximum contrail segment length in simulation to prevent unrealistic values, [:math:`m`].
282
+ max_seg_length_m: float = 40000.0
283
+
284
+ #: Max age of contrail evolution.
285
+ max_age: np.timedelta64 = np.timedelta64(20, "h")
286
+
287
+ #: Minimum contrail optical depth.
288
+ min_tau: float = 1e-6
289
+
290
+ #: Maximum contrail optical depth to prevent unrealistic values.
291
+ max_tau: float = 1e10
292
+
293
+ #: Minimum contrail ice particle number per volume of air.
294
+ min_n_ice_per_m3: float = 1e3
295
+
296
+ #: Maximum contrail ice particle number per volume of air to prevent unrealistic values.
297
+ max_n_ice_per_m3: float = 1e20
298
+
299
+
300
+ @dataclasses.dataclass
301
+ class CocipFlightParams(CocipParams):
302
+ """Flight specific CoCiP parameters."""
303
+
304
+ #: Aircraft performance model
305
+ aircraft_performance: AircraftPerformance | None = None
@@ -0,0 +1,291 @@
1
+ """Parameters for CoCiP uncertainty calculations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import dataclasses
6
+ import logging
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) -> None:
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 : Any
44
+ Used to create a number of habit distributions
45
+ **kwds : Any
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
+ @dataclasses.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 = dataclasses.field(
121
+ default_factory=lambda: stats.triang(
122
+ loc=0.0, c=CocipParams.wind_shear_enhancement_exponent, scale=1.0
123
+ )
124
+ )
125
+
126
+ #: Schumann takes ``initial_wake_vortex_depth`` = 0.5 and discusses some
127
+ #: uncertainty in this value. This parameter should be non-negative.
128
+ initial_wake_vortex_depth_uncertainty: rv_frozen | None = dataclasses.field(
129
+ default_factory=lambda: stats.triang(
130
+ loc=0.3, c=CocipParams.initial_wake_vortex_depth, scale=0.4
131
+ )
132
+ )
133
+
134
+ #: Schumann takes a default value of 0.1 and describes it as an "important adjustable parameter"
135
+ #: Currently, `CocipParams` uses a default value of 0.5
136
+ sedimentation_impact_factor_uncertainty: rv_frozen | None = dataclasses.field(
137
+ default_factory=lambda: stats.norm(loc=CocipParams.sedimentation_impact_factor, scale=0.1)
138
+ )
139
+
140
+ #: Teoh 2022 (to appear) takes values between 70% decrease and 100% increase.
141
+ #: This coincides with the log normal distribution defined below.
142
+ nvpm_ei_n_enhancement_factor_uncertainty: rv_frozen | None = dataclasses.field(
143
+ default_factory=lambda: stats.lognorm(s=0.15, scale=1 / stats.lognorm(s=0.15).mean())
144
+ )
145
+
146
+ #: Scale shortwave radiative forcing.
147
+ #: Table 2 in :cite:`schumannParametricRadiativeForcing2012`
148
+ #: provides relative RMS error for SW/LW fit to the data generated
149
+ #: by `libRadTran <http://www.libradtran.org/doku.php>`_.
150
+ #: We use the average RMS error across all habit types (pg 1397) as the standard deviation
151
+ #: of a normally distributed scaling factor for SW forcing
152
+ rf_sw_enhancement_factor_uncertainty: rv_frozen | None = dataclasses.field(
153
+ default_factory=lambda: stats.norm(loc=CocipParams.rf_sw_enhancement_factor, scale=0.106)
154
+ )
155
+
156
+ #: Scale longwave radiative forcing.
157
+ #: Table 2 in :cite:`schumannParametricRadiativeForcing2012` provides relative error for SW/LW
158
+ #: fit to the data generated by `libRadTran <http://www.libradtran.org/doku.php>`_.
159
+ #: We use the average RMS error across all habit types (pg 1397) as the standard deviation
160
+ #: of a normally distributed scaling factor for LW forcing.
161
+ rf_lw_enhancement_factor_uncertainty: rv_frozen | None = dataclasses.field(
162
+ default_factory=lambda: stats.norm(loc=CocipParams.rf_lw_enhancement_factor, scale=0.071)
163
+ )
164
+
165
+ #: Scale the habit distributions by a dirichlet distribution
166
+ #: with alpha parameter :math:`\alpha_{i} = 0.5 + \text{C} \text{G}_{i}`
167
+ #: where :math:`\text{G}_{i}` is the approximate habit weight distributions
168
+ #: defined in :attr:`CocipParams().habit_distributions`.
169
+ #: Higher values of :math:`\text{C}` correspond to higher confidence in initial estimates.
170
+ habit_distributions_uncertainty: rv_frozen | None = dataclasses.field(
171
+ default_factory=habit_dirichlet
172
+ )
173
+
174
+ def __post_init__(self) -> None:
175
+ """Override values of model parameters according to ranges."""
176
+ if self.seed is not None:
177
+ # Reset the class variable `rng`
178
+ logger.info("Reset %s random seed to %s", self.__class__.__name__, self.seed)
179
+ self.__class__.rng = np.random.default_rng(self.seed)
180
+
181
+ # Override defaults value on `CocipParams` with random parameters
182
+ for param, value in self.rvs().items():
183
+ setattr(self, param, value)
184
+
185
+ @property
186
+ def uncertainty_params(self) -> dict[str, rv_frozen]:
187
+ """Get dictionary of uncertainty parameters.
188
+
189
+ Method checks for attributes ending in `"_uncertainty"`.
190
+
191
+ Returns
192
+ -------
193
+ dict[str, rv_frozen]
194
+ Uncertainty parameters and values
195
+ """
196
+ # handle these differently starting in version 0.27.0
197
+ exclude = {"rhi_adj", "rhi_boost_exponent"}
198
+
199
+ out = {}
200
+
201
+ param_dict = dataclasses.asdict(self)
202
+ for uncertainty_param, dist in param_dict.items():
203
+ if uncertainty_param.endswith("_uncertainty") and dist is not None:
204
+ param = uncertainty_param.split("_uncertainty")[0]
205
+ if param not in exclude and param not in param_dict:
206
+ raise AttributeError(
207
+ f"Parameter {param} corresponding to uncertainty parameter "
208
+ f"{uncertainty_param} does not exist"
209
+ )
210
+
211
+ if not isinstance(dist, rv_frozen):
212
+ raise AttributeError(
213
+ f"Uncertainty parameter '{uncertainty_param}' must be instance of "
214
+ "'scipy.stats.distributions.rv_frozen'"
215
+ )
216
+
217
+ out[param] = dist
218
+
219
+ return out
220
+
221
+ def rvs(self, size: None | int = None) -> dict[str, np.float64 | npt.NDArray[np.floating]]:
222
+ """Call each distribution's `rvs` method to generate random parameters.
223
+
224
+ Seed calls to `rvs` with class variable `rng`.
225
+
226
+ Parameters
227
+ ----------
228
+ size : None | int, optional
229
+ If specified, an `array` of values is generated for each uncertainty parameter.
230
+
231
+ Returns
232
+ -------
233
+ dict[str, np.float64 | npt.NDArray[np.floating]]
234
+ Dictionary of random parameters. Dictionary keys consists of names of parameters in
235
+ `CocipParams` to be overridden by random value.
236
+
237
+ Examples
238
+ --------
239
+ >>> from pprint import pprint
240
+ >>> from pycontrails.models.cocip import CocipUncertaintyParams
241
+ >>> params = CocipUncertaintyParams(seed=456)
242
+ >>> pprint(params.rvs())
243
+ {'habit_distributions': array([[0.0000000e+00, 0.0000000e+00, 0.0000000e+00, 0.0000000e+00,
244
+ 0.0000000e+00, 0.0000000e+00, 1.0000000e+00, 0.0000000e+00],
245
+ [1.5554131e-02, 2.1363135e-01, 7.7715185e-03, 1.7690966e-02,
246
+ 3.1576434e-03, 3.2992734e-06, 7.4009895e-01, 2.0921326e-03],
247
+ [3.8193921e-03, 2.1235342e-01, 3.3554080e-04, 5.2846869e-04,
248
+ 3.1945917e-01, 4.8709914e-04, 4.6250960e-01, 5.0730183e-04],
249
+ [5.7327619e-04, 4.7781631e-01, 4.2596990e-03, 6.7235163e-04,
250
+ 1.4447135e-01, 3.6184600e-01, 1.0150939e-02, 2.1006212e-04],
251
+ [1.5397545e-02, 4.0522218e-01, 4.2781001e-01, 1.4331797e-01,
252
+ 7.1088417e-04, 9.4511814e-04, 3.3900745e-03, 3.2062260e-03],
253
+ [7.9063961e-04, 3.0336906e-03, 7.7571563e-04, 2.0577813e-02,
254
+ 9.4205803e-01, 4.3379897e-03, 3.6786550e-03, 2.4747452e-02]],
255
+ dtype=float32),
256
+ 'initial_wake_vortex_depth': np.float64(0.39805019708566847),
257
+ 'nvpm_ei_n_enhancement_factor': np.float64(0.9371878437312526),
258
+ 'rf_lw_enhancement_factor': np.float64(1.1017491252832377),
259
+ 'rf_sw_enhancement_factor': np.float64(0.99721639115012),
260
+ 'sedimentation_impact_factor': np.float64(0.5071779847244678),
261
+ 'wind_shear_enhancement_exponent': np.float64(0.34100931239701004)}
262
+ """
263
+ return {
264
+ param: distr.rvs(size=size, random_state=self.rng)
265
+ for param, distr in self.uncertainty_params.items()
266
+ }
267
+
268
+ def as_dict(self) -> dict[str, Any]:
269
+ """Convert object to dictionary.
270
+
271
+ Wrapper around :meth:`ModelBase.as_dict` that removes
272
+ uncertainty specific parameters.
273
+
274
+ Returns
275
+ -------
276
+ dict[str, Any]
277
+ Dictionary version of self.
278
+ """
279
+ obj = super().as_dict()
280
+
281
+ # remove seed and _uncertainty attributes
282
+ # these will throw an error in `ModelBase._load_params()`
283
+ keys_to_remove = ["seed"]
284
+ for key in obj:
285
+ if key.endswith("_uncertainty"):
286
+ keys_to_remove.append(key)
287
+
288
+ for key in keys_to_remove:
289
+ obj.pop(key, None)
290
+
291
+ return obj