Coverage for src/CSET/operators/ensembles.py: 100%
27 statements
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-05 21:08 +0000
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-05 21:08 +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"""
16A module containing different diagnostics for ensembles.
18The diagnostics here are applicable to ensembles and apply generally to
19ensembles. They are not just limited to considering ensemble spread.
20"""
22import iris
23import iris.cube
24import numpy as np
27def DKE(u: iris.cube.Cube, v: iris.cube.Cube) -> iris.cube.Cube:
28 r"""Calculate the Difference Kinetic Energy (DKE).
30 Parameters
31 ----------
32 u: iris.cube.Cube
33 Iris cube of the u component of the wind field for all members. Must
34 include a realization coordinate; realization must be the first
35 coordinate.
36 v: iris.cube.Cube
37 Iris cube of the v component of the wind field same format as u.
39 Returns
40 -------
41 DKE: iris.cube.Cube
42 An iris cube of the DKE for each of the control - member comparisons.
44 Notes
45 -----
46 The Difference Kinetic Energy, or DKE was first used in an ensemble sense
47 by [Zhangetal2002]_. It is calculated as
49 .. math:: \frac{1}{2}u'u' + \frac{1}{2}v'v'
51 where
53 .. math:: x' = x_{control} - x_{perturbed}
55 for each member of the ensemble.
57 The DKE is used to show links between the perturbation growth (growth of
58 ensemble spread) or errors with dynamical features. It can be particularly
59 useful in understanding differences in physical mechanisms between ensemble
60 members. The larger the DKE the greater the difference between the two
61 members being considered.
63 The DKE can be viewed as a domain average, horizontal integration (to
64 produce profiles), or vertical integration/subsampling (to provide spatial maps).
65 Furthermore, weightings based on volume or mass can be applied to these
66 integrations. However, initially the DKE per unit mass is implemented here.
68 The DKE is often considered in the form of power spectra to identify the
69 presence of upscale error growth or the dominant scales of error growth. It
70 is often plotted on a logarithmic scale.
72 References
73 ----------
74 .. [Zhangetal2002] Zhang, F., Snyder, C., and Rotunno, R., (2002)
75 "Mesoscale Predictability of the 'Surprise' Snowstorm of 24-25 January
76 2000." Monthly Weather Review, vol. 130, 1617-1632,
77 doi: 10.1175/1520-0493(2002)130<1617:MPOTSS>2.0.CO;2
79 Examples
80 --------
81 >>> DKE = ensembles.DKE(u, v)
82 """
83 for cube in [u, v]:
84 # Use dim_map to store the dimensions and check the realization
85 # coordinate is the first coordinate.
86 dim_map = {}
87 for coord in cube.dim_coords:
88 dim_index = cube.coord_dims(coord.name())[0]
89 dim_map[coord.name()] = dim_index
90 if "realization" not in dim_map:
91 raise ValueError("Cube should have a realization coordinate.")
92 if not dim_map["realization"] == 0:
93 raise ValueError("Realization should be the first coordinate in cube.")
95 # Check cubes are the same shape.
96 if u.shape != v.shape:
97 raise ValueError("Cubes should be the same shape.")
99 # Check coordinates are identical.
100 for coord_u, coord_v in zip(u.dim_coords, v.dim_coords, strict=True):
101 if not u.coord(coord_u.name()) == v.coord(coord_v.name()):
102 raise ValueError(
103 f"u and v should have matching coordinates for {coord_u.name()}."
104 )
105 # Define control member and perturbed members.
106 u_ctrl = u[np.where(u.coord("realization").points[:] == 0)[0][0], :]
107 u_mem = u[np.where(u.coord("realization").points[:] != 0)[0][:], :]
108 v_ctrl = v[np.where(v.coord("realization").points[:] == 0)[0][0], :]
109 v_mem = v[np.where(v.coord("realization").points[:] != 0)[0][:], :]
111 # Calculate the DKE
112 DKE = u_mem.copy()
113 DKE.data[:] = 0.0
114 DKE.data = (
115 0.5 * (u_ctrl.data - u_mem.data) ** 2 + 0.5 * (v_ctrl.data - v_mem.data) ** 2
116 )
117 DKE.rename("Difference Kinetic Energy")
118 return DKE