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
« 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.
15"""Operators for humidity conversions."""
17import iris.cube
18import numpy as np
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
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.
32 Arguments
33 ---------
34 specific_humidity: iris.cube.Cube | iris.cube.CubeList
35 Cubes of specific humidity to be converted to mixing ratio.
37 Returns
38 -------
39 iris.cube.Cube | iris.cube.CubeList
40 Converted mixing ratio.
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
48 .. math:: w = \frac{q}{1 - q}
50 with w the mixing ratio and q the specific humidity.
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).
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
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.
77 Arguments
78 ---------
79 mixing_ratio: iris.cube.Cube | iris.Cube.CubeList
80 Cubes of mixing ratio to be converted to specific humidity.
82 Returns
83 -------
84 iris.cube.Cube | iris.cube.CubeList
85 Converted specific humidity.
87 Notes
88 -----
89 Here, we invert the relationship from `humidity.mixing_ratio_from_specific_humidity`
90 for the following relation
92 .. math:: q = \frac{w}{1 + w}
94 with q the specific humidity and w the mixing ratio.
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).
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
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.
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.
128 Returns
129 -------
130 iris.cube.Cube | iris.cube.CubeList
131 Saturation mixing ratio in kg/kg.
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
139 .. math:: w = \epsilon \frac{e}{P - e}
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.
147 All cubes need to be on the same grid.
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
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.
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.
179 Returns
180 -------
181 iris.cube.Cube | iris.cube.CubeList
182 Saturation specific humidity in kg/kg.
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
190 .. math:: q = \epsilon \frac{e}{P}
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.
198 All cubes need to be on the same grid.
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
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.
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.
233 Returns
234 -------
235 iris.cube.Cube | iris.cube.CubeList
236 Calculated mixing ratio from relative humidity in kg/kg.
238 Notes
239 -----
240 The mixing ratio can be calculated from temperature, pressure, and
241 relative humidity using the following relation
243 .. math:: w = RH * w_s
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.
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.
253 All cubes need to be on the same grid.
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
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.
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.
293 Returns
294 -------
295 iris.cube.Cube | iris.cube.CubeList
296 Calculated specific humidity from relative humidity in kg/kg.
298 Notes
299 -----
300 The specific humidity can be calculated from temperature, pressure, and
301 relative humidity using the following relation
303 .. math:: q = RH * q_{sat}
305 for q the specific humidity, :math:`q_{sat}` the saturation specific
306 humidity, and RH the relative humidity.
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.
312 All cubes need to be on the same grid.
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
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.
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.
352 Returns
353 -------
354 iris.cube.Cube | iris.cube.CubeList
355 Relative humidity calculated from mixing ratio.
357 Notes
358 -----
359 The relative humidity can be calculated from the mixing ratio following
361 .. math:: RH = \frac{w}{w_s}
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`.
367 The RH varies predominatly between zero (completely dry) and one (saturated).
368 Values larger than one are possible and imply supersaturation.
370 All cubes must be on the same grid.
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
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.
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.
409 Returns
410 -------
411 iris.cube.Cube | iris.cube.CubeList
412 Relative humidity calculated from specific humidity.
414 Notes
415 -----
416 The relative humidity can be calculated from the specific humidity following
418 .. math:: RH = \frac{q}{q_{sat}}
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`.
424 The RH varies predominantly between zero (completely dry) and one (saturated).
425 Values larger than one are possible and imply supersaturation.
427 All cubes must be on the same grid.
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
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.
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.
461 Returns
462 -------
463 iris.cube.Cube | iris.cube.CubeList
464 A cube or cubelist of the precipitable water.
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.
471 It can be calculated following [Stull2000]_ as
473 .. math:: pw = frac{1}{\rho_w} \int w dz
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.
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]_).
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).
491 Examples
492 --------
493 >>> pwat = humidity.precipitable_water(mixing_ratio)
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
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.
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.
559 Returns
560 -------
561 iris.cube.Cube | iris.cube.CubeList
562 A cube or cubelist of the saturation precipitable water.
564 Notes
565 -----
566 The saturation precipitable water is equivalent to the precipitable
567 water assuming that the atmosphere was fully saturated.
569 It can be calculated following [Raymondetal2009]_ as
571 .. math:: spw = frac{1}{\rho_w} \int \frac{w}{RH} dz
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.
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.
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).
586 Examples
587 --------
588 >>> sat_pwat = humidity.saturated_precipitable_water(mixing_ratio, RH)
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
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.
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.
648 Returns
649 -------
650 iris.cube.Cube | iris.cube.CubeList
651 A cube or cubelist of the saturation fraction.
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]_.
661 It can be approximated following [Raymondetal09]_ as
663 .. math:: saturation_fraction = \frac{precipitable_water}{saturation_precipitable_water}
665 and can be used throughout the globe with the same interpretation.
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.
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).
677 Examples
678 --------
679 >>> sf = humidity.saturation_fraction(mixing_ratio, relative_humidity)
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