Spectral Wavenumber Validation¶
This page documents the post-inference validation procedure used by
depth_recon.inference.export_wavenumber_spectra and covered by
tests/test_wavenumber_spectra.py. The diagnostic compares the spatial
structure of DepthDif predictions against GLORYS reference fields, and when
available against surface observations, by estimating isotropic 2D wavenumber
spectra on the same selected patch windows.
Scope¶
The analysis is a spatial spectrum diagnostic, not a replacement for pixelwise MAE or profile error validation. It answers whether generated fields preserve energy at comparable horizontal scales:
- high wavenumber / short wavelength bins test small-scale texture
- low wavenumber / long wavelength bins test basin-scale smoothness
- prediction-to-GLORYS spectral ratios show scale-dependent smoothing or excess variance
Version 1 analyzes temperature and salinity fields directly in physical units. It does not estimate buoyancy spectra, internal-wave partitions, or frequency-wavenumber spectra.
Inputs¶
For each discovered single-variable run, the exporter reads:
selected_patches.csvfor patch footprints and grid offsets- prediction GeoTIFF depth exports
- GLORYS GeoTIFF depth exports, or packaged GLORYS source rasters as a fallback
- surface observation rasters for the variable when packaged source data exists
(
OSTIA analysed_sstfor temperature,SSS sosfor salinity) world_oceans.geojsonregions for basin-level aggregation
Integer source rasters with stretch tags are decoded before spectral analysis. For temperature rasters stored in Kelvin, decoded values are shifted to Celsius:
By default, patch windows with nodata or NaN are included. The exporter fits
the detrending plane on finite pixels, then zero-fills missing residuals before
the Hann-windowed FFT so prediction and GLORYS use the same land/coast support.
Pass --require-complete-patches to keep only complete finite patch windows.
Patch Geometry¶
Each selected patch row defines lon-lat bounds
(lon0, lon1, lat0, lat1) and a raster window. Pixel sizes are approximated in
kilometers from the patch center latitude:
where W and H are the patch width and height in pixels and
phi_c = (lat_0 + lat_1) / 2.
Detrending¶
For one patch field X(i, j), with row index i and column index j, a planar
trend is fitted by least squares:
Omega is the finite-pixel support. The detrended residual is:
This removes patch-scale mean and linear gradients before measuring spectral power, so the diagnostic emphasizes resolved spatial variability rather than a tilted local background.
Windowed FFT¶
A separable 2D Hann window is applied:
The windowed field is:
The 2D discrete Fourier transform follows np.fft.fft2:
Power is normalized by Hann-window energy:
This normalization keeps spectra from same-sized windows comparable after tapering.
Radial Wavelength Binning¶
FFT frequencies are converted to cycles per kilometer:
The isotropic radial wavenumber and wavelength are:
The zero-frequency term, where k_r = 0, is excluded. Wavelength-bin edges are
log-spaced:
with default lambda_min = 30 km, lambda_max = 1000 km, and Q = 32. The
displayed bin center is the geometric mean:
Patch spectral power for bin q is first computed as the mean Fourier power
over all coefficients whose wavelengths fall inside the bin:
The exporter also converts each wavelength bin to its horizontal-wavenumber width in cycles per kilometer:
and reports a one-dimensional radial PSD estimate:
Bins with no FFT coefficients are recorded as missing.
Basin Assignment¶
Each patch footprint is intersected with the ocean-region polygons. A patch is assigned to the basin with maximum overlap only when:
The default threshold is 0.30 and is controlled by --basin-overlap-threshold.
Patches below the threshold remain in All Oceans but are excluded from named-basin aggregates.
Aggregation¶
Each accepted patch spectrum is stored with variable, layer, date, season, basin, and depth metadata. Aggregation groups spectra by:
- variable
- layer: prediction, GLORYS, or surface observation
- depth
- basin scope:
All Oceansplus named basins - period: year, season, or month
For group G, the mean spectrum is:
where G_q contains spectra with finite power in bin q and N_{G,q} is the
finite count. The exported spectrum_count reports how many patch spectra
contributed at least one finite bin to the group.
The dashboard derives prediction-vs-GLORYS scale diagnostics from the aggregated spectra:
Values below 1 in R_q indicate that predictions contain less variance than
GLORYS at that wavelength. Values above 1 indicate excess variance.
Interpretation¶
The dashboard displays horizontal wavenumber in cycles per kilometer (cpkm)
on a log axis that increases left-to-right by default. Use the X Axis selector
to switch the display to wavelength in kilometers (km), also on an increasing
left-to-right log axis. Hover labels report the equivalent wavelength. Because
each patch is detrended before the FFT, the spectra describe residual spatial
variability within patches, not the patch mean or a local linear gradient.
The dashboard plots psd_mean, which is power_mean divided by the
horizontal-wavenumber bin width. The paper-style velocity unit
m^2/(s^4 cpkm) still does not apply because our fields are temperature and
salinity rather than that velocity-derived quantity. The dashboard labels the
y-axis as PSD [degC^2/cpkm] for temperature and PSD [salinity^2/cpkm] for
salinity. A good prediction spectrum tracks the GLORYS spectrum with a similar
level and slope across the wavelength range. A clearly lower prediction curve
means the model is too smooth at those scales. A clearly higher curve means it
is adding too much variance, often visible as noisy or speckled texture at
short wavelengths.
The prediction/GLORYS ratio is the most direct quality signal:
R_q = 1is ideal agreement at that wavelength.- roughly
0.8to1.25is usually a close scale match. - values around
0.5to0.8indicate moderate smoothing. - values below
0.5indicate severe loss of variance at that scale. - values around
1.25to2indicate excess variance. - values above
2indicate strong over-energizing or likely artifacts.
These ranges are rules of thumb, not pass/fail limits. They should be judged
more strictly where many spectra contribute and more cautiously where
spectrum_count is small or bins contain few FFT coefficients.
For dashboard metrics, relative_bias = 0 is ideal, negative values mean
smoothing, and positive values mean excess variance. Ratio and relative bias are
unitless; relative bias is plotted as a fraction, so 0.25 means +25%. The
difference metric is in PSD units and is best used to see where the
absolute mismatch is largest. The magnitude is not directly comparable across
variables or depths. The summary cards use the currently selected metric:
High freq averages the short-wavelength range up to about
100 km, and Large scale averages the long-wavelength range from about 300 km
upward. Slope diff is the fitted log-power-vs-log-wavelength slope for
prediction minus GLORYS. Values near zero are best. Positive values usually
mean the prediction is too dominated by large scales; negative values usually
mean relatively too much small-scale power.
Good diagnostics therefore look like:
- prediction, GLORYS, and OSTIA/SSS curves nearly parallel and close together
- prediction/GLORYS ratio near 1 across both short and long wavelengths
- relative bias near 0 without a persistent sign across wavelengths
- high
spectrum_countfor the selected basin and period
Bad diagnostics commonly look like:
- low short-wavelength ratio, showing over-smoothed fronts or missing eddies
- high short-wavelength ratio, showing noisy or speckled predictions
- low long-wavelength ratio, showing basin-scale structure is muted
- high long-wavelength ratio, showing broad false gradients or large blobs
- large slope differences, showing the model has the wrong scale balance
Test Coverage¶
tests/test_wavenumber_spectra.py validates the scientific and export
assumptions with small deterministic fixtures:
- exact planar fields detrend to numerical zero
- a sinusoidal field has its Hann-windowed FFT peak in the expected wavelength bin
- incomplete patches are skipped by default
- raster nodata is converted to
NaN - stretched uint8 temperature rasters decode to Celsius
- basin assignment respects the overlap threshold
- paired and temporal run discovery expands the expected run roots
- the full exporter writes patch spectra, aggregated spectra, summary JSON, plots, basin JSON, and static dashboard assets
- dashboard generation can be disabled with
--no-dashboard - the hosted spectral dashboard exposes the expected controls and links from the analysis landing page
Main Outputs¶
The procedure writes:
patch_spectra.npz: dense patch-by-wavelength spectrum matrix and wavelength edges/centerspatch_spectra_records.csv: one metadata row per accepted patch spectrumaggregated_spectra.csv: grouped spectra withpower_mean,psd_mean, and wavenumber-bin metadata used by plots and dashboardssummary.json: run, binning, skip-count, and artifact metadataplots/*.png: default log-log seasonal/yearly spectraspectral-config.json,basin-map.geojson,basins/*.json, and copied dashboard static files when dashboard export is enabled
Typical command:
/work/envs/depth/bin/python -m depth_recon.inference.export_wavenumber_spectra \
--run-dir inference/outputs/global_variables_2018_W25_v2 \
--include-temporal-runs \
--variables temperature salinity \
--output-dir inference/outputs/global_variables_2018_W25_v2/wavenumber_spectra