pycontrails 0.54.0__cp312-cp312-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 +2314 -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-312-darwin.so +0 -0
  18. pycontrails/core/vector.py +2190 -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 +746 -0
  24. pycontrails/datalib/ecmwf/__init__.py +73 -0
  25. pycontrails/datalib/ecmwf/arco_era5.py +340 -0
  26. pycontrails/datalib/ecmwf/common.py +109 -0
  27. pycontrails/datalib/ecmwf/era5.py +550 -0
  28. pycontrails/datalib/ecmwf/era5_model_level.py +487 -0
  29. pycontrails/datalib/ecmwf/hres.py +782 -0
  30. pycontrails/datalib/ecmwf/hres_model_level.py +459 -0
  31. pycontrails/datalib/ecmwf/ifs.py +284 -0
  32. pycontrails/datalib/ecmwf/model_levels.py +434 -0
  33. pycontrails/datalib/ecmwf/static/model_level_dataframe_v20240418.csv +139 -0
  34. pycontrails/datalib/ecmwf/variables.py +267 -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 +569 -0
  40. pycontrails/datalib/sentinel.py +511 -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 +430 -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 +982 -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 +2616 -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 +494 -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.54.0.dist-info/LICENSE +178 -0
  105. pycontrails-0.54.0.dist-info/METADATA +179 -0
  106. pycontrails-0.54.0.dist-info/NOTICE +43 -0
  107. pycontrails-0.54.0.dist-info/RECORD +109 -0
  108. pycontrails-0.54.0.dist-info/WHEEL +5 -0
  109. pycontrails-0.54.0.dist-info/top_level.txt +3 -0
@@ -0,0 +1,103 @@
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
+ B701,B752
29
+ B712,B712
30
+ B720,B752
31
+ B721,B752
32
+ B722,B722
33
+ B732,B732
34
+ B733,B733
35
+ B734,B734
36
+ B735,B735
37
+ B736,B736
38
+ B737,B737
39
+ B738,B738
40
+ B739,B739
41
+ B741,B743
42
+ B742,B742
43
+ B743,B743
44
+ B744,B744
45
+ B748,B748
46
+ B74D,B743
47
+ B74R,B743
48
+ B74S,B742
49
+ B752,B752
50
+ B753,B753
51
+ B762,B762
52
+ B763,B763
53
+ B764,B764
54
+ B772,B772
55
+ B773,B773
56
+ B77L,B77L
57
+ B77W,B77W
58
+ B788,B788
59
+ B789,B789
60
+ B78X,B78X
61
+ BCS1,BCS1
62
+ BCS3,BCS3
63
+ BLCF,B77W
64
+ C141,A310
65
+ C17,B764
66
+ C5,A345
67
+ C5M,A345
68
+ C919,A20N
69
+ CRJ7,CRJ9
70
+ CRJ9,CRJ9
71
+ DC91,B712
72
+ DC93,DC93
73
+ E135,E135
74
+ E145,E145
75
+ E170,E170
76
+ E190,E190
77
+ E195,E195
78
+ E290,E290
79
+ E295,E295
80
+ E3CF,B762
81
+ E3TF,B762
82
+ E737,B738
83
+ E75L,E75L
84
+ E75S,E75S
85
+ E767,B763
86
+ GLF5,GLF5
87
+ IL62,A30B
88
+ J328,E135
89
+ KC39,A321
90
+ MC23,A20N
91
+ MD81,MD82
92
+ MD82,MD82
93
+ MD83,MD83
94
+ MD87,MD82
95
+ MD88,MD82
96
+ MD90,MD83
97
+ NIM,B738
98
+ P1,B38M
99
+ P8,B738
100
+ R721,B722
101
+ R722,B722
102
+ RJ1H,RJ1H
103
+ SLCH,A388
@@ -0,0 +1,459 @@
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, ArrayScalarLike, 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)
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 _e_sat_liquid_prime(T: ArrayScalarLike) -> ArrayScalarLike:
243
+ r"""Calculate derivative of :func:`thermo.e_sat_liquid`.
244
+
245
+ Parameters
246
+ ----------
247
+ T : ArrayScalarLike
248
+ Temperature, [:math:`K`].
249
+
250
+ Returns
251
+ -------
252
+ ArrayScalarLike
253
+ Derivative of :func:`thermo.e_sat_liquid`, [:math:``Pa \ K^{-1}`].
254
+ """
255
+ d_inside = 6096.9385 / (T**2) - 0.02711193 + 1.673952 * 1e-5 * 2 * T + 2.433502 / T
256
+ return thermo.e_sat_liquid(T) * d_inside
257
+
258
+
259
+ def T_sat_liquid_high_accuracy(
260
+ G: ArrayLike,
261
+ maxiter: int = 5,
262
+ ) -> ArrayLike:
263
+ """Calculate temperature at which liquid saturation curve has slope G.
264
+
265
+ The function :func:`T_sat_liquid` gives a first order approximation to equation (10)
266
+ of the Schumann paper referenced below. This function uses Newton's method to
267
+ compute the numeric solution to (10).
268
+
269
+ Parameters
270
+ ----------
271
+ G : ArrayLike
272
+ Slope of the mixing line
273
+ maxiter : int, optional
274
+ Passed into :func:`scipy.optimize.newton`. Because ``T_sat_liquid`` is already
275
+ fairly accurate, few iterations are needed for Newton's method to converge.
276
+ By default, 5.
277
+
278
+ Returns
279
+ -------
280
+ ArrayLike
281
+ Maximum threshold temperature for 100% relative humidity with respect to liquid,
282
+ [:math:`K`].
283
+
284
+ References
285
+ ----------
286
+ - :cite:`schumannConditionsContrailFormation1996`
287
+
288
+ See Also
289
+ --------
290
+ :func:`T_sat_liquid_high`
291
+ """
292
+ init_guess = T_sat_liquid(G)
293
+
294
+ def func(T: ArrayLike) -> ArrayLike:
295
+ """Equation (10) from Schumann 1996."""
296
+ return _e_sat_liquid_prime(T) - G
297
+
298
+ return scipy.optimize.newton(func, init_guess, maxiter=maxiter)
299
+
300
+
301
+ def rh_critical_sac(air_temperature: ArrayLike, T_sat_liquid: ArrayLike, G: ArrayLike) -> ArrayLike:
302
+ r"""Calculate critical relative humidity threshold of contrail formation.
303
+
304
+ Parameters
305
+ ----------
306
+ air_temperature : ArrayLike
307
+ A sequence or array of temperature values, [:math:`K`]
308
+ T_sat_liquid : ArrayLike
309
+ Maximum threshold temperature for 100% relative humidity with respect to liquid, [:math:`K`]
310
+ G : ArrayLike
311
+ Slope of the mixing line in a temperature-humidity diagram.
312
+
313
+ Returns
314
+ -------
315
+ ArrayLike
316
+ Critical relative humidity of contrail formation, [:math:`[0 - 1]`]
317
+
318
+ References
319
+ ----------
320
+ - :cite:`ponaterContrailsComprehensiveGlobal2002`
321
+ """
322
+ e_sat_T_sat_liquid = thermo.e_sat_liquid(T_sat_liquid) # always positive
323
+ e_sat_T = thermo.e_sat_liquid(air_temperature) # always positive
324
+
325
+ # Below, `rh_crit` can be negative
326
+ rh_crit = (G * (air_temperature - T_sat_liquid) + e_sat_T_sat_liquid) / e_sat_T
327
+
328
+ # Per Ponater, section 2.3:
329
+ # >>> The critical relative humidity `r_contr` can range from 0 to 1
330
+ # The "first order" term `G * (air_temperature - T_sat_liquid)` can be negative.
331
+ # Consequently, we clip rh_crit at 0 and 1.
332
+ # After clipping, when rh_crit = 0 the SAC is guaranteed to be satisfied.
333
+
334
+ # Per Ponater, beginning of section 2.3:
335
+ # >>> "No contrails are possible if T > T_contr" <<<
336
+ # In our notation, this inequality is `air_temperature > T_sat_liquid`
337
+ # We set the corresponding rh_crit to infinity, indicating no SAC is possible
338
+
339
+ # numpy case
340
+ if isinstance(rh_crit, np.ndarray):
341
+ rh_crit.clip(0.0, 1.0, out=rh_crit) # clip in place
342
+ rh_crit[air_temperature > T_sat_liquid] = np.inf
343
+ return rh_crit
344
+
345
+ # xarray case
346
+ # FIXME: the two cases handle nans differently. Unfortunately, unit tests break
347
+ # if I try to consolidate to a single condition
348
+ rh_crit = rh_crit.clip(0.0, 1.0)
349
+ return rh_crit.where(air_temperature <= T_sat_liquid, np.inf)
350
+
351
+
352
+ def sac(
353
+ rh: ArrayLike,
354
+ rh_crit_sac: ArrayLike,
355
+ ) -> ArrayLike:
356
+ r"""Points at which the Schmidt-Appleman Criteria is satisfied.
357
+
358
+ Parameters of type :class:`ArrayLike` must have compatible shapes.
359
+
360
+ Parameters
361
+ ----------
362
+ rh : ArrayLike
363
+ Relative humidity values
364
+ rh_crit_sac: ArrayLike
365
+ Critical relative humidity threshold of contrail formation
366
+
367
+ Returns
368
+ -------
369
+ ArrayLike
370
+ SAC state of each point indexed by the :class:`ArrayLike` parameters.
371
+ Returned array has floating ``dtype`` with values
372
+
373
+ - 0.0 signifying SAC fails
374
+ - 1.0 signifying SAC holds
375
+
376
+ NaN entries of parameters propagate into the returned array.
377
+ """
378
+ nan_mask = np.isnan(rh) | np.isnan(rh_crit_sac)
379
+
380
+ dtype = np.result_type(rh, rh_crit_sac)
381
+ sac_ = (rh > rh_crit_sac).astype(dtype)
382
+ return apply_nan_mask_to_arraylike(sac_, nan_mask)
383
+
384
+
385
+ def T_critical_sac(
386
+ T_LM: ArrayLike,
387
+ relative_humidity: ArrayLike,
388
+ G: ArrayLike,
389
+ maxiter: int = 10,
390
+ ) -> ArrayLike:
391
+ r"""Estimate temperature threshold for persistent contrail formation.
392
+
393
+ This quantity is defined as ``T_LC`` in Schumann (see reference below). Equation (11)
394
+ of this paper implicitly defines ``T_LC`` as the solution to the equation
395
+ ::
396
+
397
+ T_LC = T_LM - (e_L(T_LM) - rh * e_L(T_LC)) / G
398
+
399
+ For relative humidity above 0.999, the corresponding entry from ``T_LM``
400
+ is returned (page 10, top of the right-hand column). Otherwise, the solution
401
+ to the equation above is approximated via Newton's method.
402
+
403
+ Parameters
404
+ ----------
405
+ T_LM : ArrayLike
406
+ Output of :func:`T_sat_liquid` calculation.
407
+ relative_humidity : ArrayLike
408
+ Relative humidity values
409
+ G : ArrayLike
410
+ Slope of the mixing line in a temperature-humidity diagram.
411
+ maxiter : int, optional
412
+ Passed into :func:`scipy.optimize.newton`. By default, 10.
413
+
414
+ Returns
415
+ -------
416
+ ArrayLike
417
+ Critical temperature threshold values.
418
+
419
+ References
420
+ ----------
421
+ - :cite:`schumannConditionsContrailFormation1996`
422
+ """
423
+ # Near U = 1, Newton's method is slow to converge (I believe this is because
424
+ # the function `func` has a double root at T_LM when U = 1, so Newton's method
425
+ # is somewhat degenerate here)
426
+ # But the answer is known in this case. This is discussed at the top of the
427
+ # right hand column on page 10 in Schumann 1996.
428
+ # We only apply Newton's method at points with rh bounded below 1 (scipy will
429
+ # raise an error if Newton's method is not converging well).
430
+ filt = (relative_humidity < 0.999) & np.isfinite(T_LM)
431
+ if not np.any(filt):
432
+ return T_LM
433
+
434
+ U_filt = relative_humidity[filt]
435
+ T_LM_filt = T_LM[filt]
436
+ e_L_of_T_LM_filt = thermo.e_sat_liquid(T_LM_filt)
437
+ G_filt = G[filt]
438
+
439
+ def func(T: ArrayLike) -> ArrayLike:
440
+ """Equation (11) from Schumann."""
441
+ return T - T_LM_filt + (e_L_of_T_LM_filt - U_filt * thermo.e_sat_liquid(T)) / G_filt
442
+
443
+ def fprime(T: ArrayLike) -> ArrayLike:
444
+ return 1.0 - U_filt * _e_sat_liquid_prime(T) / G_filt
445
+
446
+ # This initial guess should be less than T_LM.
447
+ # For relative_humidity away from 1, Newton's method converges quickly, and so
448
+ # any initial guess will work.
449
+ init_guess = T_LM_filt - 1.0
450
+ newton_approx = scipy.optimize.newton(
451
+ func, init_guess, fprime=fprime, maxiter=maxiter, disp=False
452
+ )
453
+
454
+ # For relative_humidity > 0.999, we just use T_LM
455
+ # We copy over the entire array T_LM here instead of using np.empty_like
456
+ # in order to keep typing compatible with xarray types
457
+ out = T_LM.copy()
458
+ out[filt] = newton_approx
459
+ return out