Coverage for src / CSET / operators / humidity.py: 100%
89 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-08 16:49 +0000
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-08 16: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.
15"""Operators for humidity conversions."""
17import iris.cube
19from CSET._common import iter_maybe
20from CSET.operators._atmospheric_constants import EPSILON
21from CSET.operators.misc import convert_units
22from CSET.operators.pressure import vapour_pressure
25def mixing_ratio_from_specific_humidity(
26 specific_humidity: iris.cube.Cube | iris.cube.CubeList,
27) -> iris.cube.Cube | iris.cube.CubeList:
28 """Convert specific humidity to mixing ratio."""
29 w = iris.cube.CubeList([])
30 for q in iter_maybe(specific_humidity):
31 mr = q.copy()
32 mr = q / (1 - q)
33 mr.rename("mixing_ratio")
34 w.append(mr)
35 if len(w) == 1:
36 return w[0]
37 else:
38 return w
41def specific_humidity_from_mixing_ratio(
42 mixing_ratio: iris.cube.Cube | iris.cube.CubeList,
43) -> iris.cube.Cube | iris.cube.CubeList:
44 """Convert mixing ratio to specific humidity."""
45 q = iris.cube.CubeList([])
46 for w in iter_maybe(mixing_ratio):
47 sh = w.copy()
48 sh = w / (1 + w)
49 sh.rename("specific_humidity")
50 q.append(sh)
51 if len(q) == 1:
52 return q[0]
53 else:
54 return q
57def saturation_mixing_ratio(
58 temperature: iris.cube.Cube | iris.cube.CubeList,
59 pressure: iris.cube.Cube | iris.cube.CubeList,
60) -> iris.cube.Cube | iris.cube.CubeList:
61 """Calculate saturation mixing ratio."""
62 w = iris.cube.CubeList([])
63 for T, P in zip(iter_maybe(temperature), iter_maybe(pressure), strict=True):
64 P = convert_units(P, "hPa")
65 mr = (EPSILON * vapour_pressure(T)) / (P - vapour_pressure(T))
66 mr.units = "kg/kg"
67 mr.rename("saturation_mixing_ratio")
68 w.append(mr)
69 if len(w) == 1:
70 return w[0]
71 else:
72 return w
75def saturation_specific_humidity(
76 temperature: iris.cube.Cube | iris.cube.CubeList,
77 pressure: iris.cube.Cube | iris.cube.CubeList,
78) -> iris.cube.Cube | iris.cube.CubeList:
79 """Calculate saturation specific humidity."""
80 q = iris.cube.CubeList([])
81 for T, P in zip(iter_maybe(temperature), iter_maybe(pressure), strict=True):
82 P = convert_units(P, "hPa")
83 sh = (EPSILON * vapour_pressure(T)) / P
84 sh.units = "kg/kg"
85 sh.rename("saturation_specific_humidity")
86 q.append(sh)
87 if len(q) == 1:
88 return q[0]
89 else:
90 return q
93def mixing_ratio_from_relative_humidity(
94 temperature: iris.cube.Cube | iris.cube.CubeList,
95 pressure: iris.cube.Cube | iris.cube.CubeList,
96 relative_humidity: iris.cube.Cube | iris.cube.CubeList,
97) -> iris.cube.Cube | iris.cube.CubeList:
98 """Calculate the mixing ratio from RH."""
99 w = iris.cube.CubeList([])
100 for T, P, RH in zip(
101 iter_maybe(temperature),
102 iter_maybe(pressure),
103 iter_maybe(relative_humidity),
104 strict=True,
105 ):
106 RH = convert_units(RH, "1")
107 mr = saturation_mixing_ratio(T, P) * RH
108 mr.rename("mixing_ratio")
109 mr.units = "kg/kg"
110 w.append(mr)
111 if len(w) == 1:
112 return w[0]
113 else:
114 return w
117def specific_humidity_from_relative_humidity(
118 temperature: iris.cube.Cube | iris.cube.CubeList,
119 pressure: iris.cube.Cube | iris.cube.CubeList,
120 relative_humidity: iris.cube.Cube | iris.cube.CubeList,
121) -> iris.cube.Cube | iris.cube.CubeList:
122 """Calculate the mixing ratio from RH."""
123 q = iris.cube.CubeList([])
124 for T, P, RH in zip(
125 iter_maybe(temperature),
126 iter_maybe(pressure),
127 iter_maybe(relative_humidity),
128 strict=True,
129 ):
130 RH = convert_units(RH, "1")
131 sh = saturation_specific_humidity(T, P) * RH
132 sh.rename("specific_humidity")
133 sh.units = "kg/kg"
134 q.append(sh)
135 if len(q) == 1:
136 return q[0]
137 else:
138 return q
141def relative_humidity_from_mixing_ratio(
142 mixing_ratio: iris.cube.Cube | iris.cube.CubeList,
143 temperature: iris.cube.Cube | iris.cube.CubeList,
144 pressure: iris.cube.Cube | iris.cube.CubeList,
145) -> iris.cube.Cube | iris.cube.CubeList:
146 """Convert mixing ratio to relative humidity."""
147 RH = iris.cube.CubeList([])
148 for W, T, P in zip(
149 iter_maybe(mixing_ratio),
150 iter_maybe(temperature),
151 iter_maybe(pressure),
152 strict=True,
153 ):
154 rel_h = W / saturation_mixing_ratio(T, P)
155 rel_h.rename("relative_humidity")
156 rel_h = convert_units(rel_h, "%")
157 RH.append(rel_h)
158 if len(RH) == 1:
159 return RH[0]
160 else:
161 return RH
164def relative_humidity_from_specific_humidity(
165 specific_humidity: iris.cube.Cube | iris.cube.CubeList,
166 temperature: iris.cube.Cube | iris.cube.CubeList,
167 pressure: iris.cube.Cube | iris.cube.CubeList,
168) -> iris.cube.Cube | iris.cube.CubeList:
169 """Convert specific humidity to relative humidity."""
170 RH = iris.cube.CubeList([])
171 for Q, T, P in zip(
172 iter_maybe(specific_humidity),
173 iter_maybe(temperature),
174 iter_maybe(pressure),
175 strict=True,
176 ):
177 rel_h = Q / saturation_specific_humidity(T, P)
178 rel_h.rename("relative_humidity")
179 rel_h = convert_units(rel_h, "%")
180 RH.append(rel_h)
181 if len(RH) == 1:
182 return RH[0]
183 else:
184 return RH