Coverage for src/CSET/operators/fluxes.py: 100%
41 statements
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-10 13:27 +0000
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-10 13:27 +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.
15"""Operators to calculate kinematic heat fluxes from covariances."""
17import iris
18import iris.cube
19from cf_units import Unit
20from iris.cube import Cube, CubeList
22from CSET._common import iter_maybe
23from CSET.operators._atmospheric_constants import CPD, LV, RD
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.
34 Computes:
35 SHF = rho * CPD * w'T'
37 where:
38 rho = pressure / (RD * temperature)
40 Parameters
41 ----------
42 wt_flux : iris.cube.Cube or iris.cube.CubeList
43 Turbulent temperature covariance (w'T').
45 air_temperature : iris.cube.Cube or iris.cube.CubeList
46 Air temperature.
48 pressure : iris.cube.Cube or iris.cube.CubeList
49 Air pressure.
51 Returns
52 -------
53 iris.cube.CubeList
54 Surface upward sensible heat flux cube(s).
55 """
56 from cf_units import Unit
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()
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")
76 # Density and SHF
77 rho_air = pres_Pa / (RD * temp_K)
78 shf = CPD * rho_air * wt_cov
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"
85 out.append(shf)
87 return out[0] if len(out) == 1 else out
90def latent_heat_units(
91 cubes: Cube | CubeList,
92 **kwargs,
93) -> Cube | CubeList:
94 """
95 Convert covariance into latent heat flux units.
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.
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.
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.
112 Parameters
113 ----------
114 cubes : Cube or CubeList
115 Input cube(s), typically containing w'q' covariance or other flux-like
116 quantities.
118 **kwargs : dict
119 Unused; accepted for interface consistency with other operators.
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).
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")
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
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)
159 return out[0] if len(out) == 1 else out