Skip to content

Commit a593136

Browse files
authored
WIP: Add computations along the vertical (#35)
* add calculation for model level pressure, geopotential thickness and pressure on height level
1 parent 4c58f71 commit a593136

File tree

4 files changed

+216
-2
lines changed

4 files changed

+216
-2
lines changed

src/earthkit/meteo/thermo/array/thermo.py

+19
Original file line numberDiff line numberDiff line change
@@ -1729,3 +1729,22 @@ def wet_bulb_potential_temperature_from_specific_humidity(t, q, p, ept_method="i
17291729
return _EptComp.make(ept_method).compute_wbpt(ept)
17301730
else:
17311731
return temperature_on_moist_adiabat(ept, constants.p0, ept_method=ept_method, t_method=t_method)
1732+
1733+
def specific_gas_constant(q):
1734+
"""Computes the specific gas constant of moist air
1735+
(specific content of cloud particles and hydrometeors are neglected).
1736+
1737+
Parameters
1738+
----------
1739+
q : number or ndarray
1740+
Specific humidity (kg/kg)
1741+
1742+
Returns
1743+
-------
1744+
number or ndarray
1745+
specific gas constant of moist air
1746+
"""
1747+
1748+
R = constants.Rd + (constants.Rv - constants.Rd) * q
1749+
return R
1750+

src/earthkit/meteo/thermo/thermo.py

+3
Original file line numberDiff line numberDiff line change
@@ -160,3 +160,6 @@ def wet_bulb_potential_temperature_from_dewpoint(*args, **kwargs):
160160

161161
def wet_bulb_potential_temperature_from_specific_humidity(*args, **kwargs):
162162
return array.wet_bulb_potential_temperature_from_specific_humidity(*args, **kwargs)
163+
164+
def specific_gas_constant(*arg, **kwargs):
165+
return array.specific_gas_constant(*args, **kwargs)

src/earthkit/meteo/vertical/array/vertical.py

+184-1
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,187 @@
55
# In applying this licence, ECMWF does not waive the privileges and immunities
66
# granted to it by virtue of its status as an intergovernmental organisation
77
# nor does it submit to any jurisdiction.
8-
#
8+
9+
from typing import Any
10+
from typing import Dict
11+
from typing import List
12+
from typing import Optional
13+
from typing import Tuple
14+
from typing import Union
15+
16+
import numpy as np
17+
from numpy.typing import NDArray
18+
19+
from earthkit.meteo import constants
20+
from earthkit.meteo.thermo import specific_gas_consant
21+
22+
def pressure_at_model_levels(
23+
A: NDArray[Any], B: NDArray[Any], sp: Union[float, NDArray[Any]]
24+
) -> Tuple[NDArray[Any], NDArray[Any], NDArray[Any], NDArray[Any]]:
25+
r"""Computes:
26+
- pressure at the model full- and half-levels
27+
- delta: depth of log(pressure) at full levels
28+
- alpha: alpha term #TODO: more descriptive information.
29+
30+
Parameters
31+
----------
32+
A : ndarray
33+
A-coefficients defining the model levels
34+
B : ndarray
35+
B-coefficients defining the model levels
36+
sp : number or ndarray
37+
surface pressure (Pa)
38+
39+
Returns
40+
-------
41+
ndarray
42+
pressure at model full-levels
43+
ndarray
44+
pressure at model half-levels
45+
ndarray
46+
delta at full-levels
47+
ndarray
48+
alpha at full levels
49+
50+
The pressure on the model-levels is calculated based on:
51+
52+
.. math::
53+
54+
p_{k+1/2} = A_{k+1/2} + p_{s} B_{k+1/2}
55+
p_k = 0.5 (p_{k-1/2} + p_{k+1/2})
56+
"""
57+
58+
# constants
59+
PRESSURE_TOA = 0.1 # safety when highest pressure level = 0.0
60+
61+
# make the calculation agnostic to the number of dimensions
62+
ndim = sp.ndim
63+
new_shape_half = (A.shape[0],) + (1,) * ndim
64+
A_reshaped = A.reshape(new_shape_half)
65+
B_reshaped = B.reshape(new_shape_half)
66+
67+
# calculate pressure on model half-levels
68+
p_half_level = A_reshaped + B_reshaped * sp[np.newaxis, ...]
69+
70+
# calculate delta
71+
new_shape_full = (A.shape[0] - 1,) + sp.shape
72+
delta = np.zeros(new_shape_full)
73+
delta[1:, ...] = np.log(p_half_level[2:, ...] / p_half_level[1:-1, ...])
74+
75+
# pressure at highest half level<= 0.1
76+
if np.any(p_half_level[0, ...] <= PRESSURE_TOA):
77+
delta[0, ...] = np.log(p_half_level[1, ...] / PRESSURE_TOA)
78+
# pressure at highest half level > 0.1
79+
else:
80+
delta[0, ...] = np.log(p_half_level[1, ...] / p_half_level[0, ...])
81+
82+
# calculate alpha
83+
alpha = np.zeros(new_shape_full)
84+
85+
alpha[1:, ...] = 1.0 - p_half_level[1:-1, ...] / (p_half_level[2:, ...] - p_half_level[1:-1, ...]) * delta[1:, ...]
86+
87+
# pressure at highest half level <= 0.1
88+
if np.any(p_half_level[0, ...] <= PRESSURE_TOA):
89+
alpha[0, ...] = 1.0 # ARPEGE choice, ECMWF IFS uses log(2)
90+
# pressure at highest half level > 0.1
91+
else:
92+
alpha[0, ...] = 1.0 - p_half_level[0, ...] / (p_half_level[1, ...] - p_half_level[0, ...]) * delta[0, ...]
93+
94+
# calculate pressure on model full levels
95+
# TODO: is there a faster way to calculate the averages?
96+
# TODO: introduce option to calculate full levels in more complicated way
97+
p_full_level = np.apply_along_axis(lambda m: np.convolve(m, np.ones(2) / 2, mode="valid"), axis=0, arr=p_half_level)
98+
99+
return p_full_level, p_half_level, delta, alpha
100+
101+
def relative_geopotential_thickness(alpha: NDArray[Any], q: NDArray[Any], T: NDArray[Any]) -> NDArray[Any]:
102+
"""Calculates the geopotential thickness w.r.t the surface on model full-levels.
103+
104+
Parameters
105+
----------
106+
alpha : ndarray
107+
alpha term of pressure calculations
108+
q : ndarray
109+
specific humidity (in kg/kg) on model full-levels
110+
T : ndarray
111+
temperature (in Kelvin) on model full-levels
112+
113+
Returns
114+
-------
115+
ndarray
116+
geopotential thickness of model full-levels w.r.t. the surface
117+
"""
118+
119+
R = calc_specific_gas_constant(q)
120+
dphi = np.cumsum(np.flip(alpha * R * T, axis=0), axis=0)
121+
dphi = np.flip(dphi, axis=0)
122+
123+
return dphi
124+
125+
def pressure_at_height_level(
126+
height: float, q: NDArray[Any], T: NDArray[Any], sp: NDArray[Any], A: NDArray[Any], B: NDArray[Any]
127+
) -> Union[float, NDArray[Any]]:
128+
"""Calculates the pressure at a height level given in meters above surface.
129+
This is done by finding the model level above and below the specified height
130+
and interpolating the pressure.
131+
132+
Parameters
133+
----------
134+
height : number
135+
height (in meters) above the surface for which the pressure needs to be computed
136+
q : ndarray
137+
specific humidity (kg/kg) at model full-levels
138+
T : ndarray
139+
temperature (K) at model full-levels
140+
sp : ndarray
141+
surface pressure (Pa)
142+
A : ndarray
143+
A-coefficients defining the model levels
144+
B : ndarray
145+
B-coefficients defining the model levels
146+
147+
Returns
148+
-------
149+
number or ndarray
150+
pressure (Pa) at the given height level
151+
"""
152+
153+
# geopotential thickness of the height level
154+
tdphi = height * constants.g
155+
156+
# pressure(-related) variables
157+
p_full, p_half, _, alpha = model_level_pressure(A, B, sp)
158+
159+
# relative geopot. thickness of full levels
160+
dphi = relative_geopotential_thickness(alpha, q, T)
161+
162+
# find the model full level right above the height level
163+
i_phi = (tdphi > dphi).sum(0)
164+
165+
# initialize the output array
166+
p_height = np.zeros_like(i_phi, dtype=np.float64)
167+
168+
# define mask: requested height is below the lowest model full-level
169+
mask = i_phi == 0
170+
171+
# CASE 1: requested height is below the lowest model full-level
172+
# --> interpolation between surface pressure and lowest model full-level
173+
p_height[mask] = (p_half[-1, ...] + tdphi / dphi[-1, ...] * (p_full[-1, ...] - p_half[-1, ...]))[mask]
174+
175+
# CASE 2: requested height is above the lowest model full-level
176+
# --> interpolation between between model full-level above and below
177+
178+
# define some indices for masking and readability
179+
i_lev = alpha.shape[0] - i_phi - 1 # convert phi index to model level index
180+
indices = np.indices(i_lev.shape)
181+
masked_indices = tuple(dim[~mask] for dim in indices)
182+
above = (i_lev[~mask],) + masked_indices
183+
below = (i_lev[~mask] + 1,) + masked_indices
184+
185+
dphi_above = dphi[above]
186+
dphi_below = dphi[below]
187+
188+
factor = (tdphi - dphi_above) / (dphi_below - dphi_above)
189+
p_height[~mask] = p_full[above] + factor * (p_full[below] - p_full[above])
190+
191+
return p_height

src/earthkit/meteo/vertical/vertical.py

+10-1
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,13 @@
77
# nor does it submit to any jurisdiction.
88
#
99

10-
# from . import array
10+
from . import array
11+
12+
def pressure_at_model_levels(*args, **kwargs):
13+
return array.pressure_at_model_levels(*args, **kwargs)
14+
15+
def relative_geopotential_thickness(*arg, **kwargs):
16+
return array.relative_geopotential_thickness(*arg, **kwargs)
17+
18+
def pressure_at_height_level(*args, **kwargs):
19+
return array.pressure_at_height_level(*args, **kwargs)

0 commit comments

Comments
 (0)