Coverage for src / CSET / operators / temperature.py: 100%

121 statements  

« prev     ^ index     » next       coverage.py v7.13.2, created at 2026-01-28 11:16 +0000

1# © Crown copyright, Met Office (2022-2026) and CSET contributors. 

2# 

3# Licensed under the Apache License, Version 2.0 (the "License"); 

4# you may not use this file except in compliance with the License. 

5# You may obtain a copy of the License at 

6# 

7# http://www.apache.org/licenses/LICENSE-2.0 

8# 

9# Unless required by applicable law or agreed to in writing, software 

10# distributed under the License is distributed on an "AS IS" BASIS, 

11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 

12# See the License for the specific language governing permissions and 

13# limitations under the License. 

14 

15"""Operators for temperature conversions.""" 

16 

17import iris.cube 

18import numpy as np 

19 

20from CSET._common import iter_maybe 

21from CSET.operators._atmospheric_constants import CPD, EPSILON, LV, RV, T0 

22from CSET.operators.humidity import ( 

23 mixing_ratio_from_relative_humidity, 

24 saturation_mixing_ratio, 

25) 

26from CSET.operators.misc import convert_units 

27from CSET.operators.pressure import ( 

28 exner_pressure, 

29 vapour_pressure_from_relative_humidity, 

30) 

31 

32 

33def dewpoint_temperature( 

34 temperature: iris.cube.Cube | iris.cube.CubeList, 

35 relative_humidity: iris.cube.Cube | iris.cube.CubeList, 

36) -> iris.cube.Cube | iris.cube.CubeList: 

37 r"""Calculate the dewpoint temperature. 

38 

39 Arguments 

40 --------- 

41 temperature: iris.cube.Cube | iris.cube.CubeList 

42 Cubes of temperature in Kelvin. 

43 relative_humidity: iris.cube.Cube | iris.cube.CubeList 

44 Cubes of relative humidity. 

45 

46 Returns 

47 ------- 

48 iris.cube.Cube | iris.cueb.CubeList 

49 Calculated dewpoint temperature in Kelvin. 

50 

51 Notes 

52 ----- 

53 The dewpoint temperature provides the temperature at which the air must be 

54 cooled for dew to form (i.e. the water vapour condenses to liquid water). 

55 It is calculated based upon [Bolton80]_ 

56 

57 .. math:: T_d = \frac{243.5 * ln(e) - 440.8}{19.48 - ln(e)} 

58 

59 for :math:`T_d` the dewpoint temperature and e the vapour pressure. Here the 

60 vapour pressure is calculated from relative humidity using 

61 `pressure.vapour_pressure_from_relative_humidity`. 

62 

63 The dewpoint temperature is presented when -35.0 C < T < 35.0 C as this is 

64 when the calculation is the most accurate [Bolton80]_ this roughly equates to 

65 exclusion of dewpoints on pressures of 400 hPa and above (e.g. [Flack24]_). 

66 

67 When :math:`T_d` is equivalent to T the relative humidity will be 100 %. 

68 

69 All cubes must be on the same grid. 

70 

71 References 

72 ---------- 

73 .. [Bolton80] Bolton. D. (1980) "The computation of equivalent potential 

74 temperature". Monthly Weather Review, vol. 108, 1046-1053, 

75 doi: 10.1175/1520-0493(1980)108<1046:TCOEPT>2.0.CO;2 

76 .. [Flack24] Flack, D.L.A. (2024) "Stratification of the vertical spread-skill 

77 relation by radiosonde drift in a convective-scale ensemble." 

78 Atmospheric Science Letters, vol. 25, e1194, doi: 10.1002/asl.1194 

79 

80 Examples 

81 -------- 

82 >>> Td = temperature.dewpoint_temperature(T, RH) 

83 """ 

84 Td = iris.cube.CubeList([]) 

85 for T, RH in zip( 

86 iter_maybe(temperature), iter_maybe(relative_humidity), strict=True 

87 ): 

88 vp = vapour_pressure_from_relative_humidity(T, RH) 

89 td = vp.copy() 

90 td.data = ((243.5 * np.log(vp.core_data())) - 440.8) / ( 

91 19.48 - np.log(vp.core_data()) 

92 ) 

93 td.data[T.data - T0 < -35.0] = np.nan 

94 td.data[T.data - T0 > 35.0] = np.nan 

95 td.units = "Celsius" 

96 td.rename("dewpoint_temperature") 

97 td = convert_units(td, "K") 

98 Td.append(td) 

99 if len(Td) == 1: 

100 return Td[0] 

101 else: 

102 return Td 

103 

104 

105def virtual_temperature( 

106 temperature: iris.cube.Cube | iris.cube.CubeList, 

107 mixing_ratio: iris.cube.Cube | iris.cube.CubeList, 

108) -> iris.cube.Cube | iris.cube.CubeList: 

109 r"""Calculate the virtual temperature. 

110 

111 Arguments 

112 --------- 

113 temperature: iris.cube.Cube | iris.cube.CubeList 

114 Cubes of temperature in Kelvin. 

115 mixing_ratio: iris.cube.Cube | iris.cube.CubeList 

116 Cubes of mixing ratio. 

117 

118 Returns 

119 ------- 

120 iris.cube.Cube | iris.cube.CubeList 

121 Calculated virtual temperature in Kelvin. 

122 

123 Notes 

124 ----- 

125 The virtual temperature is that required for a dry parcel to have the 

126 same density as a moist parcel at the same pressure. It is calculated 

127 as 

128 

129 .. math:: T_v = T * \frac{w + \epsilon}{\epsilon (1 + w)} 

130 

131 for :math:`T_v` the virtual temperature, T the temperature, w the mixing 

132 ratio, and :math:`\epsilon` the ratio between dry and moist air equating 

133 to 0.622. 

134 

135 T is given in K and w is given in kg kg-1. 

136 All cubes must be on the same grid. 

137 

138 Examples 

139 -------- 

140 >>> Tv = temperature.virtual_temperature(T, w) 

141 """ 

142 Tv = iris.cube.CubeList([]) 

143 for T, W in zip(iter_maybe(temperature), iter_maybe(mixing_ratio), strict=True): 

144 virT = T * ((W + EPSILON) / (EPSILON * (1 + W))) 

145 virT.rename("virtual_temperature") 

146 Tv.append(virT) 

147 if len(Tv) == 1: 

148 return Tv[0] 

149 else: 

150 return Tv 

151 

152 

153def wet_bulb_temperature( 

154 temperature: iris.cube.Cube | iris.cube.CubeList, 

155 relative_humidity: iris.cube.Cube | iris.cube.CubeList, 

156) -> iris.cube.Cube | iris.cube.CubeList: 

157 r"""Calculate the wet-bulb temperature. 

158 

159 Arguments 

160 --------- 

161 temperature: iris.cube.Cube | iris.cube.CubeList 

162 Cubes of temperature. 

163 relative_humidity: iris.cube.Cube | iris.cube.CubeList 

164 Cubes of relative humidity. 

165 

166 Returns 

167 ------- 

168 iris.cube.Cube | iris.cube.CubeList 

169 Calculated wet-bulb temperature in Kelvin. 

170 

171 Notes 

172 ----- 

173 The wet-bulb temperature is the temperature the air reaches when all the 

174 water has been evaporated out of it. It can be calculated from temperature in Celsius 

175 and relative humidity in percent following [Stull11]_ 

176 

177 .. math:: T_w = T * arctan\left(0.151977*(RH + 8.313659)^{0.5}\right) + arctan(T + RH) - arctan(RH - 1.676331) + 0.00391838*RH^{\frac{3}{2}}*arctan(0.023101*RH) - 4.686035 

178 

179 for :math:`T_w` the wet-bulb temperature, T the temperature, and RH the relative 

180 humidity. 

181 

182 The equation is valid for 5% < RH < 99% and -20 C < T < 50 C, and results are 

183 only presented for these values. 

184 

185 The temperature and relative humidity unit conversions are applied in the 

186 operator. 

187 

188 All cubes should be on the same grid. 

189 

190 References 

191 ---------- 

192 .. [Stull11] Stull, R. (2011) "Wet-Bulb Temperature from Relative Humidity 

193 and air temperature." Journal of Applied Meteorology and Climatology, vol. 50, 

194 2267-2269, doi: 10.1175/JAMC-D-11-0143.1 

195 

196 Examples 

197 -------- 

198 >>> Tw = temperature.wet_bulb_temperature(T, RH) 

199 """ 

200 Tw = iris.cube.CubeList([]) 

201 for T, RH in zip( 

202 iter_maybe(temperature), iter_maybe(relative_humidity), strict=True 

203 ): 

204 RH = convert_units(RH, "%") 

205 T = convert_units(T, "Celsius") 

206 wetT = ( 

207 T * np.arctan(0.151977 * (RH.core_data() + 8.313659) ** 0.5) 

208 + np.arctan(T.core_data() + RH.core_data()) 

209 - np.arctan(RH.core_data() - 1.676331) 

210 + 0.00391838 

211 * (RH.core_data()) ** (3.0 / 2.0) 

212 * np.arctan(0.023101 * RH.core_data()) 

213 - 4.686035 

214 ) 

215 wetT.rename("wet_bulb_temperature") 

216 wetT = convert_units(wetT, "K") 

217 wetT.data[T.core_data() < -20.0] = np.nan 

218 wetT.data[T.core_data() > 50.0] = np.nan 

219 wetT.data[RH.core_data() < 5.0] = np.nan 

220 wetT.data[RH.core_data() > 99.0] = np.nan 

221 Tw.append(wetT) 

222 if len(Tw) == 1: 

223 return Tw[0] 

224 else: 

225 return Tw 

226 

227 

228def potential_temperature( 

229 temperature: iris.cube.Cube | iris.cube.CubeList, 

230 pressure: iris.cube.Cube | iris.cube.CubeList, 

231) -> iris.cube.Cube | iris.cube.CubeList: 

232 r"""Calculate the potential temperature. 

233 

234 Arguments 

235 --------- 

236 temperature: iris.cube.Cube | iris.cube.CubeList 

237 Cubes of temperature. 

238 pressure: iris.cube.Cube | iris.cube.CuebList 

239 Cubes of pressure. 

240 

241 Returns 

242 ------- 

243 iris.cube.Cube | iris.cube.CubeList 

244 Calculated potential temperature in Kelvin. 

245 

246 Notes 

247 ----- 

248 The potential temperature is the temperature an air parcel would reach 

249 if it was moved adiabatically to a reference pressure. Here we use 1000 hPa 

250 as the reference pressure. It is calculated from 

251 

252 .. math:: \theta = \frac{T}{\Pi} 

253 

254 for :math:`\theta` the potential temperature, T the temperature, and :math:`\Pi` 

255 the exner pressure. The exner pressure is calculated using `pressure.exner_pressure`. 

256 

257 Temperature must be in Kelvin. 

258 

259 All cubes must be on the same grid. 

260 

261 Examples 

262 -------- 

263 >>> Theta = temperature.potential_temperature(T, P) 

264 """ 

265 theta = iris.cube.CubeList([]) 

266 for T, P in zip(iter_maybe(temperature), iter_maybe(pressure), strict=True): 

267 TH = T / exner_pressure(P) 

268 TH.rename("potential_temperature") 

269 theta.append(TH) 

270 if len(theta) == 1: 

271 return theta[0] 

272 else: 

273 return theta 

274 

275 

276def virtual_potential_temperature( 

277 temperature: iris.cube.Cube | iris.cube.CubeList, 

278 mixing_ratio: iris.cube.Cube | iris.cube.CubeList, 

279 pressure: iris.cube.Cube | iris.cube.CubeList, 

280) -> iris.cube.Cube | iris.cube.CubeList: 

281 r"""Calculate the virtual potential temperature. 

282 

283 Arguments 

284 --------- 

285 temperature: iris.cube.Cube | iris.cube.CubeList 

286 Cubes of temperature. 

287 mixing_ratio: iris.cube.Cube | iris.cube.CubeList 

288 Cubes of mixing ratio. 

289 pressure: iris.cube.Cube | iris.cube.CubeList 

290 Cubes of pressure. 

291 

292 Returns 

293 ------- 

294 iris.cube.Cube | iris.cube.CubeList 

295 Calculated virtual potential temperature in Kelvin. 

296 

297 Notes 

298 ----- 

299 The virtual potential temperature is mechanistically equivalent to the 

300 potential temperature, except rather than using the (dry-bulb) temperature 

301 the virtual temperature used. The virtual potential temperature is the potential 

302 temperature a parcel would have if its temperature was replaced by its virtual temperature 

303 and then the parcel is brought adiabatically to 1000 hPa. It is calculated as 

304 

305 .. math:: \theta_v = \frac{T_v}{\Pi} 

306 

307 for :math:`\theta_v` the virtual potential temperature, :math:`T_v` the 

308 virtual temperature, and :math:`\Pi` the exner pressure. The exner pressure 

309 is calculated using `pressure.exner_pressure`. 

310 

311 All cubes must be on the same grid. 

312 

313 Examples 

314 -------- 

315 >>> Theta_v = temperature.virtual_potential_temperature(T, W, P) 

316 """ 

317 theta_v = iris.cube.CubeList([]) 

318 for T, W, P in zip( 

319 iter_maybe(temperature), 

320 iter_maybe(mixing_ratio), 

321 iter_maybe(pressure), 

322 strict=True, 

323 ): 

324 TH_V = virtual_temperature(T, W) / exner_pressure(P) 

325 TH_V.rename("virtual_potential_temperature") 

326 theta_v.append(TH_V) 

327 if len(theta_v) == 1: 

328 return theta_v[0] 

329 else: 

330 return theta_v 

331 

332 

333def equivalent_potential_temperature( 

334 temperature: iris.cube.Cube | iris.cube.CubeList, 

335 relative_humidity: iris.cube.Cube | iris.cube.CubeList, 

336 pressure: iris.cube.Cube | iris.cube.CubeList, 

337) -> iris.cube.Cube | iris.cube.CubeList: 

338 r"""Calculate the equivalent potential temperature. 

339 

340 Arguments 

341 --------- 

342 temperature: iris.cube.Cube | iris.cube.CubeList 

343 Cubes of temperature. 

344 relative_humidity: iris.cube.Cube | iris.cube.CubeList 

345 Cubes of relative humidity. 

346 pressure: iris.cube.Cube | iris.cube.CubeList 

347 Cubes of pressure. 

348 

349 Returns 

350 ------- 

351 iris.cube.Cube | iris.cube.CubeList 

352 Calculated equivalent potential temperature in Kelvin. 

353 

354 Notes 

355 ----- 

356 The equivalent potential temperature is the temperature an air parcel 

357 would have if it was raised until all of the water vapour in it was 

358 condensed out and then brought adiabatically to the reference pressure, 

359 here taken as 1000 hPa. It is calculated as 

360 

361 .. math:: \theta_e = \theta * RH^{- \frac{w R_v}{c_p}} * exp\left(\frac{L_v w}{c_p T} \right) 

362 

363 for :math:`\theta_e` the equivalent potential temperature, :math:`\theta` the 

364 potential temperature, RH the relative humidity, w the mixing ratio, 

365 :math:`R_v` the specific gas constant of water vapour (461 

366 :math:`J kg^{-1} K^{-1}`), :math:`c_p` the specific heat capacity of dry air 

367 (1005.7 :math:`J kg^{-1} K^{-1}`), :math:`L_v` the latent heat of vapourization 

368 (2.5 x :math:`10^6 J kg^{-1}`), and T the temperature. 

369 

370 Potential temperature and temperature in K. 

371 Relative humidity in percentage and will be converted to fraction. 

372 Mixing ratio in kg kg-1 (dimensionless). 

373 

374 In this operator the mixing ratio is calculated from 

375 `humidity.mixing_ratio_from_relative_humidity` and the potential temperature 

376 is calculated from `temperature.potential_temperature`. 

377 

378 This equation is a simplification of [Paluch79]_ following [Emanuel94]_, 

379 whilst still holding reasonable accuracy. 

380 

381 All cubes must be on the same grid. 

382 

383 References 

384 ---------- 

385 .. [Emanuel94] Emanuel, K.A. (1994) "Atmospheric Convection" Oxford University 

386 Press, 580 pp. 

387 .. [Paluch79] Paluch, I.R., (1979) "The entrainment mechanism in Colorado 

388 Cumuli" Journal of the Atmospheric Sciences, vol. 36, 2467-2478, 

389 doi: 10.1175/1520-0469(1979)036<2467:TEMICC>2.0.CO;2 

390 

391 Examples 

392 -------- 

393 >>> Theta_e = temperature.equivalent_potential_temperature(T, RH, P) 

394 """ 

395 theta_e = iris.cube.CubeList([]) 

396 for T, RH, P in zip( 

397 iter_maybe(temperature), 

398 iter_maybe(relative_humidity), 

399 iter_maybe(pressure), 

400 strict=True, 

401 ): 

402 RH = convert_units(RH, "1") 

403 theta = potential_temperature(T, P) 

404 w = mixing_ratio_from_relative_humidity(T, P, RH) 

405 second_term_power = -(w * RV) / CPD 

406 second_term = RH.core_data() ** second_term_power.core_data() 

407 third_term_power = LV * w / (CPD * T) 

408 third_term = np.exp(third_term_power.core_data()) 

409 TH_E = theta * second_term * third_term 

410 TH_E.rename("equivalent_potential_temperature") 

411 TH_E.units = "K" 

412 theta_e.append(TH_E) 

413 if len(theta_e) == 1: 

414 return theta_e[0] 

415 else: 

416 return theta_e 

417 

418 

419def wet_bulb_potential_temperature( 

420 temperature: iris.cube.Cube | iris.cube.CubeList, 

421 relative_humidity: iris.cube.Cube | iris.cube.CubeList, 

422 pressure: iris.cube.Cube | iris.cube.CubeList, 

423) -> iris.cube.Cube | iris.cube.CubeList: 

424 r"""Calculate wet-bulb potential temperature. 

425 

426 Arguments 

427 --------- 

428 temperature: iris.cube.Cube | iris.cube.CubeList 

429 Cubes of temperature. 

430 relative_humidity: iris.cube.Cube | iris.cube.CubeList 

431 Cubes of relative humidity. 

432 pressure: iris.cube.Cube | iris.cube.CubeList 

433 Cubes of pressure. 

434 

435 Returns 

436 ------- 

437 iris.cube.Cube | iris.cube.CubeList 

438 Calculated wet-bulb potential temperature in Kelvin. 

439 

440 Notes 

441 ----- 

442 The wet-bulb potential temperature represents the temperature an air parcel 

443 would have if cooled adiabatically until saturation and then taken down to 

444 a reference pressure (1000 hPa) whilst conserving the moisture (i.e. along a 

445 psuedoadiabat). It can be calculated either through a series of iterations, 

446 or through empirical relations. Here, we use the [DaviesJones08]_ formulation: 

447 

448 .. math:: \theta_w = \theta_e - exp\left(\frac{a_0 + a_1 X + a_2 X^2 + a_3 X^3 + a_4 X^4}{1.0 + b_1 X + b_2 X^2 + b_3 X^3 + b_4 X^4} \right) 

449 

450 for :math:`\theta_w` the wet-bulb potential temperature, :math:`\theta_e` the 

451 equivalent potential temperature, X = :math:`\theta_e` / 273.15 K, and a 

452 series of constants :math:`a_0` = 7.101574, :math:`a_1` = -20.68208, 

453 :math:`a_2` = 16.11182, :math:`a_3` = 2.574631, :math:`a_4` = -5.205688, 

454 :math:`b_1` = -3.552497, :math:`b_2` = 3.781782, :math:`b_3` = -0.6899655, and 

455 :math:`b_4` = -0.5929340. 

456 

457 The wet-bulb potential temperature calculated for this method is valid for 

458 the range -30 C < :math:`\theta_w` < 50 C and is thus set to NaN outside of 

459 this range. 

460 

461 All cubes must be on the same grid. 

462 

463 References 

464 ---------- 

465 .. [DaviesJones08] Davies-Jones, R. (2008) "An Efficient and Accurate 

466 Method for Computing the Wet-Bulb Temperature along Pseudoadiabats" 

467 Monthly Weather Review, vol. 136, 2764-2785, doi: 10.1175/2007MWR2224.1 

468 

469 Examples 

470 -------- 

471 >>> Theta_w = temperature.wet_bulb_potential_temperature(T, RH, P) 

472 """ 

473 theta_w = iris.cube.CubeList([]) 

474 for T, RH, P in zip( 

475 iter_maybe(temperature), 

476 iter_maybe(relative_humidity), 

477 iter_maybe(pressure), 

478 strict=True, 

479 ): 

480 TH_E = equivalent_potential_temperature(T, RH, P) 

481 X = TH_E / T0 

482 X.units = "1" 

483 A0 = 7.101574 

484 A1 = -20.68208 

485 A2 = 16.11182 

486 A3 = 2.574631 

487 A4 = -5.205688 

488 B1 = -3.552497 

489 B2 = 3.781782 

490 B3 = -0.6899655 

491 B4 = -0.5929340 

492 exponent = (A0 + A1 * X + A2 * X**2 + A3 * X**3 + A4 * X**4) / ( 

493 1.0 + B1 * X + B2 * X**2 + B3 * X**3 + B4 * X**4 

494 ) 

495 TH_W = TH_E.copy() 

496 TH_W.data[:] = TH_E.core_data() - np.exp(exponent.core_data()) 

497 TH_W.rename("wet_bulb_potential_temperature") 

498 TH_W.data[TH_W.data - T0 < -30.0] = np.nan 

499 TH_W.data[TH_W.data - T0 > 50.0] = np.nan 

500 theta_w.append(TH_W) 

501 if len(theta_w) == 1: 

502 return theta_w[0] 

503 else: 

504 return theta_w 

505 

506 

507def saturation_equivalent_potential_temperature( 

508 temperature: iris.cube.Cube | iris.cube.CubeList, 

509 pressure: iris.cube.Cube | iris.cube.CubeList, 

510) -> iris.cube.Cube | iris.cube.CubeList: 

511 r"""Calculate the saturation equivalent potential temperature. 

512 

513 Arguments 

514 --------- 

515 temperature: iris.cube.Cube | iris.cube.CubeList 

516 Cubes of temperature. 

517 pressure: iris.cube.Cube | iris.cube.CubeList 

518 Cubes of pressure. 

519 

520 Returns 

521 ------- 

522 iris.cube.Cube | iris.cube.CubeList 

523 Calculated saturation equivalent potential temperature in Kelvin. 

524 

525 Notes 

526 ----- 

527 The saturation equivalent potential temperature, also referred to as the 

528 saturation potential temperature is as the equivalent potential temperature 

529 following a saturated process throughout. It is calculated as 

530 

531 .. math:: \theta_{es} = \theta * exp\left(\frac{L_v w}{c_p T} \right) 

532 

533 for :math:`\theta_{es}` the saturation equivalent potential temperature, 

534 :math:`\theta` the potential temperature, w the mixing ratio, 

535 :math:`c_p` the specific heat capacity of dry air (1005.7 

536 :math:`J kg^{-1} K^{-1}`), :math:`L_v` the latent heat of vapourization 

537 (2.5 x :math:`10^6 J kg^{-1}`), and T the temperature. 

538 

539 As a saturated process is assumed throughout the RH multiplier in the 

540 equivalent potential temperature will always be a value of one, and is thus 

541 omitted. In this operator the potential temperature is calculated from 

542 `temperature.potential_temperature`. 

543 

544 All cubes must be on the same grid. 

545 

546 Examples 

547 -------- 

548 >>> theta_es = temperature.saturation_equivalent_potential_temperature(T, P) 

549 """ 

550 theta_es = iris.cube.CubeList([]) 

551 for T, P in zip( 

552 iter_maybe(temperature), 

553 iter_maybe(pressure), 

554 strict=True, 

555 ): 

556 theta = potential_temperature(T, P) 

557 ws = saturation_mixing_ratio(T, P) 

558 second_term_power = LV * ws / (CPD * T) 

559 second_term = np.exp(second_term_power.core_data()) 

560 TH_ES = theta * second_term 

561 TH_ES.rename("saturation_equivalent_potential_temperature") 

562 TH_ES.units = "K" 

563 theta_es.append(TH_ES) 

564 if len(theta_es) == 1: 

565 return theta_es[0] 

566 else: 

567 return theta_es