diff --git a/CITATION.cff b/CITATION.cff index 9933682e..e2b60600 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -8,8 +8,8 @@ title: "atlite: A Lightweight Python Package for Calculating Renewable Power Pot journal: The Journal of Open Source Software doi: "10.21105/joss.03294" repository: "https://github.com/PyPSA/atlite" -version: "0.2.5" -date-released: "2021-06-24" +version: "0.7.0" +date-released: "2026-10-03" license: "MIT" authors: - family-names: "Hofmann" diff --git a/README.rst b/README.rst index 49b15535..c739e9c4 100644 --- a/README.rst +++ b/README.rst @@ -40,7 +40,7 @@ For major next development goals, consult our `vision and roadmap project `__ (3rd October 2026) +======================================================================================= -* Add ``buffer_geometry`` argument to ``ExclusionContainer.add_raster``. Choose - between the historic ``"diamond"`` buffer (default) and a new ``"circular"`` - buffer that computes a geometrically accurate Euclidean buffer, correct in - diagonal directions. +**Breaking** + +* Drop support for Python 3.10. atlite now requires Python 3.11 or above + (https://github.com/PyPSA/atlite/pull/507). +* Add the required dependencies ``zarr>=3``, ``obstore>=0.9`` and ``eccodeslib`` + (not on Windows) (https://github.com/PyPSA/atlite/pull/507). +* Rewrite CSP conversion to model the collector geometry. ``convert_csp`` now uses + ``SurfaceOrientation`` with tracking (horizontal for parabolic trough, dual-axis for + solar tower) instead of the former ``atlite.csp`` module, which was removed. This + changes the CSP output. The ``working-with-csp`` example was updated accordingly + (https://github.com/PyPSA/atlite/pull/504). + +**Features** * Add an Earth Data Hub (EDH) source for ERA5 as a new dataset module ``era5-edh``, which serves data without the CDS processing queue. Configure a DestinE API key (see :doc:`installation`) and pass ``module="era5-edh"`` when creating a cutout. Only the native 0.25°×0.25° grid is supported; use ``module="era5"`` (CDS) for - other resolutions. -* Implement glofas dataset which contains daily river discharge. ``cutout.hydro()`` now - returns discharge if ``cutout.module`` contains ``"glofas"``. - (https://github.com/PyPSA/atlite/pull/498) -* Rewrite CSP conversion to model the collector geometry. ``convert_csp`` now uses - ``SurfaceOrientation`` with tracking (horizontal for parabolic trough, dual-axis for - solar tower) instead of the former ``atlite.csp`` module, which was removed. The - ``working-with-csp`` example was updated accordingly. - (https://github.com/PyPSA/atlite/pull/504) + other resolutions (https://github.com/PyPSA/atlite/pull/507). +* Add the GloFAS dataset with daily river discharge. ``cutout.hydro()`` now returns + discharge if ``cutout.module`` contains ``"glofas"`` + (https://github.com/PyPSA/atlite/pull/498). +* Add ``buffer_geometry`` argument to ``ExclusionContainer.add_raster``. Choose + between the historic ``"diamond"`` buffer (default) and a new ``"circular"`` + buffer that computes a geometrically accurate Euclidean buffer, correct in + diagonal directions (https://github.com/PyPSA/atlite/pull/513). * ``SurfaceOrientation`` now also accepts a plain orientation dict - (``{"slope": ..., "azimuth": ...}``) in addition to a callable. - (https://github.com/PyPSA/atlite/pull/504) -* Meshgrid ``regrid``/reproject now allows dask to rechunk the spatial input, so that - multi-chunk spatial planes are merged before reprojection. - (https://github.com/PyPSA/atlite/pull/504) + (``{"slope": ..., "azimuth": ...}``) in addition to a callable + (https://github.com/PyPSA/atlite/pull/504). +* Add type annotations and NumPy-style docstrings across the code base, checked + with ``mypy`` and stricter ``ruff`` rules. String options now use ``Literal`` + types (https://github.com/PyPSA/atlite/pull/492). **Bug fixes** -* Fix the ``units`` and ``long_name`` attributes of the ERA5 variables ``height`` - (was geopotential ``m**2 s**-2``), ``wnd_azimuth`` (was the ``u100`` wind - component) and ``solar_altitude``/``solar_azimuth`` (inherited ``latitude``). - Only metadata changes, values are unchanged (`#509 `_). - -* Fix ``get_oedb_windturbineconfig`` applying the documented ``turbine_type`` - search parameter to the value of ``name``. Searching by ``turbine_type`` - alone raised ``KeyError: 'name'``, and combining it with ``name`` silently - ignored the requested turbine type. - * Fix ``Cutout.line_rating`` passing line azimuth in radians while ``convert_line_rating`` interpreted ``psi`` as degrees. Azimuths are now - computed in degrees, matching the documented unit. - + computed in degrees, matching the documented unit + (https://github.com/PyPSA/atlite/pull/492). * Fix raster exclusions being wrongly applied with rasterio 1.5.1, which perturbs source values colliding with the reprojection nodata value. Rasters - added with ``nodata=0`` excluded their whole extent. + added with ``nodata=0`` excluded their whole extent + (https://github.com/PyPSA/atlite/pull/518). +* Fix ``regrid`` failing on inputs with multiple spatial chunks (e.g. SARAH). + Dask now merges the spatial chunks before reprojection + (https://github.com/PyPSA/atlite/pull/504). +* Fix ``get_oedb_windturbineconfig`` applying the documented ``turbine_type`` + search parameter to the value of ``name``. Searching by ``turbine_type`` + alone raised ``KeyError: 'name'``, and combining it with ``name`` silently + ignored the requested turbine type (https://github.com/PyPSA/atlite/pull/519). +* Fix the ``units`` and ``long_name`` attributes of the ERA5 variables ``height`` + (was geopotential ``m**2 s**-2``), ``wnd_azimuth`` (was the ``u100`` wind + component) and ``solar_altitude``/``solar_azimuth`` (inherited ``latitude``). + Only metadata changes, values are unchanged + (https://github.com/PyPSA/atlite/pull/521). +* Fix ``aggregate_matrix`` (used by ``convert_and_aggregate`` with ``matrix``, + ``shapes`` or ``layout``) raising a ``TypeError`` with xarray 2026.9.0. For + data not backed by dask, a ``pandas.MultiIndex`` passed as ``index`` to + ``convert_and_aggregate`` is now kept; before, it was replaced by its level names + (https://github.com/PyPSA/atlite/pull/524). `v0.6.1 `__ (21st April 2026) ======================================================================================= diff --git a/atlite/aggregate.py b/atlite/aggregate.py index c495d35f..de52b4b7 100644 --- a/atlite/aggregate.py +++ b/atlite/aggregate.py @@ -55,4 +55,7 @@ def aggregate_matrix( ).assign_coords(coords) return cast("xr.DataArray", result) da = da.stack(spatial=("y", "x")).transpose("spatial", "time") - return xr.DataArray(matrix * da, [index, da.coords["time"]]) + result = xr.DataArray( + matrix * da, dims=[*coords.dims, "time"], coords={"time": da.coords["time"]} + ) + return result.assign_coords(coords) diff --git a/atlite/datasets/era5_edh.py b/atlite/datasets/era5_edh.py index 941a3357..848cbb16 100644 --- a/atlite/datasets/era5_edh.py +++ b/atlite/datasets/era5_edh.py @@ -429,9 +429,7 @@ def get_data_height(cutout: Cutout) -> xr.Dataset: Dataset containing ``height``. """ ds = _load_feature(cutout, "height", static=True) - ds = _add_height(ds) - ds["height"].attrs["units"] = "m**2 s**-2" - return ds + return _add_height(ds) _HANDLERS: dict[str, Handler] = { diff --git a/test/test_aggregate.py b/test/test_aggregate.py new file mode 100644 index 00000000..8711e976 --- /dev/null +++ b/test/test_aggregate.py @@ -0,0 +1,52 @@ +# SPDX-FileCopyrightText: Contributors to atlite +# +# SPDX-License-Identifier: MIT + +"""Tests for spatial aggregation with a sparse matrix.""" + +import numpy as np +import pandas as pd +import pytest +import scipy.sparse as sp +import xarray as xr + +from atlite.aggregate import aggregate_matrix +from atlite.utils import ensure_coords + +BUSES = pd.Index(["a", "b", "c"], name="bus") +REGIONS = pd.MultiIndex.from_tuples( + [("DE", 1), ("DE", 2), ("FR", 1)], names=["country", "n"] +) + + +@pytest.fixture +def da(): + rng = np.random.default_rng(42) + return xr.DataArray( + rng.random((5, 3, 4)), + dims=["time", "y", "x"], + coords={ + "time": pd.date_range("2020-01-01", periods=5, freq="h"), + "y": [50.0, 51.0, 52.0], + "x": [5.0, 6.0, 7.0, 8.0], + }, + ) + + +@pytest.mark.parametrize("chunked", [False, True], ids=["numpy", "dask"]) +@pytest.mark.parametrize( + "index", + [BUSES, REGIONS, ensure_coords(REGIONS)], + ids=["index", "multiindex", "coords"], +) +def test_aggregate_matrix_keeps_index(da, index, chunked): + matrix = sp.csr_matrix(np.random.default_rng(0).random((3, 12))) + data = da.chunk({"time": 2}) if chunked else da + + result = aggregate_matrix(data, matrix, index).transpose(..., "time") + + expected = matrix @ da.stack(spatial=("y", "x")).transpose("spatial", "time").values + np.testing.assert_allclose(result.values, expected) + xr.testing.assert_identical(result.coords["time"], da.coords["time"]) + dim = result.dims[0] + assert result.indexes[dim].equals(index if isinstance(index, pd.Index) else REGIONS)