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,210 @@
1
+ """Ice super-saturated regions (ISSR)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Any, overload
7
+
8
+ import numpy as np
9
+
10
+ import pycontrails
11
+ from pycontrails.core.flight import Flight
12
+ from pycontrails.core.met import MetDataset
13
+ from pycontrails.core.met_var import AirTemperature, MetVariable, SpecificHumidity
14
+ from pycontrails.core.models import Model, ModelParams
15
+ from pycontrails.core.vector import GeoVectorDataset
16
+ from pycontrails.models.humidity_scaling import HumidityScaling
17
+ from pycontrails.physics import constants, thermo
18
+ from pycontrails.utils.types import ArrayLike, apply_nan_mask_to_arraylike
19
+
20
+
21
+ @dataclass
22
+ class ISSRParams(ModelParams):
23
+ """Default ISSR model parameters."""
24
+
25
+ #: RHI Threshold
26
+ rhi_threshold: float = 1.0
27
+
28
+ #: Humidity scaling
29
+ humidity_scaling: HumidityScaling | None = None
30
+
31
+
32
+ class ISSR(Model):
33
+ """Ice super-saturated regions over a :class:`Flight` trajectory or :class:`MetDataset` grid.
34
+
35
+ This model calculates points where the relative humidity over ice is greater than 1.
36
+
37
+ Parameters
38
+ ----------
39
+ met : MetDataset
40
+ Dataset containing "air_temperature" and "specific_humidity" variables
41
+
42
+ Examples
43
+ --------
44
+ >>> from datetime import datetime
45
+ >>> from pycontrails.datalib.ecmwf import ERA5
46
+ >>> from pycontrails.models.issr import ISSR
47
+ >>> from pycontrails.models.humidity_scaling import ConstantHumidityScaling
48
+
49
+ >>> # Get met data
50
+ >>> time = datetime(2022, 3, 1, 0), datetime(2022, 3, 1, 2)
51
+ >>> variables = ["air_temperature", "specific_humidity"]
52
+ >>> pressure_levels = [200, 250, 300]
53
+ >>> era5 = ERA5(time, variables, pressure_levels)
54
+ >>> met = era5.open_metdataset()
55
+
56
+ >>> # Instantiate and run model
57
+ >>> scaling = ConstantHumidityScaling(rhi_adj=0.98)
58
+ >>> model = ISSR(met, humidity_scaling=scaling)
59
+ >>> out1 = model.eval()
60
+ >>> issr1 = out1["issr"]
61
+ >>> issr1.proportion # Get proportion of values with ice supersaturation
62
+ 0.114...
63
+
64
+ >>> # Run with a lower threshold
65
+ >>> out2 = model.eval(rhi_threshold=0.95)
66
+ >>> issr2 = out2["issr"]
67
+ >>> issr2.proportion
68
+ 0.146...
69
+ """
70
+
71
+ name = "issr"
72
+ long_name = "Ice super-saturated regions"
73
+ met_variables: tuple[MetVariable, ...] = AirTemperature, SpecificHumidity
74
+ default_params = ISSRParams
75
+
76
+ @overload
77
+ def eval(self, source: Flight, **params: Any) -> Flight: ...
78
+
79
+ @overload
80
+ def eval(self, source: GeoVectorDataset, **params: Any) -> GeoVectorDataset: ...
81
+
82
+ @overload
83
+ def eval(self, source: MetDataset | None = ..., **params: Any) -> MetDataset: ...
84
+
85
+ def eval(
86
+ self, source: GeoVectorDataset | Flight | MetDataset | None = None, **params: Any
87
+ ) -> GeoVectorDataset | Flight | MetDataset:
88
+ """Evaluate ice super-saturated regions along flight trajectory or on meteorology grid.
89
+
90
+ .. versionchanged:: 0.27.0
91
+
92
+ Humidity scaling now handled automatically. This is controlled by
93
+ model parameter ``humidity_scaling``.
94
+
95
+ .. versionchanged:: 0.48.0
96
+
97
+ If the ``source`` is a :class:`MetDataset`, the returned object will
98
+ also be a :class:`MetDataset`. Previous the "issr" :class:`MetDataArray`
99
+ was returned.
100
+
101
+ Parameters
102
+ ----------
103
+ source : GeoVectorDataset | Flight | MetDataset | None, optional
104
+ Input GeoVectorDataset or Flight.
105
+ If None, evaluates at the :attr:`met` grid points.
106
+ **params : Any
107
+ Overwrite model parameters before eval
108
+
109
+ Returns
110
+ -------
111
+ GeoVectorDataset | Flight | MetDataset
112
+ Returns 1 in ISSR, 0 everywhere else.
113
+ Returns `np.nan` if interpolating outside meteorology grid.
114
+
115
+ Raises
116
+ ------
117
+ NotImplementedError
118
+ Raises if input ``source`` is not supported.
119
+ """
120
+
121
+ self.update_params(params)
122
+ self.set_source(source)
123
+
124
+ if isinstance(self.source, GeoVectorDataset):
125
+ self.downselect_met()
126
+ self.source.setdefault("air_pressure", self.source.air_pressure)
127
+
128
+ humidity_scaling = self.params["humidity_scaling"]
129
+ scale_humidity = humidity_scaling is not None and "specific_humidity" not in self.source
130
+
131
+ self.set_source_met()
132
+
133
+ # apply humidity scaling, warn if no scaling is provided for ECMWF data
134
+ if scale_humidity:
135
+ humidity_scaling.eval(self.source, copy_source=False)
136
+
137
+ self.source["issr"] = issr( # type: ignore[type-var]
138
+ air_temperature=self.source.data["air_temperature"],
139
+ specific_humidity=self.source.data["specific_humidity"],
140
+ air_pressure=self.source.data["air_pressure"],
141
+ rhi=self.source.data.get("rhi", None), # if rhi already known, pass it in
142
+ rhi_threshold=self.params["rhi_threshold"],
143
+ )
144
+
145
+ # Tag output with additional metadata attrs
146
+ self.transfer_met_source_attrs()
147
+ self.source.attrs["pycontrails_version"] = pycontrails.__version__
148
+ if scale_humidity:
149
+ for k, v in humidity_scaling.description.items():
150
+ self.source.attrs[f"humidity_scaling_{k}"] = v
151
+
152
+ return self.source
153
+
154
+
155
+ def issr(
156
+ air_temperature: ArrayLike,
157
+ specific_humidity: ArrayLike | None = None,
158
+ air_pressure: ArrayLike | None = None,
159
+ rhi: ArrayLike | None = None,
160
+ rhi_threshold: float = 1.0,
161
+ ) -> ArrayLike:
162
+ r"""Calculate ice super-saturated regions.
163
+
164
+ Regions where the atmospheric relative humidity over ice is greater than 1.
165
+
166
+ Parameters ``air_temperature``, ``specific_humidity``, ``air_pressure``,
167
+ and ``rhi`` must have compatible shapes when defined.
168
+
169
+ Either ``specific_humidity`` and ``air_pressure`` must both be provided, or
170
+ ``rhi`` must be provided.
171
+
172
+ Parameters
173
+ ----------
174
+ air_temperature : ArrayLike
175
+ A sequence or array of temperature values, :math:`[K]`.
176
+ specific_humidity : ArrayLike | None
177
+ A sequence or array of specific humidity values, [:math:`kg_{H_{2}O} \ kg_{moist air}`]
178
+ None by default.
179
+ air_pressure : ArrayLike | None
180
+ A sequence or array of atmospheric pressure values, [:math:`Pa`]. None by default.
181
+ rhi : ArrayLike | None, optional
182
+ A sequence of array of RHi values, if already known. If not provided, this function
183
+ will compute RHi from `air_temperature`, `specific_humidity`, and `air_pressure`.
184
+ None by default.
185
+ rhi_threshold : float, optional
186
+ Relative humidity over ice threshold for determining ISSR state
187
+
188
+ Returns
189
+ -------
190
+ ArrayLike
191
+ ISSR state of each point indexed by the parameters.
192
+ """
193
+ if rhi is None:
194
+ if specific_humidity is None or air_pressure is None:
195
+ raise TypeError(
196
+ "If 'rhi' is not specified, both 'specific_humidity' "
197
+ "and 'air_pressure' must be provided."
198
+ )
199
+ rhi = thermo.rhi(specific_humidity, air_temperature, air_pressure)
200
+
201
+ # store nan values to refill after casting
202
+ nan_mask = np.isnan(rhi)
203
+
204
+ # compute issr as int
205
+ sufficiently_cold = air_temperature < -constants.absolute_zero
206
+ sufficiently_humid = rhi > rhi_threshold
207
+
208
+ issr_ = (sufficiently_cold & sufficiently_humid).astype(rhi.dtype)
209
+
210
+ return apply_nan_mask_to_arraylike(issr_, nan_mask)
@@ -0,0 +1,326 @@
1
+ """Probability of persistent contrail coverage (PCC)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import dataclasses
6
+ from typing import Any
7
+
8
+ import numpy as np
9
+ import xarray as xr
10
+
11
+ from pycontrails.core.fuel import Fuel, JetA
12
+ from pycontrails.core.met import MetDataArray, MetDataset
13
+ from pycontrails.core.met_var import AirTemperature, SpecificHumidity
14
+ from pycontrails.core.models import Model, ModelParams
15
+ from pycontrails.datalib.ecmwf.variables import SpecificCloudIceWaterContent
16
+ from pycontrails.models import sac
17
+ from pycontrails.models.humidity_scaling import HumidityScaling
18
+ from pycontrails.physics import thermo
19
+
20
+
21
+ @dataclasses.dataclass
22
+ class PCCParams(ModelParams):
23
+ """PCC Model Parameters."""
24
+
25
+ #: Cloud model
26
+ #: Options include "Smith1990", "Sundqvist1989", "Slingo1980"
27
+ cloud_model: str = "Smith1990"
28
+
29
+ #: Critical RH Factor for the model to cirrus clouds
30
+ rh_crit_factor: float = 0.7
31
+
32
+ #: Fuel type
33
+ fuel: Fuel = dataclasses.field(default_factory=JetA)
34
+
35
+ #: Engine efficiency
36
+ engine_efficiency: float = 0.35
37
+
38
+ #: Humidity scaling
39
+ humidity_scaling: HumidityScaling | None = None
40
+
41
+
42
+ class PCC(Model):
43
+ r"""Potential Contrail Coverage Algorithm.
44
+
45
+ Determines the potential of ambient atmosphere to allow contrail formation at grid points.
46
+
47
+ Parameters
48
+ ----------
49
+ met : MetDataset
50
+ Dataset containing :attr:`met_variables` variables.
51
+ surface : MetDataset
52
+ Surface level dataset containing "air_pressure".
53
+ params : dict[str, Any], optional
54
+ Override PCC model parameters with dictionary.
55
+ See :class:`PCCParams` for model parameters.
56
+ **params_kwargs
57
+ Override PCC model parameters with keyword arguments.
58
+ See :class:`PCCParams` for model parameters.
59
+
60
+ Notes
61
+ -----
62
+ Based on Ponater et al. (2002)
63
+ """
64
+
65
+ __slots__ = ("_cloud_model", "surface")
66
+ name = "pcc"
67
+ long_name = "Potential contrail coverage"
68
+ met_variables = (AirTemperature, SpecificHumidity, SpecificCloudIceWaterContent)
69
+ _cloud_model: Any
70
+ source: MetDataset
71
+ surface: MetDataset
72
+ default_params = PCCParams
73
+
74
+ def __init__(
75
+ self,
76
+ met: MetDataset,
77
+ surface: MetDataset,
78
+ params: dict[str, Any] | None = None,
79
+ **params_kwargs: Any,
80
+ ) -> None:
81
+ super().__init__(met, params=params, **params_kwargs)
82
+
83
+ # set cloud model by method
84
+ self._cloud_model = getattr(self, self.params["cloud_model"], None)
85
+ if self._cloud_model is None:
86
+ raise ValueError(
87
+ "Cloud model must be one of 'Smith1990', 'Sundqvist1989', 'Slingo1980'"
88
+ )
89
+
90
+ # make sure surface level dataset is valid
91
+ if not isinstance(surface, MetDataset):
92
+ raise TypeError("Surface air pressure required as input parameter 'surface'")
93
+
94
+ self.surface = surface.copy()
95
+ self.surface.ensure_vars(["surface_air_pressure"])
96
+
97
+ def eval(self, source: MetDataset | None = None, **params: Any) -> MetDataArray:
98
+ """Evaluate PCC model.
99
+
100
+ Currently only implemented to work on the :attr:`met` data input.
101
+
102
+ Parameters
103
+ ----------
104
+ source : MetDataset | None, optional
105
+ Input MetDataset.
106
+ If None, evaluates at the :attr:`met` grid points.
107
+ **params : Any
108
+ Overwrite model parameters before eval
109
+
110
+
111
+ Returns
112
+ -------
113
+ MetDataArray
114
+ PCC model output
115
+ """
116
+ self.update_params(params)
117
+ self.set_source(source)
118
+
119
+ # apply humidity scaling
120
+ scale_humidity = (self.params["humidity_scaling"] is not None) and (
121
+ "specific_humidity" not in self.source
122
+ )
123
+ self.set_source_met()
124
+
125
+ if scale_humidity:
126
+ self.params["humidity_scaling"].eval(self.source, copy_source=False)
127
+
128
+ return MetDataArray(self.b_contr(), name="pcc")
129
+
130
+ def b_contr(self) -> xr.DataArray:
131
+ """Calculate critical relative humidity threshold of contrail formation.
132
+
133
+ Returns
134
+ -------
135
+ xr.DataArray
136
+ Critical relative humidity of contrail formation, [:math:`[0 - 1]`]
137
+
138
+ Notes
139
+ -----
140
+ Instead of using a prescribed threshold relative humidity for ``rh_crit_old``
141
+ the threshold relative humidity now change with pressure.
142
+
143
+ This equation is described in Roeckner et al. 1996, Eq.57
144
+ THE ATMOSPHERIC GENERAL CIRCULATION MODEL ECHAM-4: MODEL DESCRIPTION AND
145
+ SIMULATION OF PRESENT-DAY CLIMATE
146
+ """
147
+ sp = self.surface["surface_air_pressure"].data.loc[dict(level=-1)] # surface air pressure
148
+
149
+ def _apply_b_contr_plev(_ds: xr.Dataset) -> xr.Dataset:
150
+ p = _ds["air_pressure"]
151
+
152
+ G = sac.slope_mixing_line(
153
+ _ds["specific_humidity"],
154
+ _ds["air_pressure"],
155
+ self.params["engine_efficiency"],
156
+ self.params["fuel"].ei_h2o,
157
+ self.params["fuel"].q_fuel,
158
+ )
159
+ T_sat_liquid = sac.T_sat_liquid(G)
160
+ rh_crit_sac = sac.rh_critical_sac(_ds["air_temperature"], T_sat_liquid, G)
161
+
162
+ # rh_crit_old = np.ones(p.shape) * self.rh_crit_factor
163
+ rh_crit_old = 0.99 + (0.6 - 0.99) * np.exp(1 - (sp / p) ** 4)
164
+ rh_crit_new = rh_crit_sac * rh_crit_old
165
+
166
+ b_crit = self._cloud_model(
167
+ _ds["air_temperature"],
168
+ _ds["air_pressure"],
169
+ _ds["specific_cloud_ice_water_content"],
170
+ _ds["specific_humidity"],
171
+ rh_crit_old,
172
+ rh_crit_old,
173
+ )
174
+ b_crit_contr = self._cloud_model(
175
+ _ds["air_temperature"],
176
+ _ds["air_pressure"],
177
+ _ds["specific_cloud_ice_water_content"],
178
+ _ds["specific_humidity"],
179
+ rh_crit_old,
180
+ rh_crit_new,
181
+ )
182
+ b_crit_potential = b_crit_contr - b_crit
183
+
184
+ b_crit_potential = xr.where((b_crit_potential > 1), 1, b_crit_potential)
185
+ b_crit_potential = xr.where((b_crit_potential < 0), 0, b_crit_potential)
186
+
187
+ # issue recombining groups arises if "level" is in dims
188
+ # convert "level" dimension to coordinate
189
+ return b_crit_potential.squeeze("level")
190
+
191
+ # apply calculation per pressure level
192
+ return (
193
+ self.source.data.groupby("level", squeeze=False) # type: ignore
194
+ .map(_apply_b_contr_plev)
195
+ .transpose(*self.source.dim_order)
196
+ )
197
+
198
+ def Smith1990(
199
+ self,
200
+ T: xr.DataArray,
201
+ p: xr.DataArray,
202
+ iwc: xr.DataArray,
203
+ q: xr.DataArray,
204
+ rh_crit_old: xr.DataArray,
205
+ rh_crit_new: xr.DataArray,
206
+ ) -> xr.DataArray:
207
+ r"""Apply Smith Scheme described in Rap et al. (2009).
208
+
209
+ Parameterization of contrails in the UK Met OfficeClimate Model;
210
+
211
+ Parameters
212
+ ----------
213
+ T : :class:`xarray:DataArray`
214
+ Air Temperature, [:math:`K`]
215
+ p : :class:`xarray:DataArray`
216
+ Air Pressure, [:math:`Pa`]
217
+ iwc : :class:`xarray:DataArray`
218
+ Cloud ice water content, [:math:`kg \ kg^{-1}`]
219
+ q : :class:`xarray:DataArray`
220
+ Specific humidity
221
+ rh_crit_old : :class:`xarray:DataArray`
222
+ Critical relative humidity, [:math:`[0 - 1]`]
223
+ rh_crit_new : :class:`xarray:DataArray`
224
+ Critical relative humidity, [:math:`[0 - 1]`]
225
+
226
+ Returns
227
+ -------
228
+ :class:`xarray:DataArray`
229
+ Probability of cirrus formation, [:math:`[0 - 1]`]
230
+ """
231
+ r = thermo.rh(q, T, p)
232
+ q_sw = thermo.q_sat(T, p)
233
+ b_crit = (1 - rh_crit_old) * q_sw
234
+ Q_n = iwc / b_crit - (1 - r) / (1 - rh_crit_new)
235
+
236
+ b_cirrus = xr.DataArray(np.zeros(Q_n.shape), coords=Q_n.coords)
237
+ b_cirrus = xr.where((Q_n <= -1), 0, b_cirrus)
238
+ b_cirrus = xr.where(
239
+ (Q_n > -1) & (Q_n <= 0), 0.5 * (1 + Q_n.where((Q_n > -1) & (Q_n <= 0))) ** 2, b_cirrus
240
+ )
241
+ b_cirrus = xr.where(
242
+ (Q_n > 0) & (Q_n <= 1), 1 - 0.5 * (1 - Q_n.where((Q_n > 0) & (Q_n <= 1))) ** 2, b_cirrus
243
+ )
244
+ return xr.where((Q_n > 1), 1, b_cirrus)
245
+
246
+ def Slingo1980(
247
+ self,
248
+ T: xr.DataArray,
249
+ p: xr.DataArray,
250
+ iwc: xr.DataArray,
251
+ q: xr.DataArray,
252
+ rh_crit_old: xr.DataArray,
253
+ rh_crit_new: xr.DataArray,
254
+ ) -> xr.DataArray:
255
+ r"""Apply Slingo scheme described in Wood and Field, 1999.
256
+
257
+ Relationships between Total Water, Condensed Water, and Cloud Fraction in
258
+ Stratiform Clouds Examined Using Aircraft Data
259
+
260
+ Parameters
261
+ ----------
262
+ T : :class:`xarray:DataArray`
263
+ Air Temperature, [:math:`K`]
264
+ p : :class:`xarray:DataArray`
265
+ Air Pressure, [:math:`Pa`]
266
+ iwc : :class:`xarray:DataArray`
267
+ Cloud ice water content, [:math:`kg \ kg^{-1}`]
268
+ q : :class:`xarray:DataArray`
269
+ Specific humidity
270
+ rh_crit_old : :class:`xarray:DataArray`
271
+ Critical relative humidity, [:math:`[0 - 1]`]
272
+ rh_crit_new : :class:`xarray:DataArray`
273
+ Critical relative humidity, [:math:`[0 - 1]`]
274
+
275
+ Returns
276
+ -------
277
+ :class:`xarray:DataArray`
278
+ Probability of cirrus formation, [:math:`[0 - 1]`]
279
+ """
280
+ r = thermo.rh(q, T, p)
281
+ b_cirrus = ((r - rh_crit_new) / (1 - rh_crit_new)) ** 2
282
+ b_cirrus = xr.where(r < rh_crit_new, 0, b_cirrus)
283
+ return xr.where(r >= 1.0, 1.0, b_cirrus)
284
+
285
+ def Sundqvist1989(
286
+ self,
287
+ T: xr.DataArray,
288
+ p: xr.DataArray,
289
+ iwc: xr.DataArray,
290
+ q: xr.DataArray,
291
+ rh_crit_old: xr.DataArray,
292
+ rh_crit_new: xr.DataArray,
293
+ ) -> xr.DataArray:
294
+ r"""Apply Sundqvist scheme described in Ponater et al. (2002).
295
+
296
+ Contrails in a comprehensive global climate model: Parameterization and radiative
297
+ forcing results
298
+
299
+ Parameters
300
+ ----------
301
+ T : :class:`xarray:DataArray`
302
+ Air Temperature, [:math:`K`]
303
+ p : :class:`xarray:DataArray`
304
+ Air Pressure, [:math:`Pa`]
305
+ iwc : :class:`xarray:DataArray`
306
+ Cloud ice water content, [:math:`kg \ kg^{-1}`]
307
+ q : :class:`xarray:DataArray`
308
+ Specific humidity
309
+ rh_crit_old : :class:`xarray:DataArray`
310
+ Critical relative humidity, [:math:`[0 - 1]`]
311
+ rh_crit_new : :class:`xarray:DataArray`
312
+ Critical relative humidity, [:math:`[0 - 1]`]
313
+
314
+ Returns
315
+ -------
316
+ :class:`xarray:DataArray`
317
+ Probability of cirrus formation, [:math:`[0 - 1]`]
318
+ """
319
+ r = thermo.rh(q, T, p)
320
+
321
+ # clipping ratio at 1 to prevent np.sqrt from taking negative arguments
322
+ ratio = (r - rh_crit_new) / (1 - rh_crit_new)
323
+ ratio = xr.where(ratio > 1, 1, ratio)
324
+
325
+ b_cirrus = 1 - (1 - ratio) ** 0.5
326
+ return xr.where(r < rh_crit_new, 0, b_cirrus)
@@ -0,0 +1,154 @@
1
+ """
2
+ Persistent contrail regions (PCR = SAC & ISSR).
3
+
4
+ Equivalent to (SAC & ISSR)
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass
10
+ from typing import Any, overload
11
+
12
+ import numpy as np
13
+
14
+ from pycontrails.core.flight import Flight
15
+ from pycontrails.core.met import MetDataset
16
+ from pycontrails.core.met_var import AirTemperature, SpecificHumidity
17
+ from pycontrails.core.models import Model
18
+ from pycontrails.core.vector import GeoVectorDataset
19
+ from pycontrails.models import issr, sac
20
+ from pycontrails.physics import thermo
21
+ from pycontrails.utils.types import ArrayLike, apply_nan_mask_to_arraylike
22
+
23
+
24
+ @dataclass
25
+ class PCRParams(sac.SACParams, issr.ISSRParams):
26
+ """Persistent Contrail Regions (PCR) parameters."""
27
+
28
+
29
+ class PCR(Model):
30
+ """Determine points with likely persistent contrails (PCR).
31
+
32
+ Intersection of Ice Super Saturated Regions (ISSR) with regions in which the Schmidt-Appleman
33
+ Criteria (SAC) is satisfied.
34
+
35
+ Parameters
36
+ ----------
37
+ met : MetDataset
38
+ Dataset containing "air_temperature", "specific_humidity" variables
39
+ params : dict[str, Any], optional
40
+ Override PCR model parameters with dictionary.
41
+ See :class:`PCRGridParams` for model parameters.
42
+ **params_kwargs
43
+ Override PCR model parameters with keyword arguments.
44
+ See :class:`PCRGridParams` for model parameters.
45
+ """
46
+
47
+ name = "pcr"
48
+ long_name = "Persistent contrail regions"
49
+ met_variables = AirTemperature, SpecificHumidity
50
+ default_params = PCRParams
51
+
52
+ @overload
53
+ def eval(self, source: Flight, **params: Any) -> Flight: ...
54
+
55
+ @overload
56
+ def eval(self, source: GeoVectorDataset, **params: Any) -> GeoVectorDataset: ...
57
+
58
+ @overload
59
+ def eval(self, source: MetDataset | None = ..., **params: Any) -> MetDataset: ...
60
+
61
+ def eval(
62
+ self, source: GeoVectorDataset | Flight | MetDataset | None = None, **params: Any
63
+ ) -> GeoVectorDataset | Flight | MetDataset:
64
+ """Evaluate potential contrails regions of the :attr:`met` grid.
65
+
66
+ Parameters
67
+ ----------
68
+ source : GeoVectorDataset | Flight | MetDataset | None, optional
69
+ Input GeoVectorDataset or Flight.
70
+ If None, evaluates at the :attr:`met` grid points.
71
+ **params : Any
72
+ Overwrite model parameters.
73
+
74
+ Returns
75
+ -------
76
+ GeoVectorDataset | Flight | MetDataset
77
+ Returns 1 in potential contrail regions, 0 everywhere else.
78
+ Returns ``np.nan`` if interpolating outside meteorology grid.
79
+
80
+ """
81
+
82
+ self.update_params(params)
83
+ self.set_source(source)
84
+ issr_params = {k: v for k, v in self.params.items() if hasattr(issr.ISSR.default_params, k)}
85
+ issr_model = issr.ISSR(self.met, params=issr_params, copy_source=False)
86
+ issr_model.eval(self.source)
87
+
88
+ sac_params = {k: v for k, v in self.params.items() if hasattr(sac.SAC.default_params, k)}
89
+ # NOTE: met is not needed here: ISSR already used it
90
+ sac_model = sac.SAC(met=None, params=sac_params, copy_source=False)
91
+ sac_model.eval(self.source)
92
+
93
+ pcr_ = _pcr_from_issr_and_sac(self.source.data["issr"], self.source.data["sac"]) # type: ignore[type-var]
94
+ self.source["pcr"] = pcr_
95
+
96
+ return self.source
97
+
98
+
99
+ def pcr(
100
+ air_temperature: ArrayLike,
101
+ specific_humidity: ArrayLike,
102
+ air_pressure: ArrayLike,
103
+ engine_efficiency: float | ArrayLike,
104
+ ei_h2o: float,
105
+ q_fuel: float,
106
+ ) -> tuple[ArrayLike, ArrayLike, ArrayLike]:
107
+ r"""Calculate regions of persistent contrail formation.
108
+
109
+ Ice Super Saturated Regions (ISSR) where the Schmidt-Appleman Criteria (SAC) is satisfied.
110
+
111
+ Parameters of type :class:`ArrayLike` must have compatible shapes.
112
+
113
+ Parameters
114
+ ----------
115
+ air_temperature : ArrayLike
116
+ A sequence or array of temperature values, [:math:`K`]
117
+ specific_humidity : ArrayLike
118
+ A sequence or array of specific humidity values, [:math:`kg_{H_{2}O} \ kg_{air}^{-1}`]
119
+ air_pressure : ArrayLike
120
+ A sequence or array of atmospheric pressure values, [:math:`Pa`].
121
+ engine_efficiency: float | ArrayLike
122
+ Engine efficiency, [:math:`0 - 1`]
123
+ ei_h2o : float
124
+ Emission index of water vapor, [:math:`kg \ kg^{-1}`]
125
+ q_fuel : float
126
+ Specific combustion heat of fuel combustion, [:math:`J \ kg^{-1} \ K^{-1}`]
127
+
128
+ Returns
129
+ -------
130
+ pcr : ArrayLike
131
+ PCR state of each point indexed by the :class:`ArrayLike` parameters.
132
+ sac : ArrayLike
133
+ SAC state
134
+ issr : ArrayLike
135
+ ISSR state
136
+ """
137
+ issr_ = issr.issr(air_temperature, specific_humidity, air_pressure)
138
+ G = sac.slope_mixing_line(specific_humidity, air_pressure, engine_efficiency, ei_h2o, q_fuel)
139
+ T_sat_liquid_ = sac.T_sat_liquid(G)
140
+ rh = thermo.rh(specific_humidity, air_temperature, air_pressure)
141
+ rh_crit_sac = sac.rh_critical_sac(air_temperature, T_sat_liquid_, G)
142
+ sac_ = sac.sac(rh, rh_crit_sac)
143
+
144
+ pcr_ = _pcr_from_issr_and_sac(issr_, sac_)
145
+ return pcr_, sac_, issr_
146
+
147
+
148
+ def _pcr_from_issr_and_sac(issr_: ArrayLike, sac_: ArrayLike) -> ArrayLike:
149
+ # store nan values to refill after casting
150
+ nan_mask = np.isnan(issr_) | np.isnan(sac_)
151
+
152
+ dtype = np.result_type(issr_, sac_)
153
+ pcr_ = ((issr_ > 0.0) & (sac_ > 0.0)).astype(dtype)
154
+ return apply_nan_mask_to_arraylike(pcr_, nan_mask)