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

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 

18 

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 

23 

24 

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 

39 

40 

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 

55 

56 

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 

73 

74 

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 

91 

92 

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 

115 

116 

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 

139 

140 

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 

162 

163 

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