Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ paper
.coverage*
!.coveragerc

notebooks

# Ignore IDE project files
.idea/
.vscode
20 changes: 14 additions & 6 deletions RELEASE_NOTES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,22 @@ Release Notes
#############


.. Upcoming Release
.. ================
Upcoming Release
================

.. .. warning::
.. warning::

.. The features listed below are not released yet, but will be part of the next release!
.. To use the features already you have to install the ``master`` branch, e.g.
.. ``pip install git+https://github.com/pypsa/atlite``.
The features listed below are not released yet, but will be part of the next release!
To use the features already you have to install the ``master`` branch, e.g.
``pip install git+https://github.com/pypsa/atlite``.

**Features**

* The method ``runoff(normalize_using_yearly=...)`` now supports handling of
partial years when normalizing runoff data based on annual data. The
normalization is applied proportionally based on the time overlap. A warning
is shown for partial years noting the strong assumption of evenly distributed
runoff throughout the year.

`v0.4.0 <https://github.com/PyPSA/atlite/releases/tag/v0.4.0>`__ (30th January 2025)
=======================================================================================
Expand Down
68 changes: 58 additions & 10 deletions atlite/convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -962,20 +962,68 @@
else:
normalize_using_yearly_i = normalize_using_yearly_i.astype(int)

years = (
pd.Series(pd.to_datetime(result.coords["time"].values).year)
.value_counts()
.loc[lambda x: x > 8700]
.index.intersection(normalize_using_yearly_i)
)
assert len(years), "Need at least a full year of data (more is better)"
years_overlap = slice(str(min(years)), str(max(years)))
result_time = pd.DatetimeIndex(result.coords["time"].values)
result_period = (result_time.min(), result_time.max())

Check warning on line 966 in atlite/convert.py

View check run for this annotation

Codecov / codecov/patch

atlite/convert.py#L965-L966

Added lines #L965 - L966 were not covered by tests

if isinstance(normalize_using_yearly.index, pd.DatetimeIndex):
norm_period = (

Check warning on line 969 in atlite/convert.py

View check run for this annotation

Codecov / codecov/patch

atlite/convert.py#L969

Added line #L969 was not covered by tests
normalize_using_yearly.index.min(),
normalize_using_yearly.index.max(),
)
else:
min_year, max_year = (

Check warning on line 974 in atlite/convert.py

View check run for this annotation

Codecov / codecov/patch

atlite/convert.py#L974

Added line #L974 was not covered by tests
normalize_using_yearly_i.min(),
normalize_using_yearly_i.max(),
)
norm_period = (pd.Timestamp(f"{min_year}"), pd.Timestamp(f"{max_year + 1}"))

Check warning on line 978 in atlite/convert.py

View check run for this annotation

Codecov / codecov/patch

atlite/convert.py#L978

Added line #L978 was not covered by tests

overlap_start = max(result_period[0], norm_period[0])
overlap_end = min(result_period[1], norm_period[1])

Check warning on line 981 in atlite/convert.py

View check run for this annotation

Codecov / codecov/patch

atlite/convert.py#L980-L981

Added lines #L980 - L981 were not covered by tests

if overlap_start >= overlap_end:
raise ValueError(

Check warning on line 984 in atlite/convert.py

View check run for this annotation

Codecov / codecov/patch

atlite/convert.py#L984

Added line #L984 was not covered by tests
f"No overlap between runoff data ({result_period}) and normalization data ({norm_period})"
)

years_overlap = slice(str(overlap_start.date()), str(overlap_end.date()))

Check warning on line 988 in atlite/convert.py

View check run for this annotation

Codecov / codecov/patch

atlite/convert.py#L988

Added line #L988 was not covered by tests
dim = result.dims[1 - result.get_axis_num("time")]

if isinstance(normalize_using_yearly.index, pd.DatetimeIndex):
norm_data = normalize_using_yearly.loc[overlap_start:overlap_end].sum()

Check warning on line 992 in atlite/convert.py

View check run for this annotation

Codecov / codecov/patch

atlite/convert.py#L992

Added line #L992 was not covered by tests
else:
# Handle year-based index with proportional scaling
overlap_years = set(pd.date_range(overlap_start, overlap_end).year)
norm_years = sorted(overlap_years.intersection(normalize_using_yearly_i))

Check warning on line 996 in atlite/convert.py

View check run for this annotation

Codecov / codecov/patch

atlite/convert.py#L995-L996

Added lines #L995 - L996 were not covered by tests

partial_years = []
norm_data = 0

Check warning on line 999 in atlite/convert.py

View check run for this annotation

Codecov / codecov/patch

atlite/convert.py#L998-L999

Added lines #L998 - L999 were not covered by tests

for year in norm_years:
year_start = pd.Timestamp(f"{year}")
year_end = pd.Timestamp(f"{year}-12-31 23:59:59")
start = max(overlap_start, year_start)
end = min(overlap_end, year_end)

Check warning on line 1005 in atlite/convert.py

View check run for this annotation

Codecov / codecov/patch

atlite/convert.py#L1002-L1005

Added lines #L1002 - L1005 were not covered by tests

if start > year_start or end < year_end:
partial_years.append(year)

Check warning on line 1008 in atlite/convert.py

View check run for this annotation

Codecov / codecov/patch

atlite/convert.py#L1008

Added line #L1008 was not covered by tests

fraction = (end - start).total_seconds() / (

Check warning on line 1010 in atlite/convert.py

View check run for this annotation

Codecov / codecov/patch

atlite/convert.py#L1010

Added line #L1010 was not covered by tests
year_end - year_start
).total_seconds()
norm_data += normalize_using_yearly.loc[year] * fraction

Check warning on line 1013 in atlite/convert.py

View check run for this annotation

Codecov / codecov/patch

atlite/convert.py#L1013

Added line #L1013 was not covered by tests

if partial_years:
logger.warning(

Check warning on line 1016 in atlite/convert.py

View check run for this annotation

Codecov / codecov/patch

atlite/convert.py#L1016

Added line #L1016 was not covered by tests
f"Normalizing partial year data for year(s) {partial_years}. "
f"This assumes runoff is evenly distributed throughout the year, "
f"which may not be accurate for seasonal patterns. Consider using "
f"time-based normalization data for more precise results."
)

result *= (
xr.DataArray(normalize_using_yearly.loc[years_overlap].sum(), dims=[dim])
xr.DataArray(norm_data, dims=[dim])
/ result.sel(time=years_overlap).sum("time")
).reindex(countries=result.coords["countries"])
).reindex({dim: result.coords[dim]})

return result

Expand Down