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

139 statements  

« prev     ^ index     » next       coverage.py v7.14.1, created at 2026-06-09 07:49 +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 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 Examples 

487 -------- 

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

489 

490 References 

491 ---------- 

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

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

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

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

496 doi: 10.1016/j.wace.2024.100696 

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

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

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

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

501 doi:10.1016/j.wace.2025.100846 

502 """ 

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

504 for w in iter_maybe(mixing_ratio): 

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

506 # (following trapezoid rule). 

507 pw = np.trapezoid( 

508 w.data, 

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

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

511 ) 

512 # Determine array information of input cube to get 

513 # correct cube to copy across to. 

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

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

516 elif ( 

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

518 ): 

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

520 elif ( 

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

522 ): 

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

524 else: 

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

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

527 pwat.data = pw 

528 pwat.rename("precipitable_water") 

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

530 pwat.units = "mm" 

531 precipitable_water.append(pwat) 

532 # Output the data. 

533 if len(precipitable_water) == 1: 

534 return precipitable_water[0] 

535 else: 

536 return precipitable_water 

537 

538 

539def saturation_precipitable_water( 

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

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

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

543 r"""Calculate saturation precipitable water. 

544 

545 Arguments 

546 --------- 

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

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

549 calculated within a recipe or a direct model output. 

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

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

552 either be calculated or used as model output. 

553 

554 Returns 

555 ------- 

556 iris.cube.Cube | iris.cube.CubeList 

557 A cube or cubelist of the saturation precipitable water. 

558 

559 Notes 

560 ----- 

561 The saturation precipitable water is equivalent to the precipitable 

562 water assuming that the atmosphere was fully saturated. 

563 

564 It can be calculated as 

565 

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

567 

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

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

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

571 

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

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

574 via the saturation fraction. 

575 

576 Examples 

577 -------- 

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

579 """ 

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

581 for w, rh in zip( 

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

583 ): 

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

585 # (following trapezoid rule). 

586 rh = convert_units(rh, "1") 

587 spw = np.trapezoid( 

588 (w / rh).data, 

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

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

591 ) 

592 # Determine array information of input cube to get 

593 # correct cube to copy across to. 

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

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

596 elif ( 

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

598 ): 

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

600 elif ( 

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

602 ): 

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

604 else: 

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

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

607 satpw.data = spw 

608 satpw.rename("saturation_precipitable_water") 

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

610 satpw.units = "mm" 

611 saturation_precipitable_water.append(satpw) 

612 # Output cube/cubelist. 

613 if len(saturation_precipitable_water) == 1: 

614 return saturation_precipitable_water[0] 

615 else: 

616 return saturation_precipitable_water 

617 

618 

619def saturation_fraction( 

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

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

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

623 r"""Calculate saturation fraction. 

624 

625 Arguments 

626 --------- 

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

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

629 calculated within a recipe or a direct model output. 

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

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

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

633 

634 Returns 

635 ------- 

636 iris.cube.Cube | iris.cube.CubeList 

637 A cube or cubelist of the saturation fraction. 

638 

639 Notes 

640 ----- 

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

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

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

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

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

646 

647 It can be approximated as 

648 

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

650 

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

652 

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

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

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

656 using precipitable water on its own. 

657 

658 Examples 

659 -------- 

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

661 

662 References 

663 ---------- 

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

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

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

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

668 doi:10.1016/j.wace.2025.100846 

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

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

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

672 """ 

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

674 for w, rh in zip( 

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

676 ): 

677 # Calculate both precipitable water and saturation 

678 # precipitable water. 

679 pw = precipitable_water(w) 

680 spw = saturation_precipitable_water(w, rh) 

681 # Calculate the saturation fraction by taking the ratio. 

682 sf = pw / spw 

683 # Rename the cube and append to cubelist. 

684 sf.rename("saturation_fraction") 

685 saturation_fraction.append(sf) 

686 # Output the cube/cubelist. 

687 if len(saturation_fraction) == 1: 

688 return saturation_fraction[0] 

689 else: 

690 return saturation_fraction