quantmet-tools 0.4.4__py3-none-any.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.
- quantmet/__init__.py +10 -0
- quantmet/conservation.py +200 -0
- quantmet/constants.py +69 -0
- quantmet/dct_spectra.py +105 -0
- quantmet/degdays.py +386 -0
- quantmet/ensemble.py +120 -0
- quantmet/epflux.py +306 -0
- quantmet/gill.py +286 -0
- quantmet/helmholtz.py +161 -0
- quantmet/packets.py +51 -0
- quantmet/pv.py +143 -0
- quantmet/qbo.py +112 -0
- quantmet/stats.py +368 -0
- quantmet/tc.py +320 -0
- quantmet/tc_detect.c +116 -0
- quantmet/tc_detect.py +181 -0
- quantmet/tem.py +360 -0
- quantmet/thermo.py +860 -0
- quantmet/verify.py +425 -0
- quantmet/waf.py +598 -0
- quantmet/wk_filter.py +202 -0
- quantmet_tools-0.4.4.dist-info/METADATA +461 -0
- quantmet_tools-0.4.4.dist-info/RECORD +26 -0
- quantmet_tools-0.4.4.dist-info/WHEEL +5 -0
- quantmet_tools-0.4.4.dist-info/licenses/LICENSE +9 -0
- quantmet_tools-0.4.4.dist-info/top_level.txt +1 -0
quantmet/thermo.py
ADDED
|
@@ -0,0 +1,860 @@
|
|
|
1
|
+
"""Moist thermodynamics, column integrals and apparent temperature, numpy-first.
|
|
2
|
+
|
|
3
|
+
UNITS. Thermodynamic functions take temperature in K, pressure in Pa, specific humidity q and the condensate mass
|
|
4
|
+
fractions ql, qi in kg/kg of moist air, mixing ratios r in kg/kg of dry air, and relative humidity in PERCENT (0-100),
|
|
5
|
+
and return SI. The two apparent-temperature indices keep the units their agencies define them in (heat index:
|
|
6
|
+
degF and %; wind chill: degF and mph); the unit helpers at the end convert. Every function broadcasts.
|
|
7
|
+
|
|
8
|
+
CONSTANTS come from ``quantmet.constants``: RD 287.04, RV 461.5, EPSILON = RD/RV, CPD 1004.7, CPV 1846, CL 4218,
|
|
9
|
+
CI 2106, LV0 2.501e6, LF0 3.337e5, G 9.80665, T0 273.15, P0 1e5. Coefficients that belong to a published fit
|
|
10
|
+
(Magnus, Bolton's 0.2854, Romps's constant set) stay with their formula so that it reproduces the paper.
|
|
11
|
+
|
|
12
|
+
Numerical notes that matter in practice:
|
|
13
|
+
- MAGNUS IS A FIT, AND WHICH FIT MATTERS. Alduchov & Eskridge (1996) coefficients (the default) stay within 0.4 % of
|
|
14
|
+
the reference vapour pressure over -40..50 degC; Bolton's (1980) are within 0.3 % over -30..35 degC and are the ones
|
|
15
|
+
his theta_e formula was fitted with, so ``equivalent_potential_temperature`` uses them internally. Over ice the
|
|
16
|
+
Magnus-type fit uses its own coefficients; "auto" switches at 0 degC. Murphy & Koop (2005) is the reference-quality
|
|
17
|
+
option (and what the Magnus accuracy claims are tested against).
|
|
18
|
+
- PRESSURE-AWARE HUMIDITY. q = eps e / (p - (1 - eps) e) and r = eps e / (p - e) are exact; the familiar
|
|
19
|
+
q ~ r ~ eps e/p is low by (1 - eps) e/p for q and e/p for r: 1.6 % and 4.2 % at a 30 degC dewpoint and 1000 hPa,
|
|
20
|
+
more at lower pressure. Where e >= p (a hot parcel at stratospheric pressure) the saturation humidities are NaN,
|
|
21
|
+
not negative.
|
|
22
|
+
- STULL'S WET BULB ASSUMES SEA-LEVEL PRESSURE. Stull (2011) is a fit at 1013.25 hPa, valid for RH 5-99 % and
|
|
23
|
+
T -20..50 degC except where it is both cold and dry (``wet_bulb_stull`` returns NaN outside RH 5-99 %,
|
|
24
|
+
T -20..50 degC unless strict=False). The psychrometric ``wet_bulb_temperature`` solves the isobaric enthalpy
|
|
25
|
+
balance at the actual pressure (it agrees with the WMO psychrometer formula and with Normand's rule to 0.1-0.3 degC).
|
|
26
|
+
Even at sea level Stull runs up to ~1 degC warm in hot, moderately dry air (+0.8 degC at 40 degC and 30 %); on a
|
|
27
|
+
high plateau the pressure adds to that: at 840 hPa, 40 degC and 15 % the wet bulb is 19.2 degC against Stull's
|
|
28
|
+
20.8 degC (20.4 degC at sea level, so 1.1 degC of the 1.5 degC is pressure alone) -- in heat-stress work the
|
|
29
|
+
difference between a dangerous and a survivable wet bulb.
|
|
30
|
+
- HEAT INDEX: SIMPLE FORMULA FIRST. The NWS computes Steadman's simple formula, averages it with T, and only if that
|
|
31
|
+
average is >= 80 degF uses the Rothfusz regression with its two adjustments. Rothfusz everywhere is wrong below
|
|
32
|
+
~80 degF, where it was never fitted: 76.9 degF at 70 degF and 50 % (NWS: 69.1), 78.7 at 60 degF and 30 % (57.1).
|
|
33
|
+
- COLUMN INTEGRALS ARE 0 -> p_s. Layer edges halfway between levels, the top edge at 0 Pa (or ``top``), the bottom at
|
|
34
|
+
the surface pressure; levels below ground get zero weight, so the values a reanalysis extrapolates (or NaN) there
|
|
35
|
+
never enter. Without p_s the lowest level is taken as the surface (half a layer below it).
|
|
36
|
+
|
|
37
|
+
References: Alduchov & Eskridge (1996, J. Appl. Meteor. 35, 601-609); Bolton (1980, Mon. Wea. Rev. 108, 1046-1053);
|
|
38
|
+
Murphy & Koop (2005, Q. J. R. Meteorol. Soc. 131, 1539-1565); Emanuel (1994, Atmospheric Convection, ch. 4);
|
|
39
|
+
Betts (1973, Q. J. R. Meteorol. Soc. 99, 178-196); Tripoli & Cotton (1981, Mon. Wea. Rev. 109, 1094-1102);
|
|
40
|
+
Davies-Jones (2008, Mon. Wea. Rev. 136, 2764-2785); Romps (2017, J. Atmos. Sci. 74, 3891-3900); Stull (2011,
|
|
41
|
+
J. Appl. Meteor. Climatol. 50, 2267-2269); AMS Glossary, "pseudoadiabatic lapse rate"; Rothfusz (1990, NWS SR
|
|
42
|
+
Technical Attachment 90-23) and the NWS heat index equation page; Steadman (1979, J. Appl. Meteor. 18, 861-873);
|
|
43
|
+
Osczevski & Bluestein (2005, Bull. Amer. Meteor. Soc. 86, 1453-1458) for the 2001 wind chill.
|
|
44
|
+
|
|
45
|
+
Requires: numpy, scipy (Lambert W for the Romps LCL).
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
from __future__ import annotations
|
|
49
|
+
|
|
50
|
+
import numpy as np
|
|
51
|
+
|
|
52
|
+
from .constants import CI, CL, CPD, CPV, EPSILON, G, KAPPA, LF0, LV0, P0, RD, RV, T0
|
|
53
|
+
|
|
54
|
+
__all__ = [
|
|
55
|
+
# vapour pressure and humidity
|
|
56
|
+
"saturation_vapour_pressure",
|
|
57
|
+
"vapour_pressure",
|
|
58
|
+
"dewpoint",
|
|
59
|
+
"relative_humidity",
|
|
60
|
+
"dewpoint_from_rh",
|
|
61
|
+
"mixing_ratio",
|
|
62
|
+
"specific_humidity",
|
|
63
|
+
"vapour_pressure_from_mixing_ratio",
|
|
64
|
+
"vapour_pressure_from_specific_humidity",
|
|
65
|
+
"specific_humidity_from_mixing_ratio",
|
|
66
|
+
"mixing_ratio_from_specific_humidity",
|
|
67
|
+
"specific_humidity_from_dewpoint",
|
|
68
|
+
"dewpoint_from_specific_humidity",
|
|
69
|
+
"relative_humidity_from_specific_humidity",
|
|
70
|
+
"specific_humidity_from_relative_humidity",
|
|
71
|
+
"saturation_mixing_ratio",
|
|
72
|
+
"saturation_specific_humidity",
|
|
73
|
+
"dewpoint_depression",
|
|
74
|
+
"vapour_pressure_deficit",
|
|
75
|
+
"latent_heat",
|
|
76
|
+
# temperatures
|
|
77
|
+
"exner",
|
|
78
|
+
"potential_temperature",
|
|
79
|
+
"temperature_from_potential_temperature",
|
|
80
|
+
"virtual_temperature",
|
|
81
|
+
"virtual_potential_temperature",
|
|
82
|
+
"density_temperature",
|
|
83
|
+
"air_density",
|
|
84
|
+
"equivalent_temperature",
|
|
85
|
+
"equivalent_potential_temperature",
|
|
86
|
+
"saturation_equivalent_potential_temperature",
|
|
87
|
+
"liquid_water_potential_temperature",
|
|
88
|
+
"ice_liquid_potential_temperature",
|
|
89
|
+
"wet_bulb_potential_temperature",
|
|
90
|
+
"wet_bulb_stull",
|
|
91
|
+
"stull_valid",
|
|
92
|
+
"wet_bulb_temperature",
|
|
93
|
+
# LCL, energies, adiabats
|
|
94
|
+
"lcl_romps",
|
|
95
|
+
"lcl_bolton",
|
|
96
|
+
"dry_static_energy",
|
|
97
|
+
"moist_static_energy",
|
|
98
|
+
"enthalpy",
|
|
99
|
+
"moist_lapse_rate",
|
|
100
|
+
"moist_adiabat",
|
|
101
|
+
# columns
|
|
102
|
+
"column_integral",
|
|
103
|
+
"precipitable_water",
|
|
104
|
+
"column_water",
|
|
105
|
+
"column_moist_static_energy",
|
|
106
|
+
"column_enthalpy",
|
|
107
|
+
"layer_mean_temperature",
|
|
108
|
+
"thickness",
|
|
109
|
+
# apparent temperature and units
|
|
110
|
+
"heat_index",
|
|
111
|
+
"wind_chill",
|
|
112
|
+
"c_to_f",
|
|
113
|
+
"f_to_c",
|
|
114
|
+
"c_to_k",
|
|
115
|
+
"k_to_c",
|
|
116
|
+
"f_to_k",
|
|
117
|
+
"k_to_f",
|
|
118
|
+
"ms_to_kt",
|
|
119
|
+
"kt_to_ms",
|
|
120
|
+
"ms_to_mph",
|
|
121
|
+
"mph_to_ms",
|
|
122
|
+
"kt_to_mph",
|
|
123
|
+
"mph_to_kt",
|
|
124
|
+
]
|
|
125
|
+
|
|
126
|
+
# --------------------------------------------------------------------------
|
|
127
|
+
# Saturation vapour pressure
|
|
128
|
+
# --------------------------------------------------------------------------
|
|
129
|
+
# Magnus form e_s = c exp(a t / (b + t)), t in degC, c in Pa
|
|
130
|
+
_MAGNUS = {
|
|
131
|
+
("magnus", "liquid"): (610.94, 17.625, 243.04), # Alduchov & Eskridge (1996) AERK
|
|
132
|
+
("magnus", "ice"): (611.21, 22.587, 273.86), # Alduchov & Eskridge (1996) AERKi
|
|
133
|
+
("bolton", "liquid"): (611.2, 17.67, 243.5),
|
|
134
|
+
} # Bolton (1980) eq. 10
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _as(x):
|
|
138
|
+
return np.asarray(x, dtype=float)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _es_phase(t, phase, formula):
|
|
142
|
+
if formula == "murphy-koop":
|
|
143
|
+
lt = np.log(t)
|
|
144
|
+
if phase == "liquid": # Murphy & Koop (2005) eq. 10, 123 < T < 332 K
|
|
145
|
+
return np.exp(
|
|
146
|
+
54.842763
|
|
147
|
+
- 6763.22 / t
|
|
148
|
+
- 4.210 * lt
|
|
149
|
+
+ 0.000367 * t
|
|
150
|
+
+ np.tanh(0.0415 * (t - 218.8)) * (53.878 - 1331.22 / t - 9.44523 * lt + 0.014025 * t)
|
|
151
|
+
)
|
|
152
|
+
if phase == "ice": # Murphy & Koop (2005) eq. 7, T > 110 K
|
|
153
|
+
return np.exp(9.550426 - 5723.265 / t + 3.53068 * lt - 0.00728332 * t)
|
|
154
|
+
raise ValueError(f"phase must be 'liquid', 'ice' or 'auto', not {phase!r}")
|
|
155
|
+
try:
|
|
156
|
+
c, a, b = _MAGNUS[(formula, phase)]
|
|
157
|
+
except KeyError:
|
|
158
|
+
raise ValueError(
|
|
159
|
+
f"no {formula!r} formula over {phase!r} (formulas: magnus [liquid, ice], "
|
|
160
|
+
"bolton [liquid], murphy-koop [liquid, ice])"
|
|
161
|
+
) from None
|
|
162
|
+
tc = t - T0
|
|
163
|
+
return c * np.exp(a * tc / (b + tc))
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def saturation_vapour_pressure(t, phase: str = "liquid", formula: str = "magnus"):
|
|
167
|
+
"""Saturation vapour pressure (Pa) at temperature t (K).
|
|
168
|
+
|
|
169
|
+
phase : "liquid", "ice", or "auto" (ice below T0 = 273.15 K, liquid above)
|
|
170
|
+
formula : "magnus" (Alduchov & Eskridge 1996: liquid c, a, b = 610.94 Pa, 17.625, 243.04 degC; ice 611.21 Pa,
|
|
171
|
+
22.587, 273.86 degC), "bolton" (Bolton 1980: 611.2 Pa, 17.67, 243.5 degC; liquid only) or
|
|
172
|
+
"murphy-koop" (Murphy & Koop 2005, reference accuracy, liquid 123-332 K, ice > 110 K)."""
|
|
173
|
+
t = _as(t)
|
|
174
|
+
if phase == "auto":
|
|
175
|
+
return np.where(t < T0, _es_phase(t, "ice", formula), _es_phase(t, "liquid", formula))
|
|
176
|
+
return _es_phase(t, phase, formula)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def vapour_pressure(td, formula: str = "magnus"):
|
|
180
|
+
"""Vapour pressure (Pa) from the dewpoint td (K): e = e_s,liquid(td)."""
|
|
181
|
+
return saturation_vapour_pressure(td, "liquid", formula)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def dewpoint(e, phase: str = "liquid", formula: str = "magnus"):
|
|
185
|
+
"""Dewpoint (K) from vapour pressure e (Pa), the inverse of the saturation vapour pressure; phase="ice" gives the
|
|
186
|
+
frost point. Magnus and Bolton invert analytically; Murphy-Koop by Newton iteration from the Magnus value."""
|
|
187
|
+
e = _as(e)
|
|
188
|
+
if phase not in ("liquid", "ice"):
|
|
189
|
+
raise ValueError("dewpoint inverts over 'liquid' (dewpoint) or 'ice' (frost point)")
|
|
190
|
+
base = "bolton" if formula == "bolton" else "magnus"
|
|
191
|
+
if (base, phase) not in _MAGNUS:
|
|
192
|
+
raise ValueError(f"no {formula!r} formula over {phase!r}")
|
|
193
|
+
c, a, b = _MAGNUS[(base, phase)]
|
|
194
|
+
with np.errstate(divide="ignore", invalid="ignore"):
|
|
195
|
+
x = np.log(e / c)
|
|
196
|
+
td = T0 + b * x / (a - x)
|
|
197
|
+
if formula == "murphy-koop":
|
|
198
|
+
for _ in range(6):
|
|
199
|
+
h = 1e-3
|
|
200
|
+
f0 = np.log(_es_phase(td, phase, formula))
|
|
201
|
+
df = (np.log(_es_phase(td + h, phase, formula)) - f0) / h
|
|
202
|
+
td = td - (f0 - np.log(e)) / df
|
|
203
|
+
elif formula not in ("magnus", "bolton"):
|
|
204
|
+
raise ValueError(f"unknown formula {formula!r}")
|
|
205
|
+
return td
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def relative_humidity(t, td, phase: str = "liquid", formula: str = "magnus"):
|
|
209
|
+
"""Relative humidity (%) = 100 e / e_s(t), with e = e_s,liquid(td). phase selects the saturation reference
|
|
210
|
+
("liquid", "ice" or "auto"); the dewpoint is always over liquid."""
|
|
211
|
+
return 100.0 * vapour_pressure(td, formula) / saturation_vapour_pressure(t, phase, formula)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def dewpoint_from_rh(t, rh, formula: str = "magnus"):
|
|
215
|
+
"""Dewpoint (K) from temperature (K) and relative humidity (%, over liquid)."""
|
|
216
|
+
return dewpoint(_as(rh) / 100.0 * saturation_vapour_pressure(t, "liquid", formula), "liquid", formula)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def mixing_ratio(e, p):
|
|
220
|
+
"""Mixing ratio (kg/kg of dry air) r = eps e / (p - e); NaN where e >= p."""
|
|
221
|
+
e = _as(e)
|
|
222
|
+
p = _as(p)
|
|
223
|
+
with np.errstate(divide="ignore", invalid="ignore"):
|
|
224
|
+
return np.where(e < p, EPSILON * e / (p - e), np.nan)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def specific_humidity(e, p):
|
|
228
|
+
"""Specific humidity (kg/kg of moist air) q = eps e / (p - (1 - eps) e); NaN where e >= p."""
|
|
229
|
+
e = _as(e)
|
|
230
|
+
p = _as(p)
|
|
231
|
+
with np.errstate(divide="ignore", invalid="ignore"):
|
|
232
|
+
return np.where(e < p, EPSILON * e / (p - (1.0 - EPSILON) * e), np.nan)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def vapour_pressure_from_mixing_ratio(r, p):
|
|
236
|
+
"""e = r p / (eps + r) (Pa)."""
|
|
237
|
+
r = _as(r)
|
|
238
|
+
return r * _as(p) / (EPSILON + r)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def vapour_pressure_from_specific_humidity(q, p):
|
|
242
|
+
"""e = q p / (eps + (1 - eps) q) (Pa)."""
|
|
243
|
+
q = _as(q)
|
|
244
|
+
return q * _as(p) / (EPSILON + (1.0 - EPSILON) * q)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def specific_humidity_from_mixing_ratio(r):
|
|
248
|
+
"""q = r / (1 + r)."""
|
|
249
|
+
r = _as(r)
|
|
250
|
+
return r / (1.0 + r)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def mixing_ratio_from_specific_humidity(q):
|
|
254
|
+
"""r = q / (1 - q)."""
|
|
255
|
+
q = _as(q)
|
|
256
|
+
return q / (1.0 - q)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def specific_humidity_from_dewpoint(td, p, formula: str = "magnus"):
|
|
260
|
+
"""q (kg/kg) from dewpoint (K) and pressure (Pa)."""
|
|
261
|
+
return specific_humidity(vapour_pressure(td, formula), p)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def dewpoint_from_specific_humidity(q, p, formula: str = "magnus"):
|
|
265
|
+
"""Dewpoint (K) from q (kg/kg) and pressure (Pa)."""
|
|
266
|
+
return dewpoint(vapour_pressure_from_specific_humidity(q, p), "liquid", formula)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def relative_humidity_from_specific_humidity(t, q, p, phase: str = "liquid", formula: str = "magnus"):
|
|
270
|
+
"""RH (%) = 100 e / e_s(t) from q (kg/kg), t (K), p (Pa)."""
|
|
271
|
+
return 100.0 * vapour_pressure_from_specific_humidity(q, p) / saturation_vapour_pressure(t, phase, formula)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def specific_humidity_from_relative_humidity(t, rh, p, phase: str = "liquid", formula: str = "magnus"):
|
|
275
|
+
"""q (kg/kg) from RH (%), t (K), p (Pa)."""
|
|
276
|
+
return specific_humidity(_as(rh) / 100.0 * saturation_vapour_pressure(t, phase, formula), p)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def saturation_mixing_ratio(t, p, phase: str = "liquid", formula: str = "magnus"):
|
|
280
|
+
"""Saturation mixing ratio r_s (kg/kg) over liquid, ice or "auto"; NaN where e_s >= p."""
|
|
281
|
+
return mixing_ratio(saturation_vapour_pressure(t, phase, formula), p)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def saturation_specific_humidity(t, p, phase: str = "liquid", formula: str = "magnus"):
|
|
285
|
+
"""Saturation specific humidity q_s (kg/kg) over liquid, ice or "auto"; NaN where e_s >= p."""
|
|
286
|
+
return specific_humidity(saturation_vapour_pressure(t, phase, formula), p)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def dewpoint_depression(t, td):
|
|
290
|
+
"""T - Td (K)."""
|
|
291
|
+
return _as(t) - _as(td)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def vapour_pressure_deficit(t, td=None, rh=None, formula: str = "magnus"):
|
|
295
|
+
"""Vapour pressure deficit e_s(t) - e (Pa), from the dewpoint td (K) or the relative humidity rh (%)."""
|
|
296
|
+
es = saturation_vapour_pressure(t, "liquid", formula)
|
|
297
|
+
if td is not None:
|
|
298
|
+
return es - vapour_pressure(td, formula)
|
|
299
|
+
if rh is not None:
|
|
300
|
+
return es * (1.0 - _as(rh) / 100.0)
|
|
301
|
+
raise ValueError("give td or rh")
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def latent_heat(t, kind: str = "vaporisation"):
|
|
305
|
+
"""Latent heat (J/kg) at t (K) by Kirchhoff's law with constant heat capacities:
|
|
306
|
+
vaporisation LV0 + (CPV - CL)(t - T0), fusion LF0 + (CL - CI)(t - T0), sublimation the sum."""
|
|
307
|
+
t = _as(t)
|
|
308
|
+
lv = LV0 + (CPV - CL) * (t - T0)
|
|
309
|
+
lf = LF0 + (CL - CI) * (t - T0)
|
|
310
|
+
if kind == "vaporisation":
|
|
311
|
+
return lv
|
|
312
|
+
if kind == "fusion":
|
|
313
|
+
return lf
|
|
314
|
+
if kind == "sublimation":
|
|
315
|
+
return lv + lf
|
|
316
|
+
raise ValueError("kind is 'vaporisation', 'fusion' or 'sublimation'")
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
# --------------------------------------------------------------------------
|
|
320
|
+
# Potential, virtual, density and equivalent temperatures
|
|
321
|
+
# --------------------------------------------------------------------------
|
|
322
|
+
def _kappa(q):
|
|
323
|
+
if q is None:
|
|
324
|
+
return KAPPA
|
|
325
|
+
q = _as(q)
|
|
326
|
+
return ((1.0 - q) * RD + q * RV) / ((1.0 - q) * CPD + q * CPV)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def exner(p, p0: float = P0, q=None):
|
|
330
|
+
"""Exner function (p / p0)^kappa, kappa = RD/CPD, or the moist kappa when q is given."""
|
|
331
|
+
return (_as(p) / p0) ** _kappa(q)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def potential_temperature(t, p, q=None, p0: float = P0):
|
|
335
|
+
"""theta = T (p0 / p)^kappa (K). Dry kappa = RD/CPD = 0.2857 by default; with q (kg/kg) the exact Poisson exponent
|
|
336
|
+
of moist air, kappa_m = ((1-q) RD + q RV) / ((1-q) CPD + q CPV), conserved by an unsaturated adiabat of fixed q."""
|
|
337
|
+
return _as(t) * (p0 / _as(p)) ** _kappa(q)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def temperature_from_potential_temperature(theta, p, q=None, p0: float = P0):
|
|
341
|
+
"""T = theta (p / p0)^kappa (K), the inverse of potential_temperature."""
|
|
342
|
+
return _as(theta) * (_as(p) / p0) ** _kappa(q)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def virtual_temperature(t, q):
|
|
346
|
+
"""T_v = T (1 + (1/eps - 1) q) (K), exact for specific humidity q (no condensate)."""
|
|
347
|
+
return _as(t) * (1.0 + (1.0 / EPSILON - 1.0) * _as(q))
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def virtual_potential_temperature(t, p, q, p0: float = P0):
|
|
351
|
+
"""theta_v = T_v (p0 / p)^(RD/CPD) (K)."""
|
|
352
|
+
return potential_temperature(virtual_temperature(t, q), p, p0=p0)
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def density_temperature(t, q, ql=0.0, qi=0.0):
|
|
356
|
+
"""Density temperature T_rho = T (1 - q_t + q/eps) (K), q_t = q + ql + qi: the temperature dry air would need to
|
|
357
|
+
have the density of the cloudy air (condensate loading included). Equals T_v when ql = qi = 0 (Emanuel 1994)."""
|
|
358
|
+
q = _as(q)
|
|
359
|
+
qt = q + _as(ql) + _as(qi)
|
|
360
|
+
return _as(t) * (1.0 - qt + q / EPSILON)
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def air_density(p, t, q=0.0, ql=0.0, qi=0.0):
|
|
364
|
+
"""Density of moist (and cloudy) air (kg/m3) = p / (RD T_rho)."""
|
|
365
|
+
return _as(p) / (RD * density_temperature(t, q, ql, qi))
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def equivalent_temperature(t, q):
|
|
369
|
+
"""Isobaric equivalent temperature T_e = T + LV0 r / CPD (K), r = q/(1-q) (AMS Glossary)."""
|
|
370
|
+
return _as(t) + LV0 * mixing_ratio_from_specific_humidity(q) / CPD
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def _t_lcl_bolton(t, td):
|
|
374
|
+
"""Bolton (1980) eq. 15: temperature (K) at the lifting condensation level from T and Td (K)."""
|
|
375
|
+
return 1.0 / (1.0 / (td - 56.0) + np.log(t / td) / 800.0) + 56.0
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def equivalent_potential_temperature(t, td, p, method: str = "bolton", rt=None):
|
|
379
|
+
"""Equivalent potential temperature (K) from temperature t (K), dewpoint td (K) and pressure p (Pa).
|
|
380
|
+
|
|
381
|
+
method "bolton" -- pseudo-adiabatic, Bolton (1980) eq. 39, the most accurate of his formulas:
|
|
382
|
+
theta_E = T (p0/(p - e))^0.2854 (T/T_L)^(0.28 r) exp[(3036/T_L - 1.78) r (1 + 0.448 r)],
|
|
383
|
+
r in kg/kg, T_L from eq. 15, e from Bolton's own Magnus coefficients (the ones the fit used).
|
|
384
|
+
method "reversible" -- Emanuel (1994, sec. 4.5), conserved in reversible moist adiabatic processes:
|
|
385
|
+
theta_e = T (p0/p_d)^(RD/c) H^(-r RV/c) exp[L_v(T) r / (c T)], c = CPD + r_t CL,
|
|
386
|
+
H = e/e_s(T), p_d = p - e, r_t the total-water mixing ratio (default r: no condensate).
|
|
387
|
+
The two differ by a few K at high water content: a pseudo-adiabat drops its condensate (and its heat capacity)."""
|
|
388
|
+
t = _as(t)
|
|
389
|
+
td = _as(td)
|
|
390
|
+
p = _as(p)
|
|
391
|
+
if method == "bolton":
|
|
392
|
+
e = saturation_vapour_pressure(td, "liquid", "bolton")
|
|
393
|
+
r = EPSILON * e / (p - e)
|
|
394
|
+
tl = _t_lcl_bolton(t, td)
|
|
395
|
+
theta_dl = t * (P0 / (p - e)) ** 0.2854 * (t / tl) ** (0.28 * r)
|
|
396
|
+
return theta_dl * np.exp((3036.0 / tl - 1.78) * r * (1.0 + 0.448 * r))
|
|
397
|
+
if method == "reversible":
|
|
398
|
+
e = vapour_pressure(td)
|
|
399
|
+
r = EPSILON * e / (p - e)
|
|
400
|
+
rt = r if rt is None else _as(rt)
|
|
401
|
+
c = CPD + rt * CL
|
|
402
|
+
h = e / saturation_vapour_pressure(t)
|
|
403
|
+
return t * (P0 / (p - e)) ** (RD / c) * h ** (-r * RV / c) * np.exp(latent_heat(t) * r / (c * t))
|
|
404
|
+
raise ValueError("method is 'bolton' or 'reversible'")
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def saturation_equivalent_potential_temperature(t, p, method: str = "bolton"):
|
|
408
|
+
"""theta_e of saturated air at (t, p): equivalent_potential_temperature with td = t."""
|
|
409
|
+
return equivalent_potential_temperature(t, t, p, method)
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def liquid_water_potential_temperature(t, p, ql, p0: float = P0):
|
|
413
|
+
"""Liquid-water potential temperature (Betts 1973), theta_l = theta - (theta/T)(LV0/CPD) ql (K): conserved to
|
|
414
|
+
first order under reversible condensation and evaporation; the shallow-cloud LES/boundary-layer variable."""
|
|
415
|
+
th = potential_temperature(t, p, p0=p0)
|
|
416
|
+
return th - th / _as(t) * (LV0 / CPD) * _as(ql)
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def ice_liquid_potential_temperature(t, p, ql, qi, p0: float = P0):
|
|
420
|
+
"""Ice-liquid water potential temperature (Tripoli & Cotton 1981),
|
|
421
|
+
theta_il = theta / (1 + (LV0 ql + LS0 qi) / (CPD max(T, 253 K))) (K); the 253 K floor is theirs."""
|
|
422
|
+
th = potential_temperature(t, p, p0=p0)
|
|
423
|
+
return th / (1.0 + (LV0 * _as(ql) + (LV0 + LF0) * _as(qi)) / (CPD * np.maximum(_as(t), 253.0)))
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def wet_bulb_potential_temperature(t, td, p):
|
|
427
|
+
"""Wet-bulb potential temperature theta_w (K): the 1000 hPa temperature of the pseudo-adiabat through the parcel,
|
|
428
|
+
by Davies-Jones (2008, eq. 3.8) from the Bolton (1980, eq. 39) theta_E: with X = theta_E / 273.15,
|
|
429
|
+
theta_w = theta_E - exp(A(X) / B(X)), A, B quartics (coefficients below), theta_w = theta_E below 173.15 K.
|
|
430
|
+
Accurate to ~0.001 K against the inversion of Bolton's theta_E (tested)."""
|
|
431
|
+
te = equivalent_potential_temperature(t, td, p, "bolton")
|
|
432
|
+
x = te / 273.15
|
|
433
|
+
a = 7.101574 - 20.68208 * x + 16.11182 * x**2 + 2.574631 * x**3 - 5.205688 * x**4
|
|
434
|
+
b = 1.0 - 3.552497 * x + 3.781782 * x**2 - 0.6899655 * x**3 - 0.5929340 * x**4
|
|
435
|
+
return np.where(te <= 173.15, te, te - np.exp(a / b))
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
# --------------------------------------------------------------------------
|
|
439
|
+
# Wet bulb
|
|
440
|
+
# --------------------------------------------------------------------------
|
|
441
|
+
def stull_valid(t, rh):
|
|
442
|
+
"""Validity domain of Stull (2011): RH 5-99 % and T -20..50 degC (t in K). Outside it the fit's error grows
|
|
443
|
+
quickly (and it never knew about pressure: it is a fit at 1013.25 hPa)."""
|
|
444
|
+
tc = _as(t) - T0
|
|
445
|
+
rh = _as(rh)
|
|
446
|
+
return (rh >= 5.0) & (rh <= 99.0) & (tc >= -20.0) & (tc <= 50.0)
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def wet_bulb_stull(t, rh, strict: bool = True):
|
|
450
|
+
"""Wet-bulb temperature (K) by Stull (2011) from t (K) and RH (%):
|
|
451
|
+
Tw = T atan(0.151977 (RH + 8.313659)^1/2) + atan(T + RH) - atan(RH - 1.676331)
|
|
452
|
+
+ 0.00391838 RH^3/2 atan(0.023101 RH) - 4.686035 (T, Tw in degC)
|
|
453
|
+
Stull quotes -1 to +0.65 degC against his reference; against the isobaric psychrometric solution
|
|
454
|
+
(wet_bulb_temperature) at 1013.25 hPa it is -1.2 to +1.1 degC for T >= 0 degC (mean |error| 0.34 degC), and worse
|
|
455
|
+
in cold dry air. It is a fit AT SEA-LEVEL PRESSURE. strict: NaN outside RH 5-99 %, T -20..50 degC (stull_valid);
|
|
456
|
+
strict=False evaluates the fit everywhere. For any other pressure use wet_bulb_temperature."""
|
|
457
|
+
tc = _as(t) - T0
|
|
458
|
+
rh = _as(rh)
|
|
459
|
+
tw = (
|
|
460
|
+
tc * np.arctan(0.151977 * np.sqrt(rh + 8.313659))
|
|
461
|
+
+ np.arctan(tc + rh)
|
|
462
|
+
- np.arctan(rh - 1.676331)
|
|
463
|
+
+ 0.00391838 * rh**1.5 * np.arctan(0.023101 * rh)
|
|
464
|
+
- 4.686035
|
|
465
|
+
) + T0
|
|
466
|
+
return np.where(stull_valid(t, rh), tw, np.nan) if strict else tw
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def wet_bulb_temperature(t, p, td=None, rh=None, formula: str = "magnus", tol: float = 1e-6):
|
|
470
|
+
"""Isobaric (psychrometric) wet-bulb temperature (K) at pressure p (Pa), from t (K) and the dewpoint td (K) or RH
|
|
471
|
+
(%). Solves the enthalpy balance of adiabatic saturation at constant pressure,
|
|
472
|
+
(CPD + r CPV)(T - Tw) = (r_s(Tw, p) - r) L_v(Tw),
|
|
473
|
+
exact for ideal gases with constant heat capacities; bracketed between Td (where the right side is 0) and T, and
|
|
474
|
+
bisected to `tol` K. Vectorised."""
|
|
475
|
+
t = _as(t)
|
|
476
|
+
p = _as(p)
|
|
477
|
+
if td is None:
|
|
478
|
+
if rh is None:
|
|
479
|
+
raise ValueError("give td or rh")
|
|
480
|
+
td = dewpoint_from_rh(t, rh, formula)
|
|
481
|
+
td = np.minimum(_as(td), t)
|
|
482
|
+
r = mixing_ratio(vapour_pressure(td, formula), p)
|
|
483
|
+
lo, hi = np.broadcast_arrays(td, t)
|
|
484
|
+
lo = lo.copy()
|
|
485
|
+
hi = hi.copy()
|
|
486
|
+
cp = CPD + r * CPV
|
|
487
|
+
|
|
488
|
+
def f(tw):
|
|
489
|
+
return cp * (t - tw) - (saturation_mixing_ratio(tw, p, "liquid", formula) - r) * latent_heat(tw)
|
|
490
|
+
|
|
491
|
+
n = int(np.ceil(np.log2(max(float(np.nanmax(hi - lo, initial=0.0)), tol) / tol))) + 1
|
|
492
|
+
for _ in range(n):
|
|
493
|
+
mid = 0.5 * (lo + hi)
|
|
494
|
+
pos = f(mid) > 0
|
|
495
|
+
lo = np.where(pos, mid, lo)
|
|
496
|
+
hi = np.where(pos, hi, mid)
|
|
497
|
+
return 0.5 * (lo + hi)
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
# --------------------------------------------------------------------------
|
|
501
|
+
# Lifting condensation level
|
|
502
|
+
# --------------------------------------------------------------------------
|
|
503
|
+
# Romps (2017) constant set: his exact expression is exact for HIS saturation vapour pressure (Rankine-Kirchhoff
|
|
504
|
+
# with these constants), so they are kept together to reproduce the paper's values.
|
|
505
|
+
ROMPS2017 = dict(
|
|
506
|
+
ttrip=273.16,
|
|
507
|
+
ptrip=611.65,
|
|
508
|
+
e0v=2.3740e6,
|
|
509
|
+
e0s=0.3337e6,
|
|
510
|
+
g=9.81,
|
|
511
|
+
rgasa=287.04,
|
|
512
|
+
rgasv=461.0,
|
|
513
|
+
cva=719.0,
|
|
514
|
+
cvv=1418.0,
|
|
515
|
+
cvl=4119.0,
|
|
516
|
+
cvs=1861.0,
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def lcl_romps(t, p, rh, phase: str = "liquid"):
|
|
521
|
+
"""Exact lifting condensation level (Romps 2017) from t (K), p (Pa) and RH (%).
|
|
522
|
+
|
|
523
|
+
Returns (t_lcl K, p_lcl Pa, z_lcl m above the parcel). phase "liquid" gives the LCL; "ice" the lifting
|
|
524
|
+
deposition level (RH then over ice); "auto" ice below the triple point. Uses Romps's own constants (ROMPS2017):
|
|
525
|
+
exactness is relative to his saturation vapour pressure, which RH is taken against (differences from the
|
|
526
|
+
Magnus RH are ~0.1 %); the Lambert W branch -1 is evaluated by scipy.special.lambertw."""
|
|
527
|
+
from scipy.special import lambertw
|
|
528
|
+
|
|
529
|
+
c_ = ROMPS2017
|
|
530
|
+
t = _as(t)
|
|
531
|
+
p = _as(p)
|
|
532
|
+
rh = _as(rh) / 100.0
|
|
533
|
+
cpa = c_["cva"] + c_["rgasa"]
|
|
534
|
+
cpv = c_["cvv"] + c_["rgasv"]
|
|
535
|
+
|
|
536
|
+
def branch(cvx, e0):
|
|
537
|
+
pstar = (
|
|
538
|
+
c_["ptrip"]
|
|
539
|
+
* (t / c_["ttrip"]) ** ((cpv - cvx) / c_["rgasv"])
|
|
540
|
+
* np.exp((e0 - (c_["cvv"] - cvx) * c_["ttrip"]) / c_["rgasv"] * (1.0 / c_["ttrip"] - 1.0 / t))
|
|
541
|
+
)
|
|
542
|
+
pv = rh * pstar
|
|
543
|
+
qv = c_["rgasa"] * pv / (c_["rgasv"] * p + (c_["rgasa"] - c_["rgasv"]) * pv)
|
|
544
|
+
rgasm = (1.0 - qv) * c_["rgasa"] + qv * c_["rgasv"]
|
|
545
|
+
cpm = (1.0 - qv) * cpa + qv * cpv
|
|
546
|
+
a = cpm / rgasm + (cvx - cpv) / c_["rgasv"]
|
|
547
|
+
b = -(e0 - (c_["cvv"] - cvx) * c_["ttrip"]) / (c_["rgasv"] * t)
|
|
548
|
+
c = b / a
|
|
549
|
+
tl = t * c / np.real(lambertw(rh ** (1.0 / a) * c * np.exp(c), -1))
|
|
550
|
+
return tl, p * (tl / t) ** (cpm / rgasm), cpm * (t - tl) / c_["g"]
|
|
551
|
+
|
|
552
|
+
liq = branch(c_["cvl"], c_["e0v"])
|
|
553
|
+
if phase == "liquid":
|
|
554
|
+
return liq
|
|
555
|
+
ice = branch(c_["cvs"], c_["e0v"] + c_["e0s"])
|
|
556
|
+
if phase == "ice":
|
|
557
|
+
return ice
|
|
558
|
+
if phase == "auto":
|
|
559
|
+
cold = t < c_["ttrip"]
|
|
560
|
+
return tuple(np.where(cold, i, w) for i, w in zip(ice, liq))
|
|
561
|
+
raise ValueError("phase is 'liquid', 'ice' or 'auto'")
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
def lcl_bolton(t, td, p):
|
|
565
|
+
"""LCL temperature (Bolton 1980 eq. 15) and pressure (Poisson with Bolton's 0.2854 (1 - 0.28 r)) from t, td (K),
|
|
566
|
+
p (Pa): returns (t_lcl K, p_lcl Pa). Within ~0.1 K of the exact (Romps) T_LCL at ordinary humidities."""
|
|
567
|
+
t = _as(t)
|
|
568
|
+
td = _as(td)
|
|
569
|
+
p = _as(p)
|
|
570
|
+
tl = _t_lcl_bolton(t, td)
|
|
571
|
+
r = mixing_ratio(saturation_vapour_pressure(td, "liquid", "bolton"), p)
|
|
572
|
+
return tl, p * (tl / t) ** (1.0 / (0.2854 * (1.0 - 0.28 * r)))
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
# --------------------------------------------------------------------------
|
|
576
|
+
# Static energy, enthalpy, the pseudo-adiabat
|
|
577
|
+
# --------------------------------------------------------------------------
|
|
578
|
+
def dry_static_energy(t, z):
|
|
579
|
+
"""s = CPD T + G z (J/kg), z the geopotential height (m)."""
|
|
580
|
+
return CPD * _as(t) + G * _as(z)
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
def moist_static_energy(t, z, q):
|
|
584
|
+
"""h = CPD T + G z + LV0 q (J/kg), z the geopotential height (m), q specific humidity: the conventional form,
|
|
585
|
+
conserved to within a few tenths of a per cent in moist adiabatic processes."""
|
|
586
|
+
return CPD * _as(t) + G * _as(z) + LV0 * _as(q)
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
def enthalpy(t, q=0.0, ql=0.0, qi=0.0, t_ref: float = T0):
|
|
590
|
+
"""Specific enthalpy of moist air (J per kg of moist air), constant heat capacities.
|
|
591
|
+
|
|
592
|
+
REFERENCE STATE: dry air and liquid water have zero enthalpy at t_ref (default T0 = 273.15 K); vapour then carries
|
|
593
|
+
L_v(t_ref) and ice -L_f(t_ref) (Kirchhoff latent heats), so
|
|
594
|
+
h = q_d CPD (T - Tr) + q (CPV (T - Tr) + L_v(Tr)) + ql CL (T - Tr) + qi (CI (T - Tr) - L_f(Tr)),
|
|
595
|
+
q_d = 1 - q - ql - qi. Differences and fluxes do not depend on the reference when water is conserved; totals do
|
|
596
|
+
-- state it whenever a column total is shown. With q = ql = qi = 0 this is the dry enthalpy CPD (T - Tr)."""
|
|
597
|
+
t = _as(t)
|
|
598
|
+
q = _as(q)
|
|
599
|
+
ql = _as(ql)
|
|
600
|
+
qi = _as(qi)
|
|
601
|
+
dt = t - t_ref
|
|
602
|
+
qd = 1.0 - q - ql - qi
|
|
603
|
+
return (
|
|
604
|
+
qd * CPD * dt
|
|
605
|
+
+ q * (CPV * dt + latent_heat(t_ref))
|
|
606
|
+
+ ql * CL * dt
|
|
607
|
+
+ qi * (CI * dt - latent_heat(t_ref, "fusion"))
|
|
608
|
+
)
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
def _dt_dlnp_pseudo(t, p, formula):
|
|
612
|
+
"""dT/dln p along the pseudo-adiabat (liquid saturation), the AMS Glossary form with L_v(T) and p_d = p - e_s."""
|
|
613
|
+
es = saturation_vapour_pressure(t, "liquid", formula)
|
|
614
|
+
r = EPSILON * es / (p - es)
|
|
615
|
+
lv = latent_heat(t)
|
|
616
|
+
num = RD * t + lv * r
|
|
617
|
+
den = CPD + r * CPV + lv * lv * r * (EPSILON + r) / (RD * t * t)
|
|
618
|
+
return p / (p - es) * num / den
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
def moist_lapse_rate(t, p, formula: str = "magnus"):
|
|
622
|
+
"""Pseudo-adiabatic (saturated) lapse rate -dT/dz (K/m) at t (K), p (Pa) (AMS Glossary):
|
|
623
|
+
G (1 + r)(1 + L_v r/(RD T)) / (CPD + r CPV + L_v^2 r (eps + r)/(RD T^2)), r = r_s(T, p), L_v = L_v(T).
|
|
624
|
+
9.8 K/km for dry air (r -> 0: G/CPD), ~3.5 K/km in warm saturated boundary layers."""
|
|
625
|
+
t = _as(t)
|
|
626
|
+
p = _as(p)
|
|
627
|
+
r = saturation_mixing_ratio(t, p, "liquid", formula)
|
|
628
|
+
lv = latent_heat(t)
|
|
629
|
+
return G * (1.0 + r) * (1.0 + lv * r / (RD * t)) / (CPD + r * CPV + lv * lv * r * (EPSILON + r) / (RD * t * t))
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
def moist_adiabat(t_start, p_start, p, formula: str = "magnus", dlnp: float = 0.002):
|
|
633
|
+
"""Temperature (K) along the pseudo-adiabat through (t_start K, p_start Pa) at the pressures p (Pa, 1-D, any
|
|
634
|
+
order, either side of p_start). RK4 in ln p with steps of at most `dlnp`; t_start and p_start broadcast, giving
|
|
635
|
+
output (len(p),) + their broadcast shape."""
|
|
636
|
+
t0, p0 = np.broadcast_arrays(_as(t_start), _as(p_start))
|
|
637
|
+
p = np.atleast_1d(_as(p))
|
|
638
|
+
out = np.empty((p.size, *t0.shape))
|
|
639
|
+
tc = t0.astype(float).copy()
|
|
640
|
+
lnp = np.log(p0.astype(float))
|
|
641
|
+
# The pseudo-adiabat is a first-order ODE in ln p, so the levels can be visited in any order (each leg may run up
|
|
642
|
+
# or down, point by point); descending pressure keeps the legs short.
|
|
643
|
+
for k in np.argsort(-p):
|
|
644
|
+
target = np.log(p[k])
|
|
645
|
+
n = max(1, int(np.ceil(np.max(np.abs(target - lnp)) / dlnp)))
|
|
646
|
+
h = (target - lnp) / n
|
|
647
|
+
for _ in range(n):
|
|
648
|
+
pp = np.exp(lnp)
|
|
649
|
+
k1 = _dt_dlnp_pseudo(tc, pp, formula)
|
|
650
|
+
k2 = _dt_dlnp_pseudo(tc + 0.5 * h * k1, pp * np.exp(0.5 * h), formula)
|
|
651
|
+
k3 = _dt_dlnp_pseudo(tc + 0.5 * h * k2, pp * np.exp(0.5 * h), formula)
|
|
652
|
+
k4 = _dt_dlnp_pseudo(tc + h * k3, pp * np.exp(h), formula)
|
|
653
|
+
tc = tc + h * (k1 + 2 * k2 + 2 * k3 + k4) / 6.0
|
|
654
|
+
lnp = lnp + h
|
|
655
|
+
out[k] = tc
|
|
656
|
+
return out
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
# --------------------------------------------------------------------------
|
|
660
|
+
# Column integrals on pressure (or model) levels
|
|
661
|
+
# --------------------------------------------------------------------------
|
|
662
|
+
def _layer_dp(p, ps, top, shape):
|
|
663
|
+
"""dp (Pa) per level, level axis first, broadcastable to `shape`: edges midway between levels, the top edge at
|
|
664
|
+
`top`, the bottom at ps (clipped) -- or at the lowest level when ps is None."""
|
|
665
|
+
p = _as(p)
|
|
666
|
+
if p.ndim == 1:
|
|
667
|
+
p = p.reshape((-1,) + (1,) * (len(shape) - 1))
|
|
668
|
+
o = np.argsort(p, axis=0)
|
|
669
|
+
q = np.take_along_axis(p, o, 0)
|
|
670
|
+
mid = 0.5 * (q[1:] + q[:-1])
|
|
671
|
+
top_e = np.full_like(q[:1], float(top))
|
|
672
|
+
bot_e = q[-1:].copy() if ps is None else np.full_like(q[:1], np.inf)
|
|
673
|
+
edges = np.concatenate([top_e, mid, bot_e], 0)
|
|
674
|
+
if ps is not None:
|
|
675
|
+
edges = np.minimum(edges, _as(ps)[None])
|
|
676
|
+
dps = np.clip(np.diff(edges, axis=0), 0.0, None)
|
|
677
|
+
dp = np.empty(np.broadcast_shapes(dps.shape, o.shape))
|
|
678
|
+
np.put_along_axis(dp, np.broadcast_to(o, dp.shape), np.broadcast_to(dps, dp.shape), 0)
|
|
679
|
+
return dp
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
def column_integral(x, p, ps=None, axis: int = 0, top: float = 0.0):
|
|
683
|
+
"""Mass-weighted vertical integral sum_k x_k dp_k / G (x units times kg/m2).
|
|
684
|
+
|
|
685
|
+
x : (..., lev, ...) with the level axis `axis`
|
|
686
|
+
p : level pressures (Pa), 1-D (pressure levels) or x's shape (model levels; any order along the axis)
|
|
687
|
+
ps : surface pressure (Pa), x's shape without the level axis; None = the lowest level is the surface.
|
|
688
|
+
Edges are midway between levels, the top edge at `top` (Pa), the bottom at ps; levels below ground get zero weight
|
|
689
|
+
and whatever they hold (extrapolated values, NaN) is ignored. NaN above ground propagates."""
|
|
690
|
+
x0 = np.moveaxis(_as(x), axis, 0)
|
|
691
|
+
pp = _as(p)
|
|
692
|
+
if pp.ndim > 1:
|
|
693
|
+
pp = np.moveaxis(pp, axis, 0)
|
|
694
|
+
dp = np.broadcast_to(_layer_dp(pp, ps, top, x0.shape), x0.shape)
|
|
695
|
+
with np.errstate(invalid="ignore"):
|
|
696
|
+
return np.where(dp > 0, x0 * dp, 0.0).sum(0) / G
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
def precipitable_water(q, p, ps=None, axis: int = 0, top: float = 0.0):
|
|
700
|
+
"""Total column water vapour / precipitable water (kg/m2 = mm), the integral of q dp/G from `top` to p_s."""
|
|
701
|
+
return column_integral(q, p, ps, axis, top)
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
def column_water(p, ps=None, q=None, ql=None, qi=None, axis: int = 0, top: float = 0.0) -> dict:
|
|
705
|
+
"""Column vapour, liquid and ice (kg/m2) for whichever of q, ql, qi are given, plus their 'total'."""
|
|
706
|
+
out = {}
|
|
707
|
+
for name, v in (("vapour", q), ("liquid", ql), ("ice", qi)):
|
|
708
|
+
if v is not None:
|
|
709
|
+
out[name] = column_integral(v, p, ps, axis, top)
|
|
710
|
+
if not out:
|
|
711
|
+
raise ValueError("give at least one of q, ql, qi")
|
|
712
|
+
out["total"] = sum(out.values())
|
|
713
|
+
return out
|
|
714
|
+
|
|
715
|
+
|
|
716
|
+
def column_moist_static_energy(t, z, q, p, ps=None, axis: int = 0, top: float = 0.0):
|
|
717
|
+
"""Column-integrated moist static energy (J/m2), the integral of (CPD T + G z + LV0 q) dp/G."""
|
|
718
|
+
return column_integral(moist_static_energy(t, z, q), p, ps, axis, top)
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
def column_enthalpy(t, p, ps=None, q=0.0, ql=0.0, qi=0.0, axis: int = 0, top: float = 0.0, t_ref: float = T0):
|
|
722
|
+
"""Column-integrated moist enthalpy (J/m2) with the reference state of `enthalpy` (zero for dry air and liquid
|
|
723
|
+
water at t_ref)."""
|
|
724
|
+
return column_integral(enthalpy(t, q, ql, qi, t_ref), p, ps, axis, top)
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
def _lnp_weights(p, p_bottom, p_top):
|
|
728
|
+
"""Weights c_k with sum_k c_k f_k = integral of f d(ln p) from p_top to p_bottom, f linear in ln p between
|
|
729
|
+
levels. No extrapolation."""
|
|
730
|
+
lp = np.log(_as(p))
|
|
731
|
+
o = np.argsort(lp)
|
|
732
|
+
x = lp[o]
|
|
733
|
+
a, b = np.log(p_top), np.log(p_bottom)
|
|
734
|
+
if not (b > a):
|
|
735
|
+
raise ValueError("p_bottom must exceed p_top")
|
|
736
|
+
if a < x[0] - 1e-12 or b > x[-1] + 1e-12:
|
|
737
|
+
raise ValueError("the layer must lie within the levels given (no extrapolation)")
|
|
738
|
+
c = np.zeros(x.size)
|
|
739
|
+
for k in range(x.size - 1):
|
|
740
|
+
u, v = max(a, x[k]), min(b, x[k + 1])
|
|
741
|
+
if v <= u:
|
|
742
|
+
continue
|
|
743
|
+
h = x[k + 1] - x[k]
|
|
744
|
+
c[k] += ((x[k + 1] - u) ** 2 - (x[k + 1] - v) ** 2) / (2 * h)
|
|
745
|
+
c[k + 1] += ((v - x[k]) ** 2 - (u - x[k]) ** 2) / (2 * h)
|
|
746
|
+
out = np.empty_like(c)
|
|
747
|
+
out[o] = c
|
|
748
|
+
return out, b - a
|
|
749
|
+
|
|
750
|
+
|
|
751
|
+
def layer_mean_temperature(t, p, p_bottom: float, p_top: float, q=None, axis: int = 0):
|
|
752
|
+
"""ln p-weighted mean temperature of the layer p_top..p_bottom (Pa), the hypsometric mean: the integral of T d ln p
|
|
753
|
+
over ln(p_bottom/p_top), T linear in ln p between the 1-D levels p (Pa). Virtual temperature when q is given."""
|
|
754
|
+
tt = _as(t) if q is None else virtual_temperature(t, q)
|
|
755
|
+
c, span = _lnp_weights(p, p_bottom, p_top)
|
|
756
|
+
return np.tensordot(c, np.moveaxis(tt, axis, 0), axes=(0, 0)) / span
|
|
757
|
+
|
|
758
|
+
|
|
759
|
+
def thickness(t, p, p_bottom: float, p_top: float, q=None, axis: int = 0):
|
|
760
|
+
"""Hypsometric thickness (m) of the layer p_top..p_bottom (Pa): (RD/G) <T_v> ln(p_bottom/p_top), with <T_v> from
|
|
761
|
+
layer_mean_temperature (pass q for the virtual correction; without it, the dry thickness)."""
|
|
762
|
+
return RD / G * layer_mean_temperature(t, p, p_bottom, p_top, q, axis) * np.log(p_bottom / p_top)
|
|
763
|
+
|
|
764
|
+
|
|
765
|
+
# --------------------------------------------------------------------------
|
|
766
|
+
# Apparent temperature (NWS)
|
|
767
|
+
# --------------------------------------------------------------------------
|
|
768
|
+
def heat_index(t_f, rh):
|
|
769
|
+
"""NWS heat index (degF) from air temperature (degF) and RH (%), exactly as the NWS specifies:
|
|
770
|
+
1. the simple formula HI = 0.5 (T + 61 + 1.2 (T - 68) + 0.094 RH) (Steadman 1979);
|
|
771
|
+
2. if the average of that and T is >= 80 degF, the Rothfusz (1990) regression instead,
|
|
772
|
+
HI = -42.379 + 2.04901523 T + 10.14333127 RH - 0.22475541 T RH - 6.83783e-3 T^2 - 5.481717e-2 RH^2
|
|
773
|
+
+ 1.22874e-3 T^2 RH + 8.5282e-4 T RH^2 - 1.99e-6 T^2 RH^2,
|
|
774
|
+
minus ((13 - RH)/4) sqrt((17 - |T - 95|)/17) if RH < 13 % and 80 <= T <= 112 degF,
|
|
775
|
+
plus ((RH - 85)/10) ((87 - T)/5) if RH > 85 % and 80 <= T <= 87 degF."""
|
|
776
|
+
t = _as(t_f)
|
|
777
|
+
r = _as(rh)
|
|
778
|
+
simple = 0.5 * (t + 61.0 + (t - 68.0) * 1.2 + r * 0.094)
|
|
779
|
+
full = (
|
|
780
|
+
-42.379
|
|
781
|
+
+ 2.04901523 * t
|
|
782
|
+
+ 10.14333127 * r
|
|
783
|
+
- 0.22475541 * t * r
|
|
784
|
+
- 6.83783e-3 * t * t
|
|
785
|
+
- 5.481717e-2 * r * r
|
|
786
|
+
+ 1.22874e-3 * t * t * r
|
|
787
|
+
+ 8.5282e-4 * t * r * r
|
|
788
|
+
- 1.99e-6 * t * t * r * r
|
|
789
|
+
)
|
|
790
|
+
dry = (r < 13.0) & (t >= 80.0) & (t <= 112.0)
|
|
791
|
+
full = full - np.where(dry, (13.0 - r) / 4.0 * np.sqrt(np.clip(17.0 - np.abs(t - 95.0), 0.0, None) / 17.0), 0.0)
|
|
792
|
+
wet = (r > 85.0) & (t >= 80.0) & (t <= 87.0)
|
|
793
|
+
full = full + np.where(wet, (r - 85.0) / 10.0 * (87.0 - t) / 5.0, 0.0)
|
|
794
|
+
return np.where(0.5 * (simple + t) >= 80.0, full, simple)
|
|
795
|
+
|
|
796
|
+
|
|
797
|
+
def wind_chill(t_f, wind_mph):
|
|
798
|
+
"""NWS (2001) wind chill temperature (degF) from air temperature (degF) and 10 m wind (mph):
|
|
799
|
+
WC = 35.74 + 0.6215 T - 35.75 V^0.16 + 0.4275 T V^0.16 for T <= 50 degF and V >= 3 mph, else T
|
|
800
|
+
(Osczevski & Bluestein 2005; the wind is the 10 m value, the formula reduces it to face height itself)."""
|
|
801
|
+
t = _as(t_f)
|
|
802
|
+
v = _as(wind_mph)
|
|
803
|
+
v16 = np.power(np.maximum(v, 0.0), 0.16)
|
|
804
|
+
wc = 35.74 + 0.6215 * t - 35.75 * v16 + 0.4275 * t * v16
|
|
805
|
+
return np.where((t <= 50.0) & (v >= 3.0), wc, t)
|
|
806
|
+
|
|
807
|
+
|
|
808
|
+
# --------------------------------------------------------------------------
|
|
809
|
+
# Units
|
|
810
|
+
# --------------------------------------------------------------------------
|
|
811
|
+
_KT = 1852.0 / 3600.0 # m/s per knot (exact)
|
|
812
|
+
_MPH = 0.44704 # m/s per mph (exact)
|
|
813
|
+
|
|
814
|
+
|
|
815
|
+
def c_to_f(x):
|
|
816
|
+
return _as(x) * 9.0 / 5.0 + 32.0
|
|
817
|
+
|
|
818
|
+
|
|
819
|
+
def f_to_c(x):
|
|
820
|
+
return (_as(x) - 32.0) * 5.0 / 9.0
|
|
821
|
+
|
|
822
|
+
|
|
823
|
+
def c_to_k(x):
|
|
824
|
+
return _as(x) + T0
|
|
825
|
+
|
|
826
|
+
|
|
827
|
+
def k_to_c(x):
|
|
828
|
+
return _as(x) - T0
|
|
829
|
+
|
|
830
|
+
|
|
831
|
+
def f_to_k(x):
|
|
832
|
+
return f_to_c(x) + T0
|
|
833
|
+
|
|
834
|
+
|
|
835
|
+
def k_to_f(x):
|
|
836
|
+
return c_to_f(_as(x) - T0)
|
|
837
|
+
|
|
838
|
+
|
|
839
|
+
def ms_to_kt(x):
|
|
840
|
+
return _as(x) / _KT
|
|
841
|
+
|
|
842
|
+
|
|
843
|
+
def kt_to_ms(x):
|
|
844
|
+
return _as(x) * _KT
|
|
845
|
+
|
|
846
|
+
|
|
847
|
+
def ms_to_mph(x):
|
|
848
|
+
return _as(x) / _MPH
|
|
849
|
+
|
|
850
|
+
|
|
851
|
+
def mph_to_ms(x):
|
|
852
|
+
return _as(x) * _MPH
|
|
853
|
+
|
|
854
|
+
|
|
855
|
+
def kt_to_mph(x):
|
|
856
|
+
return _as(x) * _KT / _MPH
|
|
857
|
+
|
|
858
|
+
|
|
859
|
+
def mph_to_kt(x):
|
|
860
|
+
return _as(x) * _MPH / _KT
|