|
5 | 5 | # In applying this licence, ECMWF does not waive the privileges and immunities
|
6 | 6 | # granted to it by virtue of its status as an intergovernmental organisation
|
7 | 7 | # 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 |
0 commit comments