Skip to content

Calculate power spectra using a separate operator power_spectrum#1872

Draft
cehalliwell wants to merge 27 commits intomainfrom
215_regional_power_spectra_new_operator_DCT
Draft

Calculate power spectra using a separate operator power_spectrum#1872
cehalliwell wants to merge 27 commits intomainfrom
215_regional_power_spectra_new_operator_DCT

Conversation

@cehalliwell
Copy link
Contributor

@cehalliwell cehalliwell commented Jan 8, 2026

Follows on from #1765

The power spectra is currently calculated and plotted in the main plot.py operator. However, this method does not allow aggregation of the power spectra (e.g. mean). The power spectra are calculated in a separate operator rather than in plot.py and the resulting power spectra plotted using the line series method in plot.py. After this, aggregation will be possible using the collapse operator.

Move the existing functions and code for calculating the power spectra into a separate operator, remove the existing power spectra code from plot.py and adjust the line series plotting code to allow for line plotting other than timeseries.

Contribution checklist

Aim to have all relevant checks ticked off before merging. See the developer's guide for more detail.

  • Documentation has been updated to reflect change.
  • New code has tests, and affected old tests have been updated.
  • All tests and CI checks pass.
  • Ensured the pull request title is descriptive.
  • Conda lock files have been updated if dependencies have changed.
  • Attributed any Generative AI, such as GitHub Copilot, used in this PR.
  • Marked the PR as ready to review.

@cehalliwell cehalliwell self-assigned this Jan 8, 2026
@github-actions
Copy link
Contributor

github-actions bot commented Jan 26, 2026

Coverage

@cehalliwell
Copy link
Contributor Author

As part of the restructuring, a wrapper is used for the power spectra code within power_spectrum operator. The field for which the power spectrum is to be calculated is split up into a cube for each model and time and power spectrum is then calculated for each before combining into one cube ahead of plotting. This is done to retain the model_name attribute for different models.

The generic function to plot a line other than a time series is called _plot_and_save_line_1D. It has been tested for power spectrum (called from plot_line_series with series_coordinate "frequency") but has not be tested for any other series_coordinate.

Microsoft Copilot was used to design the wrapper and tidy up _power_spectrum operator.

@cehalliwell
Copy link
Contributor Author

The tests from the original power spectrum code have been assessed and moved to test_power_spectrum, or within the plot_line_series code in test_plot or deleted as necessary. Extra tests have also been added for options in plot_line_series to check the options when series_coordinate is not time.

expand on explanatory text.
clarify description of internal function.
changing the code to calculate power_spectral_density instead of power spectrum. This makes it comparable between models with different resolutions and correctly reflects distribution of power per unit frequency
@Sylviabohnenstengel
Copy link
Member

I have suggested to calculate power_spectral_density instead of a power spectrum and modified the code in power_spectrum.py in def _DCT_ps(y_3d):. So this does not aligned completely with your reference anymore then. The reason I am suggesting this is: we intend to compare models vs observations including models vs model with differing resolutions. Not at the moment, but the sampling frequency might be different when comparing different CSET runs with each other i.e. differing number of data points (different length of data). A Power Spectrum's magnitude scales with length of the time series (N), sampling frequency (fs) and preprocessing choices i.e. It is not invariant to regridding (low pass filtering) or different grid resolutions. To make them comparable I suggest to plot power per spectral coefficient or power per wavenumber instead of total power per bin which would increase with more datapoints.

updated name from power_spectrum to power_spectral_density in line with changes moving from power spectrum calculation to power_spectral_density calculation
adding further info to the recipe and website output about how power spectral density enables comparison between models with different resolutions etc.
adding further explanation on power spectral density and how to interpret figures. Pulled into website.
Copy link
Member

@Sylviabohnenstengel Sylviabohnenstengel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

having added a few changes to make results comparable between different resoltuions, different number of data points, different time lengths if we were to compare results from different CSET runs. Happy to approve once James is happy with tests. Have you tested the runs with 2 ensembles?

)

# Ensure cube has a realisation coordinate by creating and adding to cube
realization_coord = iris.coords.AuxCoord(0, standard_name="realization", units="1")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For disucssion with @jfrost-mo Should the realisation component you add always be 0 or would this need to be dynamic? I assume that it is ok as the only case we might not have one is when loading a deterministic model and would want it 0 in that case.

# Sum up elements matching k
mask_k = np.where((alpha_matrix >= alpha) & (alpha_matrix < alpha_p1))
# Divide by number of coefficients in bin to get power spectral density insetad of power spectrum
ps_array[t, k - 1] = np.sum(sigma_2[mask_k]) / len(mask_k[0])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have suggested to calculate power_spectral_density instead of a power spectrum. So this does not aligned completely with your reference anymore then. The reason I am suggesting this is: 1. we intend to compare models vs observations including models vs model with differing resolutions. Not at the moment, but the sampling frequency might be different when comparing different CSET runs with each other i.e. differing number of data points (different length of data). A Power Spectrum's magnitude scales with length of the time series (N), sampling frequency (fs) and preprocessing choices i.e. It is not invariant to regridding (low pass filtering) or different grid resolutions. To make them comparable I suggest to plot power per spectral coefficient or power per wavenumber instead of total power per bin which would increase with more datapoints.

@Sylviabohnenstengel
Copy link
Member

@cehalliwell A further minor change as we are currently calculating power spectral density over a domain and not a timeseries we are plotting as function of wavenumbe rand not frequency. So in power_spectrum.py in def _power_spectrum it might be clearer to rename freq_coord = iris.coords.DimCoord(freqs, long_name="frequency", units="1") to freq_coord = iris.coords.DimCoord(freqs, long_name="wavenumber", units="1")

@Sylviabohnenstengel
Copy link
Member

power spectral density calculated instead of power spectrum.
Todo: check that the power spectral density does not increase in amplitude for longer runs to ensure it is correctly normalised.
compare against old results from @cehalliwell for pure power spectrum
include maybe a switch for user in rose gui to decide if plotting based on spatial wavenumber or wavelength
different ticket: include frequency for temporal power density spectra.

Screenshot 2026-02-05 173605 Screenshot 2026-02-05 173615

@cehalliwell cehalliwell marked this pull request as draft February 16, 2026 12:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants