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

135 statements  

« prev     ^ index     » next       coverage.py v7.15.3, created at 2026-08-04 08:32 +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 humidity conversions.""" 

16 

17import iris.cube 

18import numpy as np 

19 

20from CSET._common import iter_maybe 

21from CSET.operators._atmospheric_constants import EPSILON 

22from CSET.operators._utils import get_cube_coordindex 

23from CSET.operators.misc import convert_units 

24from CSET.operators.pressure import vapour_pressure 

25 

26 

27def mixing_ratio_from_specific_humidity( 

28 specific_humidity: iris.cube.Cube | iris.cube.CubeList, 

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

30 r"""Convert specific humidity to mixing ratio. 

31 

32 Arguments 

33 --------- 

34 specific_humidity: iris.cube.Cube | iris.cube.CubeList 

35 Cubes of specific humidity to be converted to mixing ratio. 

36 

37 Returns 

38 ------- 

39 iris.cube.Cube | iris.cube.CubeList 

40 Converted mixing ratio. 

41 

42 Notes 

43 ----- 

44 Atmospheric water vapour can be described by multiple quantities. Here, 

45 we convert the specific humidity to the mixing ratio using the following 

46 relation 

47 

48 .. math:: w = \frac{q}{1 - q} 

49 

50 with w the mixing ratio and q the specific humidity. 

51 

52 Larger mixing ratios imply more moisture in the atmosphere. The mixing 

53 ratio will have the same units as the specific humidity (kg/kg). 

54 

55 

56 Examples 

57 -------- 

58 >>> w = humidity.mixing_ratio_from_specific_humidity(specific_humidity) 

59 """ 

60 w = iris.cube.CubeList([]) 

61 for q in iter_maybe(specific_humidity): 

62 mr = q.copy() 

63 mr = q / (1 - q) 

64 mr.rename("mixing_ratio") 

65 w.append(mr) 

66 if len(w) == 1: 

67 return w[0] 

68 else: 

69 return w 

70 

71 

72def specific_humidity_from_mixing_ratio( 

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

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

75 r"""Convert mixing ratio to specific humidity. 

76 

77 Arguments 

78 --------- 

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

80 Cubes of mixing ratio to be converted to specific humidity. 

81 

82 Returns 

83 ------- 

84 iris.cube.Cube | iris.cube.CubeList 

85 Converted specific humidity. 

86 

87 Notes 

88 ----- 

89 Here, we invert the relationship from `humidity.mixing_ratio_from_specific_humidity` 

90 for the following relation 

91 

92 .. math:: q = \frac{w}{1 + w} 

93 

94 with q the specific humidity and w the mixing ratio. 

95 

96 A larger specific humidity implies a more moist atmosphere. The specific 

97 humidity will have the same units as the mixing ratio (kg/kg). 

98 

99 Examples 

100 -------- 

101 >>> q = humidity.specific_humidity_from_mixing_ratio(mixing_ratio) 

102 """ 

103 q = iris.cube.CubeList([]) 

104 for w in iter_maybe(mixing_ratio): 

105 sh = w.copy() 

106 sh = w / (1 + w) 

107 sh.rename("specific_humidity") 

108 q.append(sh) 

109 if len(q) == 1: 

110 return q[0] 

111 else: 

112 return q 

113 

114 

115def saturation_mixing_ratio( 

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

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

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

119 r"""Calculate saturation mixing ratio. 

120 

121 Arguments 

122 --------- 

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

124 Cubes of temperature in Kelvin. 

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

126 Cubes of pressure. 

127 

128 Returns 

129 ------- 

130 iris.cube.Cube | iris.cube.CubeList 

131 Saturation mixing ratio in kg/kg. 

132 

133 Notes 

134 ----- 

135 The saturation mixing ratio is required to help calculate the relative 

136 humidity and other diagnostics with respect to the mixing ratio. It can 

137 be calculated from 

138 

139 .. math:: w = \epsilon \frac{e}{P - e} 

140 

141 for w the mixing ratio, :math:`\epsilon` the ratio between the mixing ratio 

142 of dry and moist air equating to 0.62197, P the pressure and e the vapour 

143 pressure. To ensure that the saturation mixing ratio (:math:`w_s`) is 

144 calculated the vapour pressure calculated should be with the (dry-bulb) 

145 temperature to ensure it is the saturated vapour pressure. 

146 

147 All cubes need to be on the same grid. 

148 

149 Examples 

150 -------- 

151 >>> w_s = humidity.saturation_mixing_ratio(temperature, pressure) 

152 """ 

153 w = iris.cube.CubeList([]) 

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

155 P = convert_units(P, "hPa") 

156 mr = (EPSILON * vapour_pressure(T)) / (P - vapour_pressure(T)) 

157 mr.units = "kg/kg" 

158 mr.rename("saturation_mixing_ratio") 

159 w.append(mr) 

160 if len(w) == 1: 

161 return w[0] 

162 else: 

163 return w 

164 

165 

166def saturation_specific_humidity( 

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

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

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

170 r"""Calculate saturation specific humidity. 

171 

172 Arguments 

173 --------- 

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

175 Cubes of temperature in Kelvin. 

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

177 Cubes of pressure. 

178 

179 Returns 

180 ------- 

181 iris.cube.Cube | iris.cube.CubeList 

182 Saturation specific humidity in kg/kg. 

183 

184 Notes 

185 ----- 

186 The saturation specific humidity is required to help calculate the relative 

187 humidity and other diagnostics with respect to the mixing ratio. It can 

188 be calculated from 

189 

190 .. math:: q = \epsilon \frac{e}{P} 

191 

192 for q the specific humidity, :math:`\epsilon` the ratio between the mixing ratio 

193 of dry and moist air equating to 0.62197, P the pressure and e the vapour 

194 pressure. To ensure that the saturation specific humidity (:math:`q_{sat}`) is 

195 calculated the vapour pressure calculated should be with the (dry-bulb) 

196 temperature to ensure it is the saturated vapour pressure. 

197 

198 All cubes need to be on the same grid. 

199 

200 Examples 

201 -------- 

202 >>> q_sat = humidity.saturation_specific_humidity(temperature, pressure) 

203 """ 

204 q = iris.cube.CubeList([]) 

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

206 P = convert_units(P, "hPa") 

207 sh = (EPSILON * vapour_pressure(T)) / P 

208 sh.units = "kg/kg" 

209 sh.rename("saturation_specific_humidity") 

210 q.append(sh) 

211 if len(q) == 1: 

212 return q[0] 

213 else: 

214 return q 

215 

216 

217def mixing_ratio_from_relative_humidity( 

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

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

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

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

222 r"""Calculate the mixing ratio from RH. 

223 

224 Arguments 

225 --------- 

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

227 Cubes of temperature in Kelvin. 

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

229 Cubes of pressure. 

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

231 Cubes of relative humidity. 

232 

233 Returns 

234 ------- 

235 iris.cube.Cube | iris.cube.CubeList 

236 Calculated mixing ratio from relative humidity in kg/kg. 

237 

238 Notes 

239 ----- 

240 The mixing ratio can be calculated from temperature, pressure, and 

241 relative humidity using the following relation 

242 

243 .. math:: w = RH * w_s 

244 

245 for w the mixing ratio, :math:`w_s` the saturation mixing ratio, and 

246 RH the relative humidity. RH is converted to dimensionless fraction rather 

247 than percentage. 

248 

249 The operator uses `humidity.saturation_mixing_ratio` to calculate the 

250 saturation mixing ratio from the temperature and pressure. The relative 

251 humidity is converted into a decimal before the multiplication occurs. 

252 

253 All cubes need to be on the same grid. 

254 

255 Examples 

256 -------- 

257 >>> w = humidity.mixing_ratio_from_relative_humidity(T, P, RH) 

258 """ 

259 w = iris.cube.CubeList([]) 

260 for T, P, RH in zip( 

261 iter_maybe(temperature), 

262 iter_maybe(pressure), 

263 iter_maybe(relative_humidity), 

264 strict=True, 

265 ): 

266 RH = convert_units(RH, "1") 

267 mr = saturation_mixing_ratio(T, P) * RH 

268 mr.rename("mixing_ratio") 

269 mr.units = "kg/kg" 

270 w.append(mr) 

271 if len(w) == 1: 

272 return w[0] 

273 else: 

274 return w 

275 

276 

277def specific_humidity_from_relative_humidity( 

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

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

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

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

282 r"""Calculate the specific humidity from relative humidity. 

283 

284 Arguments 

285 --------- 

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

287 Cubes of temperature in Kelvin. 

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

289 Cubes of pressure. 

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

291 Cubes of relative humidity. 

292 

293 Returns 

294 ------- 

295 iris.cube.Cube | iris.cube.CubeList 

296 Calculated specific humidity from relative humidity in kg/kg. 

297 

298 Notes 

299 ----- 

300 The specific humidity can be calculated from temperature, pressure, and 

301 relative humidity using the following relation 

302 

303 .. math:: q = RH * q_{sat} 

304 

305 for q the specific humidity, :math:`q_{sat}` the saturation specific 

306 humidity, and RH the relative humidity. 

307 

308 The operator uses `humidity.saturation_specific_humidity` to calculate the 

309 saturation specific humidity from the temperature and pressure. The relative 

310 humidity is converted into a decimal before the multiplication occurs. 

311 

312 All cubes need to be on the same grid. 

313 

314 Examples 

315 -------- 

316 >>> q = humidity.specific_humidity_from_relative_humidity(T, P, RH) 

317 """ 

318 q = iris.cube.CubeList([]) 

319 for T, P, RH in zip( 

320 iter_maybe(temperature), 

321 iter_maybe(pressure), 

322 iter_maybe(relative_humidity), 

323 strict=True, 

324 ): 

325 RH = convert_units(RH, "1") 

326 sh = saturation_specific_humidity(T, P) * RH 

327 sh.rename("specific_humidity") 

328 sh.units = "kg/kg" 

329 q.append(sh) 

330 if len(q) == 1: 

331 return q[0] 

332 else: 

333 return q 

334 

335 

336def relative_humidity_from_mixing_ratio( 

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

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

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

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

341 r"""Convert mixing ratio to relative humidity. 

342 

343 Arguments 

344 --------- 

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

346 Cubes of mixing ratio. 

347 temperature: iris.cube.Cube | iris.cube.CuebList 

348 Cubes of temperature in Kelvin. 

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

350 Cubes of pressure. 

351 

352 Returns 

353 ------- 

354 iris.cube.Cube | iris.cube.CubeList 

355 Relative humidity calculated from mixing ratio. 

356 

357 Notes 

358 ----- 

359 The relative humidity can be calculated from the mixing ratio following 

360 

361 .. math:: RH = \frac{w}{w_s} 

362 

363 for RH the relative humidity, w the mixing ratio, and :math:`w_s` the 

364 saturation mixing ratio. The saturation mixing ratio is calculated using 

365 `humidity.saturation_mixing_ratio`. 

366 

367 The RH varies predominatly between zero (completely dry) and one (saturated). 

368 Values larger than one are possible and imply supersaturation. 

369 

370 All cubes must be on the same grid. 

371 

372 Examples 

373 -------- 

374 >>> RH = humidity.relative_humidity_from_mixing_ratio(w, T, P) 

375 """ 

376 RH = iris.cube.CubeList([]) 

377 for W, T, P in zip( 

378 iter_maybe(mixing_ratio), 

379 iter_maybe(temperature), 

380 iter_maybe(pressure), 

381 strict=True, 

382 ): 

383 rel_h = W / saturation_mixing_ratio(T, P) 

384 rel_h.rename("relative_humidity") 

385 rel_h = convert_units(rel_h, "%") 

386 RH.append(rel_h) 

387 if len(RH) == 1: 

388 return RH[0] 

389 else: 

390 return RH 

391 

392 

393def relative_humidity_from_specific_humidity( 

394 specific_humidity: iris.cube.Cube | iris.cube.CubeList, 

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

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

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

398 r"""Convert specific humidity to relative humidity. 

399 

400 Arguments 

401 --------- 

402 specific_humidity: iris.cube.Cube | iris.cube.CubeList 

403 Cubes of specific humidity. 

404 temperature: iris.cube.Cube | iris.cube.CuebList 

405 Cubes of temperature in Kelvin. 

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

407 Cubes of pressure. 

408 

409 Returns 

410 ------- 

411 iris.cube.Cube | iris.cube.CubeList 

412 Relative humidity calculated from specific humidity. 

413 

414 Notes 

415 ----- 

416 The relative humidity can be calculated from the specific humidity following 

417 

418 .. math:: RH = \frac{q}{q_{sat}} 

419 

420 for RH the relative humidity, q the specific humidity, and :math:`q_{sat}` the 

421 saturation specific humidity. The saturation specific humidity is calculated using 

422 `humidity.saturation_specific_humidity`. 

423 

424 The RH varies predominantly between zero (completely dry) and one (saturated). 

425 Values larger than one are possible and imply supersaturation. 

426 

427 All cubes must be on the same grid. 

428 

429 Examples 

430 -------- 

431 >>> RH = humidity.relative_humidity_from_specific_humidity(q, T, P) 

432 """ 

433 RH = iris.cube.CubeList([]) 

434 for Q, T, P in zip( 

435 iter_maybe(specific_humidity), 

436 iter_maybe(temperature), 

437 iter_maybe(pressure), 

438 strict=True, 

439 ): 

440 rel_h = Q / saturation_specific_humidity(T, P) 

441 rel_h.rename("relative_humidity") 

442 rel_h = convert_units(rel_h, "%") 

443 RH.append(rel_h) 

444 if len(RH) == 1: 

445 return RH[0] 

446 else: 

447 return RH 

448 

449 

450def precipitable_water( 

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

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

453 r"""Calculate the precipitable water. 

454 

455 Arguments 

456 --------- 

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

458 A cube or cubelist of the mixing ratio. It can be 

459 calculated within a recipe or a direct model output. 

460 

461 Returns 

462 ------- 

463 iris.cube.Cube | iris.cube.CubeList 

464 A cube or cubelist of the precipitable water. 

465 

466 Notes 

467 ----- 

468 The precipitable water is the total depth of liquid water produced by 

469 condensing all of the moisture in a column of the atmopshere. 

470 

471 It can be calculated following [Stull2000]_ as 

472 

473 .. math:: pw = frac{1}{\rho_w} \int w dz 

474 

475 for pw the precipitable water, ..math::\rho_{w} the density of water, 

476 w the mixing ratio, and z the height. It is integrated from the surface 

477 to the top of the atmosphere. 

478 

479 Generally, the precipitable water is widely applicable across the globe. 

480 It is likely that larger precipitation totals are associated with greater 

481 precipitable water. However, this is not strictly the case and you can get 

482 lower precipitable water values with large precipitaiton amounts 

483 (e.g. [Daviesetal24]_). Therefore, caution is needed with its interpretation. 

484 A diagnostic such as saturation fraction maybe more beneficial (e.g. [Daviesetal26]_). 

485 

486 Currently, it is assumed that a cube is given with dimensions of the order 

487 [realization, time, height, latitude, longitude] for a five dimension cube, 

488 and derivatives thereof for 4 and 3 Dimension cubes (i.e., remove realization, 

489 and time as appropriate). 

490 

491 Examples 

492 -------- 

493 >>> pwat = humidity.precipitable_water(mixing_ratio) 

494 

495 References 

496 ---------- 

497 .. [Daviesetal24] Davies, P.A., Fowler, H.J, Villalobos-Herrera, R., 

498 Slingo, J., Flack, D.L.A., and Taszarek, M (2024) 

499 "A New Conceptual Model for Understanding and Predicting Life-Threatening 

500 Rainfall Extremes." Weather and Climate Extremes, vol. 45, 100696, 

501 doi: 10.1016/j.wace.2024.100696 

502 .. [Daviesetal26] Davies, P. A., Flack, D. L. A., Pirret, J., Fowler, H. J. 

503 (2026) "Application of the Davies Four-Stage Conceptual Model for 

504 Life-Threatening Rainfall Extremes on the April 2024 United Arab Emirates 

505 and Oman Floods." Weather and Climate Extremes, vol. 51, 100846. 

506 doi:10.1016/j.wace.2025.100846 

507 .. [Stull2000] Stull, R.B., (2000) "Meteorology for Scientists and Engineers", 

508 2nd Edition, Brooks/Cole, California, USA, 502 pp. 

509 """ 

510 precipitable_water = iris.cube.CubeList([]) 

511 for w in iter_maybe(mixing_ratio): 

512 # Integrate the data in the vertical using np.trapezoid 

513 # (following trapezoid rule). 

514 pw = np.trapezoid( 

515 w.data, 

516 x=w.coord("level_height").points[:], 

517 axis=get_cube_coordindex(w, "level_height"), 

518 ) 

519 # Determine array information of input cube to get 

520 # correct cube to copy across to. 

521 if len(w.coord("realization").points) != 1 and len(w.coord("time").points) != 1: 

522 pwat = w[:, :, 0, :, :].copy() 

523 elif ( 

524 len(w.coord("realization").points) != 1 and len(w.coord("time").points) == 1 

525 ) or ( 

526 len(w.coord("time").points) != 1 and len(w.coord("realization").points) == 1 

527 ): 

528 pwat = w[:, 0, :, :].copy() 

529 else: 

530 pwat = w[0, :, :].copy() 

531 # Create the data array, rename, and correct units. 

532 pwat.data = pw 

533 pwat.rename("precipitable_water") 

534 # Setting units to mm to account for normalization by density of water. 

535 pwat.units = "mm" 

536 precipitable_water.append(pwat) 

537 # Output the data. 

538 if len(precipitable_water) == 1: 

539 return precipitable_water[0] 

540 else: 

541 return precipitable_water 

542 

543 

544def saturation_precipitable_water( 

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

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

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

548 r"""Calculate saturation precipitable water. 

549 

550 Arguments 

551 --------- 

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

553 A cube or cubelist of the mixing ratio. It can be 

554 calculated within a recipe or a direct model output. 

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

556 A cube or cubelist of the relative humidity. It can 

557 either be calculated or used as model output. 

558 

559 Returns 

560 ------- 

561 iris.cube.Cube | iris.cube.CubeList 

562 A cube or cubelist of the saturation precipitable water. 

563 

564 Notes 

565 ----- 

566 The saturation precipitable water is equivalent to the precipitable 

567 water assuming that the atmosphere was fully saturated. 

568 

569 It can be calculated following [Raymondetal2009]_ as 

570 

571 .. math:: spw = frac{1}{\rho_w} \int \frac{w}{RH} dz 

572 

573 for spw the saturated precipitable water, ..math::\rho_{w} the density of water, 

574 w the mixing ratio, RH the relative humidity (as a decimal) and z the height. 

575 It is integrated from the surface to the top of the atmosphere. 

576 

577 It is applicable throughout the globe and is, perhaps, best considered 

578 in relation to the precipitable water. A useful way to do this is 

579 via the saturation fraction. 

580 

581 Currently, it is assumed that a cube is given with dimensions of the order 

582 [realization, time, height, latitude, longitude] for a five dimension cube, 

583 and derivatives thereof for 4 and 3 Dimension cubes (i.e., remove realization, 

584 and time as appropriate). 

585 

586 Examples 

587 -------- 

588 >>> sat_pwat = humidity.saturated_precipitable_water(mixing_ratio, RH) 

589 

590 References 

591 ---------- 

592 .. [Raymondetal2009] Raymond, D.J., Sessions, S.L., Sobel, A.H., Fuchs, Z. 

593 (2009) "The Mechanics of Gross Moist Stability" Journal of Advances in 

594 Modelling Earth Systems, vol. 1, 20 pp. doi: 10.3894/JAMES.2009.1.9 

595 """ 

596 saturation_precipitable_water = iris.cube.CubeList([]) 

597 for w, rh in zip( 

598 iter_maybe(mixing_ratio), iter_maybe(relative_humidity), strict=True 

599 ): 

600 # Integrate the data in the vertical using np.trapezoid 

601 # (following trapezoid rule). 

602 rh = convert_units(rh, "1") 

603 spw = np.trapezoid( 

604 (w / rh).data, 

605 x=w.coord("level_height").points[:], 

606 axis=get_cube_coordindex(w, "level_height"), 

607 ) 

608 # Determine array information of input cube to get 

609 # correct cube to copy across to. 

610 if len(w.coord("realization").points) != 1 and len(w.coord("time").points) != 1: 

611 satpw = w[:, :, 0, :, :].copy() 

612 elif ( 

613 len(w.coord("realization").points) != 1 and len(w.coord("time").points) == 1 

614 ) or ( 

615 len(w.coord("time").points) != 1 and len(w.coord("realization").points) == 1 

616 ): 

617 satpw = w[:, 0, :, :].copy() 

618 else: 

619 satpw = w[0, :, :].copy() 

620 # Store the data for output, rename cube, and correct units. 

621 satpw.data = spw 

622 satpw.rename("saturation_precipitable_water") 

623 # Setting units to mm to account for normalization by density of water. 

624 satpw.units = "mm" 

625 saturation_precipitable_water.append(satpw) 

626 # Output cube/cubelist. 

627 if len(saturation_precipitable_water) == 1: 

628 return saturation_precipitable_water[0] 

629 else: 

630 return saturation_precipitable_water 

631 

632 

633def saturation_fraction( 

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

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

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

637 r"""Calculate saturation fraction. 

638 

639 Arguments 

640 --------- 

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

642 A cube or cubelist of the mixing ratio. It can be 

643 calculated within a recipe or a direct model output. 

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

645 A cube or cubelist of the relative humidity. It can be 

646 calculated within a recipe or used as a direct model output. 

647 

648 Returns 

649 ------- 

650 iris.cube.Cube | iris.cube.CubeList 

651 A cube or cubelist of the saturation fraction. 

652 

653 Notes 

654 ----- 

655 The saturation fraction indicates how moist a column of the atmosphere 

656 is. A value close to one implies that the atmosphere is fully saturated 

657 throughout the entire column. Smaller values imply the atmosphere is 

658 drier throughout the column. It is based around ideas of specific entropy 

659 ([Zengetal05]_) but can be simplified to an approximation following [Daviesetal2026]_. 

660 

661 It can be approximated following [Raymondetal09]_ as 

662 

663 .. math:: saturation_fraction = \frac{precipitable_water}{saturation_precipitable_water} 

664 

665 and can be used throughout the globe with the same interpretation. 

666 

667 For a recent example, [Daviesetal2026]_ have applied the concept to their 

668 conceptual model for extreme rainfall. Thus it is a potentially useful diagnostic 

669 to consider for extreme events, and is thought of as more reliable than 

670 using precipitable water on its own. 

671 

672 Currently, it is assumed that a cube is given with dimensions of the order 

673 [realization, time, height, latitude, longitude] for a five dimension cube, 

674 and derivatives thereof for 4 and 3 Dimension cubes (i.e., remove realization, 

675 and time as appropriate). 

676 

677 Examples 

678 -------- 

679 >>> sf = humidity.saturation_fraction(mixing_ratio, relative_humidity) 

680 

681 References 

682 ---------- 

683 .. [Daviesetal2026] Davies, P. A., Flack, D. L. A., Pirret, J., Fowler, H. J. 

684 (2026) "Application of the Davies Four-Stage Conceptual Model for 

685 Life-Threatening Rainfall Extremes on the April 2024 United Arab Emirates 

686 and Oman Floods." Weather and Climate Extremes, vol. 51, 100846. 

687 doi:10.1016/j.wace.2025.100846 

688 .. [Raymondetal09] Raymond, D.J., Sessions, S.L., Sobel, A.H., Fuchs, Z. 

689 (2009) "The Mechanics of Gross Moist Stability" Journal of Advances in 

690 Modelling Earth Systems, vol. 1, 20 pp. doi: 10.3894/JAMES.2009.1.9 

691 .. [Zengetal05] Zeng, X., Tao, W-K, and Simpson, J. (2005) "An Equation for Moist 

692 Entropy in a Precipitating and Icy Atmosphere" Journal of the Atmospheric Sciences, 

693 vol. 2, 4293-4309, doi: 10.1175/JAS3570.1 

694 """ 

695 saturation_fraction = iris.cube.CubeList([]) 

696 for w, rh in zip( 

697 iter_maybe(mixing_ratio), iter_maybe(relative_humidity), strict=True 

698 ): 

699 # Calculate both precipitable water and saturation 

700 # precipitable water. 

701 pw = precipitable_water(w) 

702 spw = saturation_precipitable_water(w, rh) 

703 # Calculate the saturation fraction by taking the ratio. 

704 sf = pw / spw 

705 # Rename the cube and append to cubelist. 

706 sf.rename("saturation_fraction") 

707 saturation_fraction.append(sf) 

708 # Output the cube/cubelist. 

709 if len(saturation_fraction) == 1: 

710 return saturation_fraction[0] 

711 else: 

712 return saturation_fraction