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

41 statements  

« prev     ^ index     » next       coverage.py v7.15.0, created at 2026-07-09 15:26 +0000

1# © Crown copyright, Met Office (2022-2025) 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 to calculate kinematic heat fluxes from covariances.""" 

16 

17import iris 

18import iris.cube 

19from cf_units import Unit 

20from iris.cube import Cube, CubeList 

21 

22from CSET._common import iter_maybe 

23from CSET.operators._atmospheric_constants import CPD, LV, RD 

24 

25 

26def sensible_heat_flux_from_covariance( 

27 wt_flux: iris.cube.Cube | iris.cube.CubeList, 

28 air_temperature: iris.cube.Cube | iris.cube.CubeList, 

29 pressure: iris.cube.Cube | iris.cube.CubeList, 

30) -> iris.cube.CubeList: 

31 """ 

32 Convert turbulent temperature covariance into sensible heat flux. 

33 

34 Computes: 

35 SHF = rho * CPD * w'T' 

36 

37 where: 

38 rho = pressure / (RD * temperature) 

39 

40 Parameters 

41 ---------- 

42 wt_flux : iris.cube.Cube or iris.cube.CubeList 

43 Turbulent temperature covariance (w'T'). 

44 

45 air_temperature : iris.cube.Cube or iris.cube.CubeList 

46 Air temperature. 

47 

48 pressure : iris.cube.Cube or iris.cube.CubeList 

49 Air pressure. 

50 

51 Returns 

52 ------- 

53 iris.cube.CubeList 

54 Surface upward sensible heat flux cube(s). 

55 """ 

56 from cf_units import Unit 

57 

58 out = iris.cube.CubeList() 

59 for wt_cube, temp_cube, pres_cube in zip( 

60 iter_maybe(wt_flux), 

61 iter_maybe(air_temperature), 

62 iter_maybe(pressure), 

63 strict=True, 

64 ): 

65 # Unit conversions 

66 temp_K = temp_cube.copy() 

67 temp_K.convert_units("K") 

68 pres_Pa = pres_cube.copy() 

69 pres_Pa.convert_units("Pa") 

70 wt_cov = wt_cube.copy() 

71 

72 # Treat degC covariance numerically as K covariance 

73 if str(wt_cov.units) == "degC m s-1": 

74 wt_cov.units = Unit("K m s-1") 

75 

76 # Density and SHF 

77 rho_air = pres_Pa / (RD * temp_K) 

78 shf = CPD * rho_air * wt_cov 

79 

80 shf.units = Unit("W m-2") 

81 shf.rename("surface_upward_sensible_heat_flux") 

82 # Keep compatibility with existing tests/output 

83 shf.var_name = "surface_upward_sensible_heat_flux" 

84 

85 out.append(shf) 

86 

87 return out[0] if len(out) == 1 else out 

88 

89 

90def latent_heat_units( 

91 cubes: Cube | CubeList, 

92 **kwargs, 

93) -> Cube | CubeList: 

94 """ 

95 Convert covariance into latent heat flux units. 

96 

97 This operator converts any cube with units convertible to kg m-2 s-1 

98 (i.e. water mass flux) into latent heat flux (W m-2) by multiplying 

99 by a constant latent heat of vaporisation. 

100 

101 No attempt is made to distinguish between turbulent fluxes (e.g. w'q') 

102 and other water mass fluxes. This generalisation seems reasonable 

103 given that interpreting rainfall or dewfall, for example, as an 

104 equivalent heat flux is physically meaningful. 

105 

106 This function operates on one or more Iris cubes. Any cube with 

107 units convertible to mass flux (kg m-2 s-1) is multiplied by a 

108 constant latent heat of vaporisation to produce a latent heat flux. 

109 Cubes with incompatible, missing, or unknown units are passed through 

110 unchanged. 

111 

112 Parameters 

113 ---------- 

114 cubes : Cube or CubeList 

115 Input cube(s), typically containing w'q' covariance or other flux-like 

116 quantities. 

117 

118 **kwargs : dict 

119 Unused; accepted for interface consistency with other operators. 

120 

121 Returns 

122 ------- 

123 Cube or CubeList 

124 Output cube(s) where: 

125 - Cubes with units convertible to kg m-2 s-1 are converted to W m-2. 

126 - All other cubes are returned unchanged. 

127 - The return type matches the input type (single Cube or CubeList). 

128 

129 Notes 

130 ----- 

131 - The conversion uses a fixed latent heat of vaporisation: 

132 LV = 2.5 × 10^6 J kg-1 

133 - In reality, Lc varies with temperature (~5% variation between -20 °C 

134 and +40 °C). This dependency is currently neglected but could be 

135 included in future improvements. 

136 - This function does not attempt to identify specific variables; it relies 

137 solely on unit convertibility to determine applicability. 

138 """ 

139 REQUIRED_UNITS = Unit("kg m-2 s-1") 

140 OUTPUT_UNITS = Unit("W m-2") 

141 

142 out = iris.cube.CubeList() 

143 for cube in iter_maybe(cubes): 

144 # ACT ON MASS FLUXES 

145 if cube.units is None or cube.units.is_unknown(): 

146 out.append(cube) 

147 continue 

148 if not cube.units.is_convertible(REQUIRED_UNITS): 

149 # e.g. if UM LE or some other diagnostic — leave untouched 

150 out.append(cube) 

151 continue 

152 

153 cube_a = cube.copy() 

154 cube_a = cube_a * LV 

155 cube_a.units = cube.units * Unit("J kg-1") 

156 cube_a.convert_units(OUTPUT_UNITS) 

157 out.append(cube_a) 

158 

159 return out[0] if len(out) == 1 else out