Repository navigation
Power spectra plots #170
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Power spectra plots #170
Changes from all commits
Commits
Show all changes
24 commits
Select commit
Hold shift + click to select a range
d40c905
add power-spectra QC diagnostic
frazane bb3a148
Merge remote-tracking branch 'origin/main' into feat/power-spectra
frazane 5775803
merge main into feat/power-spectra
frazane 37ccf15
merge origin/main into feat/power-spectra
frazane 2c5e0d1
read spectra config from optional experiment block
frazane 38c9197
fix xr.dataset typo in compute_source_spectra docstring
frazane 069f51d
move spectra plotting to spectra.plot
frazane 3e2989c
allow any loadable param as spectra variable, default to tot_prec1
frazane 0b48cf0
propagate nan when aggregating spectra over init times
frazane a4fd7c5
use shared experiment participants for spectra
frazane 65469ac
make spectrum nan when an input field has missing values
frazane b999f2c
read sal config from optional experiment block
frazane 2bec025
document that tot_prec1 spectra need hourly steps
frazane 940bb11
track spectra settings as rule params
frazane 5395bec
pass the baseline member to the spectra loader
frazane de45a6c
load only the requested spectra lead times
frazane 27f7a57
check spectra lead times against every participant
frazane b9795df
require unique labels across runs and baselines
frazane da06001
fall back to run id for unlabelled spectra runs
frazane d3d606a
reject spectra sources that are not on an icon native grid
frazane 0aa2592
fail early when spectra init_hours match no date
frazane 333d15e
reserve memory-proportional cores for spectra jobs
frazane 845b978
move regridding out of spectra into a top-level regrid package
frazane d075b18
drop mislabelled cerra reference line from spectra plots
frazane File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,92 @@ | ||
| """ICON native (unstructured) -> regular rotlatlon regridding via iconremap RBF | ||
| weights. The weights file is selected at runtime from the native point count. | ||
|
|
||
| Shared by the spectra diagnostic and usable elsewhere (e.g. plotting).""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from functools import lru_cache | ||
| from pathlib import Path | ||
|
|
||
| import numpy as np | ||
| import scipy.sparse | ||
| import xarray as xr | ||
|
|
||
| KM_PER_DEG = 111.2 | ||
|
|
||
| # Hardcoded iconremap RBF weight files on Balfrin, keyed by native cell count. | ||
| # CH1 native count is 1,147,980 (per the icon-power-spectra skill). | ||
| # TODO confirm CH2 count (see plan Task 2 Step 1). | ||
| _WEIGHTS_ROOT = Path("/store_new/mch/msopr/icon_workflow_2/iconremap-weights") | ||
| CH1_NPOINTS = 1_147_980 # confirmed vs ICON-CH1-EPS native T_2M field | ||
| CH2_NPOINTS = 283_876 # confirmed vs ICON-CH2-EPS native T_2M field | ||
| ICON_WEIGHTS = { | ||
| CH1_NPOINTS: _WEIGHTS_ROOT / "icon-ch1-eps-rotlatlon.nc", | ||
| CH2_NPOINTS: _WEIGHTS_ROOT / "icon-ch2-eps-rotlatlon.nc", | ||
| } | ||
| # TODO: prefer uuidOfHGrid as the registry key if point counts ever collide. | ||
| # TODO: eventually consume precomputed .npz sparse matrices directly. | ||
|
|
||
|
|
||
| def weights_path_for_npoints(npoints: int) -> Path: | ||
| """Resolve the iconremap weights file for a native grid of `npoints` cells.""" | ||
| try: | ||
| return ICON_WEIGHTS[npoints] | ||
| except KeyError as exc: | ||
| raise ValueError( | ||
| f"No iconremap weights registered for native grid with {npoints} " | ||
| f"cells. Power spectra need fields on the ICON-CH1 ({CH1_NPOINTS} cells) " | ||
| f"or ICON-CH2 ({CH2_NPOINTS} cells) native grid." | ||
| ) from exc | ||
|
|
||
|
|
||
| def build_truncation_matrix(iconremap_nc, n_source): | ||
| """Sparse native(n_source) -> regular(ny*nx) matrix from iconremap weights.""" | ||
| with xr.open_dataset(iconremap_nc) as ds: | ||
| idx = ds["rbf_B_glbidx"].values | ||
| wgt = ds["rbf_B_wgt"].values | ||
| nx, ny = int(ds.attrs["nx"]), int(ds.attrs["ny"]) | ||
| npoints = idx.shape[-1] | ||
| idx = idx.reshape(ny * nx, npoints) | ||
| wgt = wgt.reshape(ny * nx, npoints) | ||
| n_targets = ny * nx | ||
|
|
||
| # iconremap indices are 0-based and used as-is (cf. meteodata-lab's | ||
| # `_icon2regular`, which indexes with `np.take(field, indices)`). | ||
| rows = np.broadcast_to(np.arange(n_targets)[:, None], idx.shape).ravel() | ||
| cols = idx.ravel() | ||
| vals = wgt.ravel().astype(np.float32) | ||
|
|
||
| M = scipy.sparse.coo_matrix( | ||
| (vals, (rows, cols)), shape=(n_targets, n_source) | ||
| ).tocsr() | ||
| return M, (ny, nx) | ||
|
|
||
|
|
||
| def grid_geometry(iconremap_nc): | ||
| """(ny, nx, dx_km) of the regular target grid from the iconremap weights.""" | ||
| with xr.open_dataset(iconremap_nc) as ds: | ||
| return ( | ||
| int(ds.attrs["ny"]), | ||
| int(ds.attrs["nx"]), | ||
| float(ds.attrs["dx"]) * KM_PER_DEG, | ||
| ) | ||
|
|
||
|
|
||
| def regrid(native_1d, matrix, ny, nx): | ||
| """Apply a truncation matrix to a native field -> (ny, nx) regular array.""" | ||
| return (matrix @ np.asarray(native_1d)).reshape(ny, nx) | ||
|
|
||
|
|
||
| @lru_cache(maxsize=4) | ||
| def load_regridder(npoints: int): | ||
| """Cached regridder for a native grid: (matrix, ny, nx, dx_km).""" | ||
| weights = weights_path_for_npoints(npoints) | ||
| matrix, (ny, nx) = build_truncation_matrix(weights, n_source=npoints) | ||
| g_ny, g_nx, dx_km = grid_geometry(weights) | ||
| if (g_ny, g_nx) != (ny, nx): | ||
| raise ValueError( | ||
| f"Grid geometry mismatch for {npoints} cells: " | ||
| f"matrix target {(ny, nx)} vs grid_geometry {(g_ny, g_nx)}." | ||
| ) | ||
| return matrix, ny, nx, dx_km |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| """Variance power-spectra diagnostics for evalml. | ||
|
|
||
| Public surface re-exported for convenience; submodules can also be imported | ||
| directly. The spectral `core` is numpy-only and reusable for any model. | ||
| """ | ||
|
|
||
| from spectra import compute, core, io, plot # noqa: F401 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| """Orchestration: per-source spectra computation and over-init aggregation. | ||
| Defines the spectra.nc schema.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import numpy as np | ||
| import xarray as xr | ||
|
|
||
| import regrid | ||
| from spectra import core, io | ||
|
|
||
|
|
||
| def compute_source_spectra(ds: xr.Dataset, variables, lead_times, method, label): | ||
| """Compute spectra for one source (already-loaded native dataset). | ||
|
|
||
| Returns an xr.Dataset with dims (variable, leadtime, wavenumber) and a shared | ||
| `wavelength` coordinate. The native grid is detected from the field length. | ||
| """ | ||
| if not variables or not lead_times: | ||
| raise ValueError( | ||
| "compute_source_spectra requires non-empty `variables` and `lead_times`." | ||
| ) | ||
| spectrum_fn = core.SPECTRUM_FUNCS.get(method) | ||
| if spectrum_fn is None: | ||
| raise ValueError( | ||
| f"Unknown spectrum method {method!r}. Valid: {list(core.SPECTRUM_FUNCS)}." | ||
| ) | ||
| npoints = io.native_field( | ||
| ds, io.required_params(variables)[0], lead_times[0] | ||
| ).shape[0] | ||
| matrix, ny, nx, dx_km = regrid.load_regridder(npoints) | ||
|
|
||
| wavelength = None | ||
| power = np.full((len(variables), len(lead_times), min(ny, nx) // 2), np.nan) | ||
| for vi, var in enumerate(variables): | ||
| for li, step in enumerate(lead_times): | ||
| comps, factor = io.native_components(ds, var, step) | ||
| grids = [regrid.regrid(c, matrix, ny, nx) for c in comps] | ||
| wl, p = core.combined_spectrum(spectrum_fn, grids, dx_km, factor=factor) | ||
| wavelength = wl | ||
| power[vi, li, :] = p | ||
|
|
||
| return xr.Dataset( | ||
| {"power": (("variable", "leadtime", "wavenumber"), power)}, | ||
| coords={ | ||
| "variable": list(variables), | ||
| "leadtime": list(lead_times), | ||
| "wavenumber": np.arange(power.shape[-1]), | ||
| "wavelength": ("wavenumber", wavelength), | ||
| }, | ||
| attrs={ | ||
| "dx_km": float(dx_km), | ||
| "npoints": int(npoints), | ||
| "label": label, | ||
| "method": method, | ||
| }, | ||
| ) | ||
|
|
||
|
|
||
| def aggregate_spectra(spectra_files) -> xr.Dataset: | ||
| """Average power over init times. All inputs share one grid. | ||
|
|
||
| NaN propagates: a value missing in any input is missing in the average. | ||
| """ | ||
| datasets = [xr.open_dataset(f) for f in spectra_files] | ||
| try: | ||
| stacked = xr.concat(datasets, dim="init") | ||
| agg = stacked.mean(dim="init", skipna=False) | ||
| agg.attrs = datasets[0].attrs | ||
| agg["wavelength"] = datasets[0]["wavelength"].copy() | ||
| agg.load() | ||
| finally: | ||
| for ds in datasets: | ||
| ds.close() | ||
| return agg | ||
|
frazane marked this conversation as resolved.
Resolved
|
||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.