pycontrails 0.58.0__cp314-cp314-macosx_10_13_x86_64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of pycontrails might be problematic. Click here for more details.

Files changed (122) hide show
  1. pycontrails/__init__.py +70 -0
  2. pycontrails/_version.py +34 -0
  3. pycontrails/core/__init__.py +30 -0
  4. pycontrails/core/aircraft_performance.py +679 -0
  5. pycontrails/core/airports.py +228 -0
  6. pycontrails/core/cache.py +889 -0
  7. pycontrails/core/coordinates.py +174 -0
  8. pycontrails/core/fleet.py +483 -0
  9. pycontrails/core/flight.py +2185 -0
  10. pycontrails/core/flightplan.py +228 -0
  11. pycontrails/core/fuel.py +140 -0
  12. pycontrails/core/interpolation.py +702 -0
  13. pycontrails/core/met.py +2931 -0
  14. pycontrails/core/met_var.py +387 -0
  15. pycontrails/core/models.py +1321 -0
  16. pycontrails/core/polygon.py +549 -0
  17. pycontrails/core/rgi_cython.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 +757 -0
  37. pycontrails/datalib/himawari/__init__.py +27 -0
  38. pycontrails/datalib/himawari/header_struct.py +266 -0
  39. pycontrails/datalib/himawari/himawari.py +667 -0
  40. pycontrails/datalib/landsat.py +589 -0
  41. pycontrails/datalib/leo_utils/__init__.py +5 -0
  42. pycontrails/datalib/leo_utils/correction.py +266 -0
  43. pycontrails/datalib/leo_utils/landsat_metadata.py +300 -0
  44. pycontrails/datalib/leo_utils/search.py +250 -0
  45. pycontrails/datalib/leo_utils/sentinel_metadata.py +748 -0
  46. pycontrails/datalib/leo_utils/static/bq_roi_query.sql +6 -0
  47. pycontrails/datalib/leo_utils/vis.py +59 -0
  48. pycontrails/datalib/sentinel.py +650 -0
  49. pycontrails/datalib/spire/__init__.py +5 -0
  50. pycontrails/datalib/spire/exceptions.py +62 -0
  51. pycontrails/datalib/spire/spire.py +604 -0
  52. pycontrails/ext/bada.py +42 -0
  53. pycontrails/ext/cirium.py +14 -0
  54. pycontrails/ext/empirical_grid.py +140 -0
  55. pycontrails/ext/synthetic_flight.py +431 -0
  56. pycontrails/models/__init__.py +1 -0
  57. pycontrails/models/accf.py +425 -0
  58. pycontrails/models/apcemm/__init__.py +8 -0
  59. pycontrails/models/apcemm/apcemm.py +983 -0
  60. pycontrails/models/apcemm/inputs.py +226 -0
  61. pycontrails/models/apcemm/static/apcemm_yaml_template.yaml +183 -0
  62. pycontrails/models/apcemm/utils.py +437 -0
  63. pycontrails/models/cocip/__init__.py +29 -0
  64. pycontrails/models/cocip/cocip.py +2742 -0
  65. pycontrails/models/cocip/cocip_params.py +305 -0
  66. pycontrails/models/cocip/cocip_uncertainty.py +291 -0
  67. pycontrails/models/cocip/contrail_properties.py +1530 -0
  68. pycontrails/models/cocip/output_formats.py +2270 -0
  69. pycontrails/models/cocip/radiative_forcing.py +1260 -0
  70. pycontrails/models/cocip/radiative_heating.py +520 -0
  71. pycontrails/models/cocip/unterstrasser_wake_vortex.py +508 -0
  72. pycontrails/models/cocip/wake_vortex.py +396 -0
  73. pycontrails/models/cocip/wind_shear.py +120 -0
  74. pycontrails/models/cocipgrid/__init__.py +9 -0
  75. pycontrails/models/cocipgrid/cocip_grid.py +2552 -0
  76. pycontrails/models/cocipgrid/cocip_grid_params.py +138 -0
  77. pycontrails/models/dry_advection.py +602 -0
  78. pycontrails/models/emissions/__init__.py +21 -0
  79. pycontrails/models/emissions/black_carbon.py +599 -0
  80. pycontrails/models/emissions/emissions.py +1353 -0
  81. pycontrails/models/emissions/ffm2.py +336 -0
  82. pycontrails/models/emissions/static/default-engine-uids.csv +239 -0
  83. pycontrails/models/emissions/static/edb-gaseous-v29b-engines.csv +596 -0
  84. pycontrails/models/emissions/static/edb-nvpm-v29b-engines.csv +215 -0
  85. pycontrails/models/extended_k15.py +1327 -0
  86. pycontrails/models/humidity_scaling/__init__.py +37 -0
  87. pycontrails/models/humidity_scaling/humidity_scaling.py +1075 -0
  88. pycontrails/models/humidity_scaling/quantiles/era5-model-level-quantiles.pq +0 -0
  89. pycontrails/models/humidity_scaling/quantiles/era5-pressure-level-quantiles.pq +0 -0
  90. pycontrails/models/issr.py +210 -0
  91. pycontrails/models/pcc.py +326 -0
  92. pycontrails/models/pcr.py +154 -0
  93. pycontrails/models/ps_model/__init__.py +18 -0
  94. pycontrails/models/ps_model/ps_aircraft_params.py +381 -0
  95. pycontrails/models/ps_model/ps_grid.py +701 -0
  96. pycontrails/models/ps_model/ps_model.py +1000 -0
  97. pycontrails/models/ps_model/ps_operational_limits.py +525 -0
  98. pycontrails/models/ps_model/static/ps-aircraft-params-20250328.csv +69 -0
  99. pycontrails/models/ps_model/static/ps-synonym-list-20250328.csv +104 -0
  100. pycontrails/models/sac.py +442 -0
  101. pycontrails/models/tau_cirrus.py +183 -0
  102. pycontrails/physics/__init__.py +1 -0
  103. pycontrails/physics/constants.py +117 -0
  104. pycontrails/physics/geo.py +1138 -0
  105. pycontrails/physics/jet.py +968 -0
  106. pycontrails/physics/static/iata-cargo-load-factors-20250221.csv +74 -0
  107. pycontrails/physics/static/iata-passenger-load-factors-20250221.csv +74 -0
  108. pycontrails/physics/thermo.py +551 -0
  109. pycontrails/physics/units.py +472 -0
  110. pycontrails/py.typed +0 -0
  111. pycontrails/utils/__init__.py +1 -0
  112. pycontrails/utils/dependencies.py +66 -0
  113. pycontrails/utils/iteration.py +13 -0
  114. pycontrails/utils/json.py +187 -0
  115. pycontrails/utils/temp.py +50 -0
  116. pycontrails/utils/types.py +163 -0
  117. pycontrails-0.58.0.dist-info/METADATA +180 -0
  118. pycontrails-0.58.0.dist-info/RECORD +122 -0
  119. pycontrails-0.58.0.dist-info/WHEEL +6 -0
  120. pycontrails-0.58.0.dist-info/licenses/LICENSE +178 -0
  121. pycontrails-0.58.0.dist-info/licenses/NOTICE +43 -0
  122. pycontrails-0.58.0.dist-info/top_level.txt +3 -0
@@ -0,0 +1,104 @@
1
+ ICAO Aircraft Code,PS ATYP
2
+ A178,E190
3
+ A19N,A20N
4
+ A20N,A20N
5
+ A21N,A21N
6
+ A306,A306
7
+ A30B,A30B
8
+ A310,A310
9
+ A313,A313
10
+ A318,A318
11
+ A319,A319
12
+ A320,A320
13
+ A321,A321
14
+ A332,A332
15
+ A333,A333
16
+ A338,A338
17
+ A339,A339
18
+ A342,A342
19
+ A343,A343
20
+ A345,A345
21
+ A346,A346
22
+ A359,A359
23
+ A35K,A35K
24
+ A388,A388
25
+ B37M,B37M
26
+ B38M,B38M
27
+ B39M,B39M
28
+ B3XM,B3XM
29
+ B701,B752
30
+ B712,B712
31
+ B720,B752
32
+ B721,B752
33
+ B722,B722
34
+ B732,B732
35
+ B733,B733
36
+ B734,B734
37
+ B735,B735
38
+ B736,B736
39
+ B737,B737
40
+ B738,B738
41
+ B739,B739
42
+ B741,B743
43
+ B742,B742
44
+ B743,B743
45
+ B744,B744
46
+ B748,B748
47
+ B74D,B743
48
+ B74R,B743
49
+ B74S,B742
50
+ B752,B752
51
+ B753,B753
52
+ B762,B762
53
+ B763,B763
54
+ B764,B764
55
+ B772,B772
56
+ B773,B773
57
+ B77L,B77L
58
+ B77W,B77W
59
+ B788,B788
60
+ B789,B789
61
+ B78X,B78X
62
+ BCS1,BCS1
63
+ BCS3,BCS3
64
+ BLCF,B77W
65
+ C141,A310
66
+ C17,B764
67
+ C5,A345
68
+ C5M,A345
69
+ C919,A20N
70
+ CRJ7,CRJ9
71
+ CRJ9,CRJ9
72
+ DC91,B712
73
+ DC93,DC93
74
+ E135,E135
75
+ E145,E145
76
+ E170,E170
77
+ E190,E190
78
+ E195,E195
79
+ E290,E290
80
+ E295,E295
81
+ E3CF,B762
82
+ E3TF,B762
83
+ E737,B738
84
+ E75L,E75L
85
+ E75S,E75S
86
+ E767,B763
87
+ GLF5,GLF5
88
+ IL62,A30B
89
+ J328,E135
90
+ KC39,A321
91
+ MC23,A20N
92
+ MD81,MD82
93
+ MD82,MD82
94
+ MD83,MD83
95
+ MD87,MD82
96
+ MD88,MD82
97
+ MD90,MD83
98
+ NIM,B738
99
+ P1,B38M
100
+ P8,B738
101
+ R721,B722
102
+ R722,B722
103
+ RJ1H,RJ1H
104
+ SLCH,A388
@@ -0,0 +1,442 @@
1
+ """Schmidt-Appleman criteria (SAC)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import dataclasses
6
+ from typing import Any, overload
7
+
8
+ import numpy as np
9
+ import scipy.optimize
10
+
11
+ import pycontrails
12
+ from pycontrails.core.flight import Flight
13
+ from pycontrails.core.fuel import Fuel, JetA
14
+ from pycontrails.core.met import MetDataset
15
+ from pycontrails.core.met_var import AirTemperature, SpecificHumidity
16
+ from pycontrails.core.models import Model, ModelParams
17
+ from pycontrails.core.vector import GeoVectorDataset
18
+ from pycontrails.models.humidity_scaling import HumidityScaling
19
+ from pycontrails.physics import constants, thermo
20
+ from pycontrails.utils.types import ArrayLike, apply_nan_mask_to_arraylike
21
+
22
+ # -----------------
23
+ # Models as classes
24
+ # -----------------
25
+
26
+
27
+ @dataclasses.dataclass
28
+ class SACParams(ModelParams):
29
+ """Parameters for :class:`SAC`."""
30
+
31
+ #: Jet engine efficiency, [:math:`0 - 1`]
32
+ engine_efficiency: float = 0.3
33
+
34
+ #: Fuel type.
35
+ #: Overridden by Fuel provided on input ``source`` attributes
36
+ fuel: Fuel = dataclasses.field(default_factory=JetA)
37
+
38
+ #: Humidity scaling
39
+ humidity_scaling: HumidityScaling | None = None
40
+
41
+
42
+ class SAC(Model):
43
+ """Determine points where Schmidt-Appleman Criteria is satisfied.
44
+
45
+ Parameters
46
+ ----------
47
+ met : MetDataset
48
+ Dataset containing "air_temperature", "specific_humidity" variables.
49
+ params : dict[str, Any], optional
50
+ Override :class:`SACParams` with dictionary.
51
+ **params_kwargs
52
+ Override :class:`SACParams` with keyword arguments.
53
+ """
54
+
55
+ name = "sac"
56
+ long_name = "Schmidt-Appleman contrail formation criteria"
57
+ met_variables = AirTemperature, SpecificHumidity
58
+ default_params = SACParams
59
+
60
+ @overload
61
+ def eval(self, source: Flight, **params: Any) -> Flight: ...
62
+
63
+ @overload
64
+ def eval(self, source: GeoVectorDataset, **params: Any) -> GeoVectorDataset: ...
65
+
66
+ @overload
67
+ def eval(self, source: MetDataset | None = ..., **params: Any) -> MetDataset: ...
68
+
69
+ def eval(
70
+ self, source: GeoVectorDataset | Flight | MetDataset | None = None, **params: Any
71
+ ) -> GeoVectorDataset | Flight | MetDataset:
72
+ """Evaluate the Schmidt-Appleman criteria along flight trajectory or on meteorology grid.
73
+
74
+ .. versionchanged:: 0.27.0
75
+
76
+ Humidity scaling now handled automatically. This is controlled by
77
+ model parameter ``humidity_scaling``.
78
+
79
+ .. versionchanged:: 0.48.0
80
+
81
+ If the ``source`` is a :class:`MetDataset`, the returned object will
82
+ also be a :class:`MetDataset`. Previous the "sac" :class:`MetDataArray`
83
+ was returned.
84
+
85
+ Parameters
86
+ ----------
87
+ source : GeoVectorDataset | Flight | MetDataset | None, optional
88
+ Input GeoVectorDataset or Flight.
89
+ If None, evaluates at the :attr:`met` grid points.
90
+ **params : Any
91
+ Overwrite model parameters before eval
92
+
93
+ Returns
94
+ -------
95
+ GeoVectorDataset | Flight | MetDataset
96
+ Returns 1 where SAC is satisfied, 0 everywhere else.
97
+ Returns ``np.nan`` if interpolating outside meteorology grid.
98
+
99
+ """
100
+
101
+ self.update_params(params)
102
+ self.set_source(source)
103
+ self.require_source_type((GeoVectorDataset, MetDataset))
104
+
105
+ if isinstance(self.source, GeoVectorDataset):
106
+ self.downselect_met()
107
+ self.source.setdefault("air_pressure", self.source.air_pressure)
108
+
109
+ humidity_scaling = self.params["humidity_scaling"]
110
+ scale_humidity = humidity_scaling is not None and "specific_humidity" not in self.source
111
+ self.set_source_met()
112
+
113
+ # apply humidity scaling, warn if no scaling is provided for ECMWF data
114
+ if scale_humidity:
115
+ humidity_scaling.eval(self.source, copy_source=False)
116
+
117
+ # Extract source data
118
+ air_temperature = self.source.data["air_temperature"]
119
+ specific_humidity = self.source.data["specific_humidity"]
120
+ air_pressure = self.source.data["air_pressure"]
121
+ engine_efficiency = self.get_source_param("engine_efficiency")
122
+
123
+ # Flight class has fuel attribute, use this instead of params
124
+ if isinstance(self.source, Flight):
125
+ fuel = self.source.fuel
126
+ else:
127
+ # NOTE: Not setting fuel on MetDataset source
128
+ fuel = self.get_source_param("fuel", set_attr=False)
129
+ assert isinstance(fuel, Fuel), "The fuel attribute must be of type Fuel"
130
+
131
+ ei_h2o = fuel.ei_h2o
132
+ q_fuel = fuel.q_fuel
133
+
134
+ G = slope_mixing_line(specific_humidity, air_pressure, engine_efficiency, ei_h2o, q_fuel)
135
+ T_sat_liquid_ = T_sat_liquid(G)
136
+ rh_crit_sac = rh_critical_sac(air_temperature, T_sat_liquid_, G)
137
+ rh = thermo.rh(specific_humidity, air_temperature, air_pressure) # type: ignore[type-var]
138
+ sac_ = sac(rh, rh_crit_sac)
139
+
140
+ # Attaching some intermediate artifacts onto the source
141
+ self.source["G"] = G
142
+ self.source["T_sat_liquid"] = T_sat_liquid_
143
+ self.source["rh"] = rh
144
+ self.source["rh_critical_sac"] = rh_crit_sac
145
+ self.source["sac"] = sac_
146
+
147
+ # Tag output with additional metadata attrs
148
+ self.transfer_met_source_attrs()
149
+ self.source.attrs["pycontrails_version"] = pycontrails.__version__
150
+ if scale_humidity:
151
+ for k, v in humidity_scaling.description.items():
152
+ self.source.attrs[f"humidity_scaling_{k}"] = v
153
+ if isinstance(engine_efficiency, int | float):
154
+ self.source.attrs["engine_efficiency"] = engine_efficiency
155
+
156
+ return self.source
157
+
158
+
159
+ # -------------------
160
+ # Models as functions
161
+ # -------------------
162
+
163
+
164
+ def slope_mixing_line(
165
+ specific_humidity: ArrayLike,
166
+ air_pressure: ArrayLike,
167
+ engine_efficiency: float | ArrayLike,
168
+ ei_h2o: float,
169
+ q_fuel: float,
170
+ ) -> ArrayLike:
171
+ r"""Calculate the slope of the mixing line in a temperature-humidity diagram.
172
+
173
+ This quantity is often notated with ``G`` in the literature.
174
+
175
+ Parameters
176
+ ----------
177
+ specific_humidity : ArrayLike
178
+ A sequence or array of specific humidity values, [:math:`kg_{H_{2}O} \ kg_{air}`]
179
+ air_pressure : ArrayLike
180
+ A sequence or array of atmospheric pressure values, [:math:`Pa`].
181
+ engine_efficiency: float | ArrayLike
182
+ Engine efficiency, [:math:`0 - 1`]
183
+ ei_h2o : float
184
+ Emission index of water vapor, [:math:`kg \ kg^{-1}`]
185
+ q_fuel : float
186
+ Specific combustion heat of fuel combustion, [:math:`J \ kg^{-1} \ K^{-1}`]
187
+
188
+ Returns
189
+ -------
190
+ ArrayLike
191
+ Slope of the mixing line in a temperature-humidity diagram, [:math:`Pa \ K^{-1}`]
192
+ """
193
+ c_pm = thermo.c_pm(specific_humidity) # Often taken as 1004 (= constants.c_pd)
194
+ return (ei_h2o * c_pm * air_pressure) / (constants.epsilon * q_fuel * (1.0 - engine_efficiency))
195
+
196
+
197
+ def T_sat_liquid(G: ArrayLike) -> ArrayLike:
198
+ r"""Calculate temperature at which liquid saturation curve has slope G.
199
+
200
+ Parameters
201
+ ----------
202
+ G : ArrayLike
203
+ Slope of the mixing line in a temperature-humidity diagram.
204
+
205
+ Returns
206
+ -------
207
+ ArrayLike
208
+ Maximum threshold temperature for 100% relative humidity with respect to liquid,
209
+ [:math:`K`]. This can also be interpreted as the temperature at which the liquid
210
+ saturation curve has slope G.
211
+
212
+ References
213
+ ----------
214
+ - :cite:`schumannConditionsContrailFormation1996`
215
+
216
+ See Also
217
+ --------
218
+ :func:`T_sat_liquid_high_accuracy`
219
+
220
+ Notes
221
+ -----
222
+ Defined (using notation T_LM) in :cite:`schumannConditionsContrailFormation1996`
223
+ in the first full paragraph on page 10 as
224
+
225
+ for T = T_LC, the mixing line just touches [is tangent to]
226
+ the saturation curve. See equation (10).
227
+
228
+ The formula used here is taken from equation (31).
229
+ """
230
+ # FIXME: Presently, mypy is not aware that numpy ufuncs will return `xr.DataArray``
231
+ # when xr.DataArray is passed in. This will get fixed at some point in the future
232
+ # as `numpy` their typing patterns, after which the "type: ignore" comment can
233
+ # get ripped out.
234
+ # We could explicitly check for `xr.DataArray` then use `xr.apply_ufunc`, but
235
+ # this only renders our code more boilerplate and less performant.
236
+ # This comment is pasted several places in `pycontrails` -- they should all be
237
+ # addressed at the same time.
238
+ log_ = np.log(G - 0.053)
239
+ return -46.46 - constants.absolute_zero + 9.43 * log_ + 0.72 * log_**2 # type: ignore[return-value]
240
+
241
+
242
+ def T_sat_liquid_high_accuracy(
243
+ G: ArrayLike,
244
+ maxiter: int = 5,
245
+ ) -> ArrayLike:
246
+ """Calculate temperature at which liquid saturation curve has slope G.
247
+
248
+ The function :func:`T_sat_liquid` gives a first order approximation to equation (10)
249
+ of the Schumann paper referenced below. This function uses Newton's method to
250
+ compute the numeric solution to (10).
251
+
252
+ Parameters
253
+ ----------
254
+ G : ArrayLike
255
+ Slope of the mixing line
256
+ maxiter : int, optional
257
+ Passed into :func:`scipy.optimize.newton`. Because ``T_sat_liquid`` is already
258
+ fairly accurate, few iterations are needed for Newton's method to converge.
259
+ By default, 5.
260
+
261
+ Returns
262
+ -------
263
+ ArrayLike
264
+ Maximum threshold temperature for 100% relative humidity with respect to liquid,
265
+ [:math:`K`].
266
+
267
+ References
268
+ ----------
269
+ - :cite:`schumannConditionsContrailFormation1996`
270
+
271
+ See Also
272
+ --------
273
+ :func:`T_sat_liquid_high`
274
+ """
275
+ init_guess = T_sat_liquid(G)
276
+
277
+ def func(T: ArrayLike) -> ArrayLike:
278
+ """Equation (10) from Schumann 1996."""
279
+ return thermo.e_sat_liquid_prime(T) - G
280
+
281
+ return scipy.optimize.newton(func, init_guess, maxiter=maxiter)
282
+
283
+
284
+ def rh_critical_sac(air_temperature: ArrayLike, T_sat_liquid: ArrayLike, G: ArrayLike) -> ArrayLike:
285
+ r"""Calculate critical relative humidity threshold of contrail formation.
286
+
287
+ Parameters
288
+ ----------
289
+ air_temperature : ArrayLike
290
+ A sequence or array of temperature values, [:math:`K`]
291
+ T_sat_liquid : ArrayLike
292
+ Maximum threshold temperature for 100% relative humidity with respect to liquid, [:math:`K`]
293
+ G : ArrayLike
294
+ Slope of the mixing line in a temperature-humidity diagram.
295
+
296
+ Returns
297
+ -------
298
+ ArrayLike
299
+ Critical relative humidity of contrail formation, [:math:`[0 - 1]`]
300
+
301
+ References
302
+ ----------
303
+ - :cite:`ponaterContrailsComprehensiveGlobal2002`
304
+ """
305
+ e_sat_T_sat_liquid = thermo.e_sat_liquid(T_sat_liquid) # always positive
306
+ e_sat_T = thermo.e_sat_liquid(air_temperature) # always positive
307
+
308
+ # Below, `rh_crit` can be negative
309
+ rh_crit = (G * (air_temperature - T_sat_liquid) + e_sat_T_sat_liquid) / e_sat_T
310
+
311
+ # Per Ponater, section 2.3:
312
+ # >>> The critical relative humidity `r_contr` can range from 0 to 1
313
+ # The "first order" term `G * (air_temperature - T_sat_liquid)` can be negative.
314
+ # Consequently, we clip rh_crit at 0 and 1.
315
+ # After clipping, when rh_crit = 0 the SAC is guaranteed to be satisfied.
316
+
317
+ # Per Ponater, beginning of section 2.3:
318
+ # >>> "No contrails are possible if T > T_contr" <<<
319
+ # In our notation, this inequality is `air_temperature > T_sat_liquid`
320
+ # We set the corresponding rh_crit to infinity, indicating no SAC is possible
321
+
322
+ # numpy case
323
+ if isinstance(rh_crit, np.ndarray):
324
+ rh_crit.clip(0.0, 1.0, out=rh_crit) # clip in place
325
+ rh_crit[air_temperature > T_sat_liquid] = np.inf
326
+ return rh_crit
327
+
328
+ # xarray case
329
+ # FIXME: the two cases handle nans differently. Unfortunately, unit tests break
330
+ # if I try to consolidate to a single condition
331
+ rh_crit = rh_crit.clip(0.0, 1.0)
332
+ return rh_crit.where(air_temperature <= T_sat_liquid, np.inf)
333
+
334
+
335
+ def sac(
336
+ rh: ArrayLike,
337
+ rh_crit_sac: ArrayLike,
338
+ ) -> ArrayLike:
339
+ r"""Points at which the Schmidt-Appleman Criteria is satisfied.
340
+
341
+ Parameters of type :class:`ArrayLike` must have compatible shapes.
342
+
343
+ Parameters
344
+ ----------
345
+ rh : ArrayLike
346
+ Relative humidity values
347
+ rh_crit_sac: ArrayLike
348
+ Critical relative humidity threshold of contrail formation
349
+
350
+ Returns
351
+ -------
352
+ ArrayLike
353
+ SAC state of each point indexed by the :class:`ArrayLike` parameters.
354
+ Returned array has floating ``dtype`` with values
355
+
356
+ - 0.0 signifying SAC fails
357
+ - 1.0 signifying SAC holds
358
+
359
+ NaN entries of parameters propagate into the returned array.
360
+ """
361
+ nan_mask = np.isnan(rh) | np.isnan(rh_crit_sac)
362
+
363
+ dtype = np.result_type(rh, rh_crit_sac)
364
+ sac_ = (rh > rh_crit_sac).astype(dtype)
365
+ return apply_nan_mask_to_arraylike(sac_, nan_mask)
366
+
367
+
368
+ def T_critical_sac(
369
+ T_LM: ArrayLike,
370
+ relative_humidity: ArrayLike,
371
+ G: ArrayLike,
372
+ maxiter: int = 10,
373
+ ) -> ArrayLike:
374
+ r"""Estimate temperature threshold for persistent contrail formation.
375
+
376
+ This quantity is defined as ``T_LC`` in Schumann (see reference below). Equation (11)
377
+ of this paper implicitly defines ``T_LC`` as the solution to the equation
378
+ ::
379
+
380
+ T_LC = T_LM - (e_L(T_LM) - rh * e_L(T_LC)) / G
381
+
382
+ For relative humidity above 0.999, the corresponding entry from ``T_LM``
383
+ is returned (page 10, top of the right-hand column). Otherwise, the solution
384
+ to the equation above is approximated via Newton's method.
385
+
386
+ Parameters
387
+ ----------
388
+ T_LM : ArrayLike
389
+ Output of :func:`T_sat_liquid` calculation.
390
+ relative_humidity : ArrayLike
391
+ Relative humidity values
392
+ G : ArrayLike
393
+ Slope of the mixing line in a temperature-humidity diagram.
394
+ maxiter : int, optional
395
+ Passed into :func:`scipy.optimize.newton`. By default, 10.
396
+
397
+ Returns
398
+ -------
399
+ ArrayLike
400
+ Critical temperature threshold values.
401
+
402
+ References
403
+ ----------
404
+ - :cite:`schumannConditionsContrailFormation1996`
405
+ """
406
+ # Near U = 1, Newton's method is slow to converge (I believe this is because
407
+ # the function `func` has a double root at T_LM when U = 1, so Newton's method
408
+ # is somewhat degenerate here)
409
+ # But the answer is known in this case. This is discussed at the top of the
410
+ # right hand column on page 10 in Schumann 1996.
411
+ # We only apply Newton's method at points with rh bounded below 1 (scipy will
412
+ # raise an error if Newton's method is not converging well).
413
+ filt = (relative_humidity < 0.999) & np.isfinite(T_LM)
414
+ if not np.any(filt):
415
+ return T_LM
416
+
417
+ U_filt = relative_humidity[filt]
418
+ T_LM_filt = T_LM[filt]
419
+ e_L_of_T_LM_filt = thermo.e_sat_liquid(T_LM_filt)
420
+ G_filt = G[filt]
421
+
422
+ def func(T: ArrayLike) -> ArrayLike:
423
+ """Equation (11) from Schumann."""
424
+ return T - T_LM_filt + (e_L_of_T_LM_filt - U_filt * thermo.e_sat_liquid(T)) / G_filt
425
+
426
+ def fprime(T: ArrayLike) -> ArrayLike:
427
+ return 1.0 - U_filt * thermo.e_sat_liquid_prime(T) / G_filt
428
+
429
+ # This initial guess should be less than T_LM.
430
+ # For relative_humidity away from 1, Newton's method converges quickly, and so
431
+ # any initial guess will work.
432
+ init_guess = T_LM_filt - 1.0
433
+ newton_approx = scipy.optimize.newton(
434
+ func, init_guess, fprime=fprime, maxiter=maxiter, disp=False
435
+ )
436
+
437
+ # For relative_humidity > 0.999, we just use T_LM
438
+ # We copy over the entire array T_LM here instead of using np.empty_like
439
+ # in order to keep typing compatible with xarray types
440
+ out = T_LM.copy()
441
+ out[filt] = newton_approx
442
+ return out