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

139 statements  

« prev     ^ index     » next       coverage.py v7.15.0, created at 2026-07-02 14:04 +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 ): 

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

527 elif ( 

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

529 ): 

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

531 else: 

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

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

534 pwat.data = pw 

535 pwat.rename("precipitable_water") 

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

537 pwat.units = "mm" 

538 precipitable_water.append(pwat) 

539 # Output the data. 

540 if len(precipitable_water) == 1: 

541 return precipitable_water[0] 

542 else: 

543 return precipitable_water 

544 

545 

546def saturation_precipitable_water( 

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

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

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

550 r"""Calculate saturation precipitable water. 

551 

552 Arguments 

553 --------- 

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

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

556 calculated within a recipe or a direct model output. 

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

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

559 either be calculated or used as model output. 

560 

561 Returns 

562 ------- 

563 iris.cube.Cube | iris.cube.CubeList 

564 A cube or cubelist of the saturation precipitable water. 

565 

566 Notes 

567 ----- 

568 The saturation precipitable water is equivalent to the precipitable 

569 water assuming that the atmosphere was fully saturated. 

570 

571 It can be calculated following [Raymondetal2009]_ as 

572 

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

574 

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

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

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

578 

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

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

581 via the saturation fraction. 

582 

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

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

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

586 and time as appropriate). 

587 

588 Examples 

589 -------- 

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

591 

592 References 

593 ---------- 

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

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

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

597 """ 

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

599 for w, rh in zip( 

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

601 ): 

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

603 # (following trapezoid rule). 

604 rh = convert_units(rh, "1") 

605 spw = np.trapezoid( 

606 (w / rh).data, 

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

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

609 ) 

610 # Determine array information of input cube to get 

611 # correct cube to copy across to. 

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

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

614 elif ( 

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

616 ): 

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

618 elif ( 

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

620 ): 

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

622 else: 

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

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

625 satpw.data = spw 

626 satpw.rename("saturation_precipitable_water") 

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

628 satpw.units = "mm" 

629 saturation_precipitable_water.append(satpw) 

630 # Output cube/cubelist. 

631 if len(saturation_precipitable_water) == 1: 

632 return saturation_precipitable_water[0] 

633 else: 

634 return saturation_precipitable_water 

635 

636 

637def saturation_fraction( 

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

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

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

641 r"""Calculate saturation fraction. 

642 

643 Arguments 

644 --------- 

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

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

647 calculated within a recipe or a direct model output. 

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

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

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

651 

652 Returns 

653 ------- 

654 iris.cube.Cube | iris.cube.CubeList 

655 A cube or cubelist of the saturation fraction. 

656 

657 Notes 

658 ----- 

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

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

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

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

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

664 

665 It can be approximated following [Raymondetal09]_ as 

666 

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

668 

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

670 

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

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

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

674 using precipitable water on its own. 

675 

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

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

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

679 and time as appropriate). 

680 

681 Examples 

682 -------- 

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

684 

685 References 

686 ---------- 

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

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

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

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

691 doi:10.1016/j.wace.2025.100846 

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

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

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

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

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

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

698 """ 

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

700 for w, rh in zip( 

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

702 ): 

703 # Calculate both precipitable water and saturation 

704 # precipitable water. 

705 pw = precipitable_water(w) 

706 spw = saturation_precipitable_water(w, rh) 

707 # Calculate the saturation fraction by taking the ratio. 

708 sf = pw / spw 

709 # Rename the cube and append to cubelist. 

710 sf.rename("saturation_fraction") 

711 saturation_fraction.append(sf) 

712 # Output the cube/cubelist. 

713 if len(saturation_fraction) == 1: 

714 return saturation_fraction[0] 

715 else: 

716 return saturation_fraction