Skip to content

feat: Add temporal clustering functionality#1508

Merged
FabianHofmann merged 12 commits intomasterfrom
feature/temporal-clustering
Jan 26, 2026
Merged

feat: Add temporal clustering functionality#1508
FabianHofmann merged 12 commits intomasterfrom
feature/temporal-clustering

Conversation

@FabianHofmann
Copy link
Copy Markdown
Contributor

@FabianHofmann FabianHofmann commented Jan 3, 2026

Changes proposed in this Pull Request

This PR implements temporal clustering functionality for PyPSA, enabling users to reduce the temporal resolution of networks while preserving total modeled hours through snapshot weighting adjustments. This is an upstream implementation based on patterns from PyPSA-EUR.

New Functions in pypsa.clustering.temporal

  • resample(n, offset) - Aggregate snapshots at regular intervals (e.g., "3h", "6h", "24h")
  • downsample(n, stride) - Select every Nth snapshot as representative with scaled weights
  • segment(n, num_segments) - Use TSAM k-means clustering for variable-duration segments
  • from_snapshot_map(n, snapshot_map) - Apply pre-computed temporal aggregation mapping

Accessor Pattern

All methods are accessible via n.cluster.temporal.*:

# Resample to 3-hour resolution
m = n.cluster.temporal.resample("3h")

# Select every 4th snapshot  
m = n.cluster.temporal.downsample(4)

# TSAM segmentation (requires tsam package)
m = n.cluster.temporal.segment(100)

# Get full result with mapping for disaggregation
result = n.cluster.temporal.get_resample_result("3h")
print(result.snapshot_map)  # Original -> aggregated mapping

Additional Features

  • n.nyears property - Returns total modeled years based on snapshot weightings
  • TemporalClustering dataclass - Returns both clustered network and snapshot mapping
  • Configurable aggregation rules - Default mean, with min for e_max_pu and max for e_min_pu
  • Multi-period support - resample() handles networks with investment periods
  • Leap day handling - Transfer weights to March 1 rather than dropping hours
  • tsam optional dependency - For segment() function

Weight Preservation Invariant

All methods preserve the invariant: sum(weights) == total_modeled_hours

original_hours = n.snapshot_weightings["objective"].sum()
m = n.cluster.temporal.resample("3h")
assert np.isclose(m.snapshot_weightings["objective"].sum(), original_hours)

Checklist

  • Code changes are sufficiently documented; i.e. new functions contain docstrings and further explanations may be given in docs.
  • Unit tests for new features were added (if applicable).
  • A note for the release notes docs/release-notes.md of the upcoming release is included.
  • I consent to the release of this PR's code under the MIT license.

FabianHofmann and others added 4 commits January 3, 2026 15:21
Implement temporal clustering methods for reducing network time resolution:
- resample(): Aggregate snapshots at regular intervals (e.g., "3h", "24h")
- downsample(): Select every Nth snapshot as representative
- segment(): Use TSAM k-means clustering for variable-duration segments
- from_snapshot_map(): Apply pre-computed aggregation mapping

Key features:
- TemporalClustering dataclass returns both network and snapshot mapping
- TemporalClusteringAccessor accessible via n.cluster.temporal.*
- Add n.nyears property for total modeled years
- Configurable aggregation rules (mean, min, max for storage bounds)
- Multi-period support for resample()
- Leap day handling (transfer weights to March 1)
- tsam optional dependency for segment()

Co-Authored-By: Claude <[email protected]>
- Test downsample with multiperiod (NotImplementedError)
- Test from_snapshot_map with DataFrame input
- Test e_min_pu and e_max_pu aggregation rules
- Test get_from_snapshot_map_result accessor
- Test segment function with tsam (basic, accessor, errors)
- Test segment with invalid num_segments
- Test segment with multiperiod
- Test segment with no dynamic data
@fneum fneum self-requested a review January 3, 2026 21:35
Without overwrite=True, existing time series columns from the
copied network are not updated with the resampled/aggregated data,
since _import_series_from_df by default skips columns that already exist.
Copy link
Copy Markdown
Member

@fneum fneum left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool! That's very useful! Just details as comments.

@fneum
Copy link
Copy Markdown
Member

fneum commented Jan 5, 2026

Took the liberty to update time-series-aggregation.ipynb example. Reproduces same results with new functions 👍

@lkstrp lkstrp linked an issue Jan 5, 2026 that may be closed by this pull request
FabianHofmann and others added 5 commits January 20, 2026 14:44
- vectorize _build_resample_map and segment snapshot_map for performance
- handle downsample edge case when snapshots not divisible by stride
- add warning when clustering solved networks
- add DatetimeIndex validation in resample
- add num_segments upper bound validation in segment
- fix docstrings: agglomerative clustering, not inplace, MIP solver
- add explanatory comments for contiguous range and year-wise resampling
- rename annual_max to normalization_factors
- add edge case tests for non-divisible stride and large frequency
- update API docs and user guide with temporal clustering
- add code example to release notes
- Replace deprecated pd.Index.append() with pd.MultiIndex.from_tuples()
- Vectorize snapshot_map assignment using numpy indexing
- Add tests for snapshot_map, non-divisible stride, and large stride
Stochastic networks (with scenarios) are not yet supported by temporal
clustering. Add explicit check to all four public functions to raise
NotImplementedError with clear message.
@FabianHofmann FabianHofmann requested a review from fneum January 21, 2026 09:11
@FabianHofmann
Copy link
Copy Markdown
Contributor Author

@fneum thanks for the review, I (hope I) addressed all of your points. the resample is now much saver (datetime index required, multiperiod support, stoch networks raising error, zero/undivisble arguments tested). have a look again if you want

Copy link
Copy Markdown
Member

@fneum fneum left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am happy with that version! Nice use of np.searchsorted!

@FabianHofmann FabianHofmann enabled auto-merge (squash) January 26, 2026 17:02
@FabianHofmann FabianHofmann merged commit f01690d into master Jan 26, 2026
26 of 27 checks passed
@FabianHofmann FabianHofmann deleted the feature/temporal-clustering branch January 26, 2026 19:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Draft time-clustering module

2 participants