Skip to content
Merged
9 changes: 9 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,15 @@ The changes listed in this file are categorised as follows:
[Unreleased]
---------------------

### Changed

- Now possibly to send variable length temperature scaling timeseries, fixed noise generator for wrong ordering of base data dimensions

### Fixed

- Variable length timeseries now works also when don't have "year" as time dimension.
- Fixes to generate annual and monthly gridded ensembles with unified noise and preserving more of the variance.


[Version 1.6.0]
-----------------------------
Expand Down
4 changes: 4 additions & 0 deletions src/meteor/ensemble_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
climate variables, spatial aggregations, and impact metrics.
"""

from pathlib import Path

import xarray as xr


Expand Down Expand Up @@ -177,6 +179,8 @@ def to_netcdf(self, path, include_impacts=True):
combined.attrs[key] = value

# Save to netCDF
path = Path(path)
path.parent.mkdir(exist_ok=True, parents=True)
combined.to_netcdf(path)
print(f"✅ Saved ensemble to {path}")

Expand Down
76 changes: 69 additions & 7 deletions src/meteor/geo_data_utils.py
Comment thread
maritsandstad marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -1,4 +1,21 @@
"""Utility functions for handling geographic data."""
"""
Geographic data utilities for METEOR's gridded climate fields.

This module groups the helpers METEOR uses to navigate, weight, and
spatially aggregate xarray datasets on (lat, lon, time) grids:

- Coordinate discovery: :func:`get_time_name`, :func:`get_lat_name`,
:func:`get_lon_name`, :func:`get_year_series`.
- Area weighting: :func:`get_weights_for_ds`,
:func:`apply_weights_and_do_spatial_mean`.
- Spatial aggregation: :func:`global_mean`, :func:`regional_mean` (AR6
reference regions via ``regionmask``), :func:`extract_point`,
:func:`create_region_mask` (custom bounding boxes or precomputed masks),
:func:`list_ar6_regions`.
- Time-axis helpers used by the pattern-scaling pipeline:
:func:`extend_temeperature_anomaly_timeseries_for_scaling`,
:func:`find_time_dim_and_cut`.
"""

import logging

Expand Down Expand Up @@ -35,6 +52,18 @@ def get_time_name(ds):
raise RuntimeError("Couldn't find a time coordinate")


def get_year_series(ds: xr.Dataset) -> np.ndarray:
"""Extract a DataFrame of just the year columns from a larger DataFrame."""
time_name = get_time_name(ds)
if time_name == "year":
return ds[time_name].values
if time_name == "time":
return ds[time_name].dt.year.values
if time_name == "month":
return ds[time_name].dt.year.values + (ds[time_name].dt.month.values - 1) / 12.0
raise ValueError(f"Unexpected time dimension name '{time_name}'")


def get_lat_name(ds):
"""
Get name of latitude dimension
Expand Down Expand Up @@ -509,23 +538,56 @@ def extend_temeperature_anomaly_timeseries_for_scaling(
temperature_input_anomaly : xarray.DataArray
Temperature anomaly time series used for scaling (relative to base year)
"""
years_prediction = annual_temp_prediction_anomaly_gm[
get_time_name(annual_temp_prediction_anomaly_gm)
].values
years_input = temperature_input_anomaly[
get_time_name(temperature_input_anomaly)
].values
years_prediction = get_year_series(annual_temp_prediction_anomaly_gm)
years_input = get_year_series(temperature_input_anomaly)
temperature_input_anomaly_extended = np.copy(
annual_temp_prediction_anomaly_gm.values
)
input_time_name = get_time_name(temperature_input_anomaly)
if input_time_name != "year":
temperature_input_anomaly = temperature_input_anomaly.rename(
{input_time_name: "year"}
)
temperature_input_anomaly.coords["year"] = years_input
for i, year in enumerate(years_prediction):
if year in years_input:
temperature_input_anomaly_extended[i] = temperature_input_anomaly.sel(
{get_time_name(temperature_input_anomaly): year}
).values

temperature_input_anomaly_extended = xr.DataArray(
data=temperature_input_anomaly_extended,
coords={get_time_name(annual_temp_prediction_anomaly_gm): years_prediction},
dims=annual_temp_prediction_anomaly_gm.dims,
)

return temperature_input_anomaly_extended


def find_time_dim_and_cut(base_array, n_time, n_lat, n_lon):
"""
Base_array should have dimensions (time, lat, lon), but the ordering can be off
"""
base_shape = base_array.shape
lat_dim = None
lon_dim = None
time_dim = 0
for i, dim_size in enumerate(base_shape):
if dim_size == n_lat and lat_dim is None:
lat_dim = i
elif dim_size == n_lon:
lon_dim = i
else:
time_dim = i
if lat_dim is None or lon_dim is None:
raise ValueError(
f"Couldn't find lat/lon dimensions in base array with shape {base_shape}"
)
if base_shape[time_dim] != n_time:
if time_dim == 0:
base_array = base_array[:n_time, :, :]
elif time_dim == 1:
base_array = base_array[:, :n_time, :]
elif time_dim == 2:
base_array = base_array[:, :, :n_time]
return base_array.transpose(time_dim, lat_dim, lon_dim)
Loading
Loading