Coverage for src/CSET/operators/_spectra.py: 100%
23 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-17 15:44 +0000
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-17 15:44 +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"""Functions to support calculation of power spectra."""
17import numpy as np
18import scipy.fft as fft
21def DCT_ps(y_3d):
22 """Calculate power spectra for regional domains.
24 Parameters
25 ----------
26 y_3d: 3D array
27 3 dimensional array to calculate spectrum for.
28 (2D field data with 3rd dimension of time)
30 Returns: ps_array
31 Array of power spectra values calculated for input field (for each time)
33 Method for regional domains:
34 Calculate power spectra over limited area domain using Discrete Cosine Transform (DCT)
35 as described in Denis et al 2002 [Denis_etal_2002]_.
37 References
38 ----------
39 .. [Denis_etal_2002] Bertrand Denis, Jean Côté and René Laprise (2002)
40 "Spectral Decomposition of Two-Dimensional Atmospheric Fields on
41 Limited-Area Domains Using the Discrete Cosine Transform (DCT)"
42 Monthly Weather Review, Vol. 130, 1812-1828
43 doi: https://doi.org/10.1175/1520-0493(2002)130<1812:SDOTDA>2.0.CO;2
44 """
45 Nt, Ny, Nx = y_3d.shape
47 # Max coefficient
48 Nmin = min(Nx - 1, Ny - 1)
50 # Create alpha matrix (of wavenumbers)
51 alpha_matrix = create_alpha_matrix(Ny, Nx)
53 # Prepare output array
54 ps_array = np.zeros((Nt, Nmin))
56 # Loop over time to get spectrum for each time.
57 for t in range(Nt):
58 y_2d = y_3d[t]
60 # Apply 2D DCT to transform y_3d[t] from physical space to spectral space.
61 # fkk is a 2D array of DCT coefficients, representing the amplitudes of
62 # cosine basis functions at different spatial frequencies.
64 # normalise spectrum to allow comparison between models.
65 fkk = fft.dctn(y_2d, norm="ortho")
67 # Normalise fkk
68 fkk = fkk / np.sqrt(Ny * Nx)
70 # calculate variance of spectral coefficient
71 sigma_2 = fkk**2 / Nx / Ny
73 # Group ellipses of alphas into the same wavenumber k/Nmin
74 for k in range(1, Nmin + 1):
75 alpha = k / Nmin
76 alpha_p1 = (k + 1) / Nmin
78 # Sum up elements matching k
79 mask_k = np.where((alpha_matrix >= alpha) & (alpha_matrix < alpha_p1))
80 ps_array[t, k - 1] = np.sum(sigma_2[mask_k])
82 return ps_array
85def create_alpha_matrix(Ny, Nx):
86 """Construct an array of 2D wavenumbers from 2D wavenumber pair.
88 Parameters
89 ----------
90 Ny, Nx:
91 Dimensions of the 2D field for which the power spectra is calculated. Used to
92 create the array of 2D wavenumbers. Each Ny, Nx pair is associated with a
93 single-scale parameter.
95 Returns: alpha_matrix
96 normalisation of 2D wavenumber axes, transforming the spectral domain into
97 an elliptic coordinate system.
99 """
100 # Create x_indices: each row is [1, 2, ..., Nx]
101 x_indices = np.tile(np.arange(1, Nx + 1), (Ny, 1))
103 # Create y_indices: each column is [1, 2, ..., Ny]
104 y_indices = np.tile(np.arange(1, Ny + 1).reshape(Ny, 1), (1, Nx))
106 # Compute alpha_matrix
107 alpha_matrix = np.sqrt((x_indices**2) / Nx**2 + (y_indices**2) / Ny**2)
109 return alpha_matrix