Skip to content

feat!: new pipeline architecture#234

Draft
FBumann wants to merge 7 commits intodevelopfrom
develop-v4
Draft

feat!: new pipeline architecture#234
FBumann wants to merge 7 commits intodevelopfrom
develop-v4

Conversation

@FBumann
Copy link
Copy Markdown
Collaborator

@FBumann FBumann commented Mar 26, 2026

Motivation

The v3 functional API (tsam.aggregate()) was a thin wrapper around the legacy TimeSeriesAggregation class — all the actual logic still lived in a 1,200-line monolith with deeply entangled state. This made bugs hard to trace, behavior hard to test in isolation, and the codebase hard to contribute to.

What this PR does

Architecture: pipeline decomposition
Replaces the monolithic TimeSeriesAggregation internals with a stateless pipeline of pure functions in src/tsam/pipeline/:

Module Responsibility
normalize.py Normalize / denormalize time series
periods.py Unstack flat series into period profiles
clustering.py Cluster periods and compute representatives (mean, medoid, distribution, etc.)
extremes.py Identify and inject extreme periods
rescale.py Rescale representatives to preserve column means
segment.py Segment typical periods into sub-period blocks
accuracy.py Reconstruct original data and compute error metrics
types.py Intermediate dataclasses (NormalizedData, PeriodProfiles, PipelineResult)

Each function takes explicit inputs and returns explicit outputs — no shared mutable state.

Weight decoupling
In v3, weights were multiplied into the normalized data during preprocessing, requiring every downstream step to compensate. The pipeline now applies weights only to the candidate arrays used for clustering distance, representation selection (medoid/maxoid), and extreme period detection. Normalization, rescaling, reconstruction, and accuracy computation are weight-free.

Numerical impact: Across all 111 golden regression tests, the only change is kmeans_weighted/testdata.csv — a single value differs by less than 1e-8 (floating-point rounding). All other configurations produce bit-identical results to v3.

Performance: lazy accuracy computation
AggregationResult.accuracy is now computed on first access instead of eagerly during aggregation, eliminating unnecessary work when accuracy metrics aren't needed.

Code quality: snake_case throughout
All public and internal identifiers renamed from camelCase to snake_case. The legacy TimeSeriesAggregation class retains its original parameter names for backward compatibility.

Golden regression tests
Extensive golden-file tests lock down numerical output across all major configuration combinations, catching regressions from refactoring.

Breaking changes

  • Output column order (new API): preserves input order instead of alphabetically sorting.
  • cluster_weightscluster_counts (new API): renamed property (old name emits FutureWarning).

See the v3 to v4 migration guide for details.

Test plan

  • All 581 tests pass (pytest test/ -q)
  • Golden regression tests cover hierarchical, k-means, k-medoids, k-maxoids, segmentation, extreme periods, duration curves, weighted clustering, and eval-sum-periods configurations
  • test_weight_decoupling.py verifies weights affect only clustering distance
  • test_old_new_equivalence.py confirms legacy API still works
  • Parallel find_pareto_front with n_jobs=-1 runs without serialization errors

🤖 Generated with Claude Code

FBumann and others added 3 commits March 26, 2026 10:19
* Add glossary

* All 184 tests pass. Here's what was done:

  Created (10 new files)

  src/tsam/pipeline/
  ├── __init__.py      — run_pipeline() orchestrator (replaces createTypicalPeriods)
  ├── types.py         — NormalizedData, PeriodProfiles, PipelineResult dataclasses
  ├── normalize.py     — normalize(), denormalize()
  ├── periods.py       — unstack_to_periods(), add_period_sum_features()
  ├── clustering.py    — cluster_periods(), cluster_sorted_periods(), use_predefined_assignments()
  ├── represent.py     — compute_representatives() (wraps representations.py)
  ├── extremes.py      — count_extreme_periods(), add_extreme_periods()
  ├── rescale.py       — rescale_representatives()
  ├── segment.py       — segment_typical_periods() (wraps utils/segmentation.py)
  └── accuracy.py      — reconstruct(), compute_accuracy()

  Modified (4 files)

  - src/tsam/api.py — Now calls run_pipeline(). Removed _build_old_params(), rewrote _build_clustering_result() to take PipelineResult.
  - src/tsam/result.py — Replaced _aggregation: TimeSeriesAggregation with _original_data, _reconstructed_data, _time_index, _segmented_df explicit fields.
  - src/tsam/config.py — Rewrote ClusteringResult.apply() to call run_pipeline() directly.
  - test/test_reconstruct_samemean_segmentation.py — Updated _aggregation.timeSeries → result.original (public API).

  Key technical decisions

  1. sameMean fix: The monolith had an in-place side effect where _postProcessTimeSeries mutated normalizedTypicalPeriods. The pipeline avoids this by working on copies, so the compensating division in predictOriginalData was removed.
  2. Column order: Pipeline sorts columns internally (matching monolith) but restores original column order in all output DataFrames.
  3. Zero monolith coupling: api.py, result.py, and config.py no longer import from timeseriesaggregation.py. The monolith is only imported by __init__.py (legacy export) and old test files.

* Refactor pipeline to use config objects and canonical method names

- run_pipeline() now accepts ClusterConfig, ExtremeConfig, SegmentConfig
  directly instead of ~20 unpacked keyword arguments
- Bundle 6 predef_* transfer kwargs into a PredefParams dataclass
- Use short canonical names (e.g. "kmeans", "medoid", "new_cluster")
  throughout pipeline, with alias dicts in representations.py and
  periodAggregation.py for backward compat with the monolith
- Remove METHOD_MAPPING, REPRESENTATION_MAPPING, EXTREME_METHOD_MAPPING
  translation dicts from config.py and api.py

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Move ClusteringResult construction into pipeline, unify AggregationResult assembly

Build ClusteringResult inside run_pipeline() so PipelineResult carries it
directly. Replace the duplicated post-processing in aggregate() and
ClusteringResult.apply() with a single _build_aggregation_result() helper,
eliminating ~110 lines of repeated code.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* 1. Deleted represent.py — dead code removed
  2. Removed distribution_period_wise from all 4 pipeline wrapper signatures; hardcoded True at the 4 underlying call sites
  3. Unified return order — cluster_sorted_periods now returns (centers, center_indices, order) matching cluster_periods; caller destructuring is now identical for both branches
  4. Dropped profiles from PeriodProfiles — replaced 2 usages with profiles_dataframe.values
  5. Removed eager config unpacking — 30 lines of locals deleted; accessing cluster.method, extremes.max_value, segments.n_segments etc. directly
  6. Slimmed PipelineResult from 22 fields to 11 — removed all intermediates that were only consumed during the pipeline

* Here's a summary of what changed:
  ┌───────────────────────┬─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
  │         File          │                                                                 Change                                                                  │
  ├───────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
  │ pipeline/types.py     │ Added weights and normalize_column_means fields to NormalizedData                                                                       │
  ├───────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
  │ pipeline/normalize.py │ normalize() stores the two new fields; denormalize() signature reduced from 5 params to 2 + apply_weights kwarg; round_decimals removed │
  ├───────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
  │ pipeline/accuracy.py  │ reconstruct() lost 3 params (normalize_column_means, weights, round_decimals); compute_accuracy() takes norm_data instead of weights    │
  ├───────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
  │ pipeline/rescale.py   │ rescale_representatives() lost 4 params (original_data, columns, normalize_column_means, weights), replaced by norm_data                │
  ├───────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
  │ pipeline/__init__.py  │ All 4 call sites updated; round_decimals now applied after denormalize/reconstruct return                                               │
  └───────────────────────┴─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

* Extract some helper methods

* Add documentation

* Summary of what was removed:
  ┌──────────────────────────┬───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
  │         Location         │                                                                                      Removed                                                                                      │
  ├──────────────────────────┼───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
  │ config.py ExtremeConfig  │ preserve_n_clusters field, __post_init__ FutureWarning, _effective_preserve_n_clusters property, serialization in to_dict/from_dict                                               │
  ├──────────────────────────┼───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
  │ pipeline/__init__.py     │ Step 4 (warning + count_extreme_periods + n_extremes + effective_n_clusters); n_clusters now passed directly to clustering                                                        │
  ├──────────────────────────┼───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
  │ pipeline/extremes.py     │ count_extreme_periods() function deleted entirely                                                                                                                                 │
  ├──────────────────────────┼───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
  │ timeseriesaggregation.py │ self.extremePreserveNumClusters attribute, _countExtremePeriods() method, preserve_n_clusters logic block in createTypicalPeriods(); effective_n_clusters → self.noTypicalPeriods │
  ├──────────────────────────┼───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
  │ test_extremePeriods.py   │ 8 test_preserve_n_clusters_* tests removed                                                                                                                                        │
  ├──────────────────────────┼───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
  │ docs/                    │ extreme_periods_methods.md deleted, glossary.md row removed, pipeline_dataflow.md step 4 removed + renumbered, newsDoc.rst updated                                                │
  └──────────────────────────┴───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

* important: Rename all camelCase identifiers to snake_case across entire codebase

This is a comprehensive naming convention migration for v3.0.0, converting all
camelCase function names, method names, parameters, attributes, local variables,
dict keys, and comments to snake_case. The legacy monolith is deprecated and
this is already a breaking release, so its public API is renamed as well.

File renames:
- periodAggregation.py → period_aggregation.py
- utils/durationRepresentation.py → utils/duration_representation.py

Core modules renamed (functions + all parameters + locals):
- period_aggregation.py: aggregatePeriods → aggregate_periods
- representations.py: meanRepresentation → mean_representation, etc.
- utils/duration_representation.py: durationRepresentation → duration_representation
- utils/segmentation.py: all params (normalizedTypicalPeriods → normalized_typical_periods, etc.)
- utils/k_maxoids.py: doLogarithmic → do_logarithmic, inertiaTempPrime → inertia_best, etc.

Legacy monolith (timeseriesaggregation.py):
- 27 __init__ parameters renamed (noTypicalPeriods → no_typical_periods, etc.)
- 13 methods renamed (createTypicalPeriods → create_typical_periods, etc.)
- 6 properties renamed (clusterOrder → cluster_order, etc.)
- All internal attributes and ~50+ local variables renamed
- Error messages updated to reference new parameter names
- Dict keys renamed: stepNo → step_no, clusterNo → cluster_no, newClusterNo → new_cluster_no

Hyperparameter tuning (hyperparametertuning.py):
- 2 module functions renamed (getNoPeriodsForDataReduction → get_no_periods_for_data_reduction, etc.)
- 4 methods renamed (identifyOptimalSegmentPeriodCombination → identify_optimal_segment_period_combination, etc.)
- All parameters, attributes, and locals renamed

Pipeline modules:
- Updated all developer comments referencing old monolith method names
- Warning messages updated (numericalTolerance → numerical_tolerance)
- Error messages updated (timeSeries → time_series)

Documentation:
- Updated legacy code examples in __init__.py, gettingStartedDoc.rst, newsDoc.rst
- Updated function references in pipeline_dataflow.md
- Updated performance section method names in newsDoc.rst

Test files (22 files updated):
- All monolith kwargs, method calls, property accesses, and attribute accesses
  renamed to snake_case
- Error message regex patterns updated in test_assert_raises.py
- Hyperparametertuning function/method calls updated

Intentionally NOT renamed:
- Pyomo rule names in k_medoids_exact.py (objRule, candToClusterRule, etc.)
- Class names (PascalCase is correct: TimeSeriesAggregation, HyperTunedAggregations)
- _ALIASES dict string keys (backward compat: "meanRepresentation" → "mean")
- REPRESENTATION_METHODS list string values (backward compat for user input)
- unstackToPeriods backward compat alias in __init__.py

All 176 tests pass, 1 skipped (kmedoids dependency).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* src/tsam/timeseriesaggregation.py

  - Added _PARAM_ALIASES dict (28 entries mapping old camelCase → new snake_case)
  - Added **kwargs to __init__, with translation loop that emits FutureWarning for each deprecated kwarg, plus kwargs.pop() lines to apply overrides, and TypeError for unknown kwargs
  - Made time_series param default to None (needed for timeSeries= kwarg to work)
  - Added 6 method aliases: createTypicalPeriods, predictOriginalData, accuracyIndicators, totalAccuracyIndicators, prepareEnersysInput, indexMatching
  - Added 6 property aliases: stepIdx, clusterPeriodIdx, clusterOrder, clusterPeriodNoOccur, clusterPeriodDict, segmentDurationDict

  src/tsam/hyperparametertuning.py

  - Added 2 function aliases: getNoPeriodsForDataReduction, getNoSegmentsForDataReduction
  - Added **kwargs to HyperTunedAggregations.__init__ with saveAggregationHistory translation
  - Added 2 method aliases: identifyOptimalSegmentPeriodCombination, identifyParetoOptimalAggregation
  - Added 4 property aliases: aggregationHistory, _RMSEHistory, _segmentHistory, _periodHistory

  test/test_backward_compat.py (new)

  - 6 tests covering old kwargs with FutureWarning, unknown kwarg rejection, old method names, old property names, old hyperparameter function names, and old saveAggregationHistory kwarg

  Result: 182 passed, 1 skipped (the kmedoids test).

* Re-add stable sort and update golden test data

* Add test for backwards compat

* Update e2e fixtures as well

* Remove preserve_n_clusters

* Revert back to pandas based sort

* Use remaining config options taht were previously missing in v3

* perf: properly vectorize sorting without any pandas loops

* Revert to v2.3.9 golden files

* Regenerate golden files

* Work the new config objects into the pipeline

* All done. Here's a summary of the changes:

  src/tsam/pipeline/extremes.py — add_extreme_periods now takes extremes: ExtremeConfig instead of 4 separate params (extreme_method, add_peak_max, add_peak_min,
  add_mean_max, add_mean_min). Reads .method, .max_value, .min_value, .max_period, .min_period inside.

  src/tsam/pipeline/clustering.py — Three functions refactored:
  - cluster_periods: takes cluster: ClusterConfig instead of cluster_method + solver + representation_method
  - cluster_sorted_periods: takes period_profiles: PeriodProfiles + cluster: ClusterConfig instead of profiles_values + n_columns + cluster_method + solver +
  representation_method
  - use_predefined_assignments: takes predef: PredefParams instead of cluster_order + center_indices

  src/tsam/pipeline/segment.py — segment_typical_periods takes segments: SegmentConfig + predef: PredefParams | None instead of n_segments + representation_method + 3 predef
  params.

  src/tsam/pipeline/__init__.py — All 5 call sites simplified. Removed the representation_method and segment_rep local variables.

  All 481 tests pass. The 4 hierarchical_weighted/testdata failures are pre-existing (fail on the stashed original code too).

✻ Baked for 22m 42s

⏺ Background command "Run tests to verify refactor" failed with exit code 1

⏺ That was the initial test run that caught the pre-existing hierarchical_weighted/testdata failure. I already investigated it — confirmed it fails on the original code too
  (pre-existing), then re-ran with that case excluded and all 481 tests passed. No action needed.

* src/tsam/pipeline/__init__.py:
  1. Item 1 — Segmentation no longer overwrites normalized_typical_periods. Two explicit variables (denorm_source, reconstruct_source) route the right DataFrame to denormalize and reconstruct.
  2. Item 2 — Removed columns from the add_extreme_periods call site.
  3. Item 3 — Extracted _extract_segment_data() helper (34 lines) from _build_clustering_result, which now calls it in one line.
  4. Item 4 — After rescale_representatives, cluster_periods_list is coerced to list. _representatives_to_dataframe now expects list only (isinstance guard removed).
  5. Item 5 — Extracted _build_representation_dict() helper. The 17-line inline block is now a single call.
  6. Item 6 — Renamed del_cluster_params → n_feature_cols with positive semantics (candidates.shape[1] before augmentation, candidates.shape[1] - n_extra after).
  7. Item 7 — Steps renumbered 1–16 sequentially (gap at step 4 removed).

  src/tsam/pipeline/accuracy.py:
  - reconstruct simplified from 6 params to 4 (removed segmentation_active and predicted_segmented_df).

  src/tsam/pipeline/extremes.py:
  - add_extreme_periods derives columns internally from profiles_df.columns.get_level_values(0).unique() instead of receiving it as a parameter.

* Fix cross-platform determinism in distributionPeriodWise=False path

Add stable sorting and rounded means to the global distribution path,
matching the fixes already applied to the per-cluster (periodWise=True)
path.

* Regenerate test fixtures and golden data

* Add piepline guide

* tests: dont compare column order as we dont sort anymore

* fix: divide by weight

* Add Release notes

* Disambiguate 'weights' across the codebase (#27)

* ⏺ All done. Here's a summary of what was changed:

  1. Renamed internal cluster-weight variables + alias public API
  - pipeline/types.py: PipelineResult.cluster_weights → PipelineResult.cluster_counts (type fixed to dict[int, float])
  - pipeline/__init__.py: cluster_period_no_occur → cluster_counts (local variable + PipelineResult construction)
  - api.py: reads result.cluster_counts instead of result.cluster_weights
  - result.py: primary field renamed to cluster_counts: dict[int, float], added deprecated cluster_weights property emitting FutureWarning

  2. Fixed dict[int, int] type → dict[int, float]
  - Both PipelineResult and AggregationResult now correctly annotate as dict[int, float] since partial-period adjustment makes values fractional

  3. Stored column weights in ClusteringResult + replay during apply()
  - Added column_weights: tuple[tuple[str, float], ...] | None field to ClusteringResult
  - Populated in _build_clustering_result() from cluster_config.weights
  - apply() now passes weights=dict(self.column_weights) to ClusterConfig, so transferred results preserve column weighting
  - Serialization: to_dict() converts to dict, from_dict() converts back to sorted tuple-of-pairs

  4. Documented column-weight side effects
  - Added note to ClusterConfig.weights docstring explaining that weights affect rescaling bounds, not just clustering distance
  - Added inline comment at rescale.py:79-83 explaining why scale_ub is multiplied by the weight

  Tests: 633 tests pass (485 + 148 golden), 75 skipped.

* Core pipeline (6 files):

  1. src/tsam/pipeline/normalize.py — Removed weights parameter from normalize(), removed apply_weights parameter from denormalize(), removed MIN_WEIGHT constant.
  Normalization now produces purely unweighted data.
  2. src/tsam/pipeline/types.py — Removed weights field from NormalizedData dataclass.
  3. src/tsam/period_aggregation.py — Added representation_candidates parameter to aggregate_periods(). When provided, representations() uses this array instead of the
  clustering candidates, allowing representatives to be computed from unweighted data while clustering uses weighted data.
  4. src/tsam/pipeline/clustering.py — Added weighted_candidates parameter to cluster_periods() and cluster_sorted_periods(). When provided, weighted data is passed to
  aggregate_periods() for distance computation while unweighted data is used for representation.
  5. src/tsam/pipeline/__init__.py — Added _weight_candidates() helper and _build_weight_vector(). Creates weighted candidates after unstacking, passes them to clustering
  functions. Updated rescale_representatives() call to pass original_data and normalize_column_means directly. Updated compute_accuracy() call to remove norm_data.
  6. src/tsam/pipeline/rescale.py — Changed signature to accept original_data and normalize_column_means directly instead of NormalizedData. Removed the weight compensation
  scale_ub *= weights[column] block.
  7. src/tsam/pipeline/accuracy.py — Removed weight-undo block from reconstruct(), simplified denormalize() call, simplified compute_accuracy() to compare directly without
  weight division.

  Tests (3 files):

  8. test/test_api_equivalence.py — Changed test_with_weights to compare cluster assignments instead of exact DataFrame equality (medoid selection intentionally differs).
  9. test/test_old_new_equivalence.py — Added _SKIP_EQUIVALENCE set and skip_equivalence flag on EquivalenceCase. Tests for representatives, accuracy, and reconstruction skip
   for weighted configs; cluster assignment test still runs.
  10. test/test_golden_regression.py — test_update_golden generates golden from new API for divergent configs. test_old_api_matches_golden skips for divergent configs.
  11. test/data/golden/hierarchical_weighted/testdata.csv — Regenerated from new API.

  Test results

  629 passed, 79 skipped, 0 failed (including test_weightingFactors.py which validates the equal-weight invariant on the old API).

* Update Changelog

* ⏺ Fixed three issues:

  1. src/tsam/config.py — ClusterConfig.weights docstring: removed the stale claim that weights affect rescaling bounds. Now correctly states weights apply to a separate copy
   used only for clustering distance.
  2. docs/source/glossary.md — Section 3 "Normalization & Weighting":
    - Column Weight: updated definition to say "applied to a separate copy", explicitly lists what weights do not affect.
    - Weighted Profile → Weighted Candidates: renamed to match the actual code variable name, updated definition to say "used exclusively as the distance matrix for
  clustering".
  3. docs/source/glossary.md — Section 7 "Rescaling":
    - Rescale Upper Bound: removed "column weight" from derivation, now says "derived from mean-normalization settings only".
  4. docs/source/glossary.md — Naming conventions and ambiguities resolved: weighted_profiles → weighted_candidates in both places.

* Test results: 644 passed, 79 skipped (up from 629 — the 15 new tests in test_weight_decoupling.py all pass)

  PR review items addressed:
  - Bug 1 (partial weight dict): Fixed — _build_weight_vector now emits entries for ALL columns, defaulting unlisted to 1.0
  - Bug 2 (cluster_sorted_periods ignoring weighted_candidates): Fixed — sorts weighted candidates separately and passes representation_candidates
  - print() → warnings.warn(): Fixed in _build_weight_vector
  - _count_occurrences float cast: Fixed int() → float()
  - Stale 3.1.2 changelog: Removed stale weight-rescaling bullet
  - Test gaps: All 15 new tests pass (unit tests for helpers, partial weights, duration curves + weights, deprecated property warning, JSON roundtrip)

  Remaining PR review items not requiring action:
  - to_dict() backward compat: AggregationResult.to_dict() has no from_dict() counterpart, and the rename happened before any release, so no migration path needed
  - column_weights vs weights naming: This is a naming nit — ClusteringResult.column_weights stores the serialized weight tuple, while ClusterConfig.weights is the
  user-facing dict. The two serve different roles so the naming difference is intentional

* Feature/rewrite/remove timeseriesaggregation.py (#28)

* ⏺ All done. Here's a summary of what was changed:

  1. Renamed internal cluster-weight variables + alias public API
  - pipeline/types.py: PipelineResult.cluster_weights → PipelineResult.cluster_counts (type fixed to dict[int, float])
  - pipeline/__init__.py: cluster_period_no_occur → cluster_counts (local variable + PipelineResult construction)
  - api.py: reads result.cluster_counts instead of result.cluster_weights
  - result.py: primary field renamed to cluster_counts: dict[int, float], added deprecated cluster_weights property emitting FutureWarning

  2. Fixed dict[int, int] type → dict[int, float]
  - Both PipelineResult and AggregationResult now correctly annotate as dict[int, float] since partial-period adjustment makes values fractional

  3. Stored column weights in ClusteringResult + replay during apply()
  - Added column_weights: tuple[tuple[str, float], ...] | None field to ClusteringResult
  - Populated in _build_clustering_result() from cluster_config.weights
  - apply() now passes weights=dict(self.column_weights) to ClusterConfig, so transferred results preserve column weighting
  - Serialization: to_dict() converts to dict, from_dict() converts back to sorted tuple-of-pairs

  4. Documented column-weight side effects
  - Added note to ClusterConfig.weights docstring explaining that weights affect rescaling bounds, not just clustering distance
  - Added inline comment at rescale.py:79-83 explaining why scale_ub is multiplied by the weight

  Tests: 633 tests pass (485 + 148 golden), 75 skipped.

* Core pipeline (6 files):

  1. src/tsam/pipeline/normalize.py — Removed weights parameter from normalize(), removed apply_weights parameter from denormalize(), removed MIN_WEIGHT constant.
  Normalization now produces purely unweighted data.
  2. src/tsam/pipeline/types.py — Removed weights field from NormalizedData dataclass.
  3. src/tsam/period_aggregation.py — Added representation_candidates parameter to aggregate_periods(). When provided, representations() uses this array instead of the
  clustering candidates, allowing representatives to be computed from unweighted data while clustering uses weighted data.
  4. src/tsam/pipeline/clustering.py — Added weighted_candidates parameter to cluster_periods() and cluster_sorted_periods(). When provided, weighted data is passed to
  aggregate_periods() for distance computation while unweighted data is used for representation.
  5. src/tsam/pipeline/__init__.py — Added _weight_candidates() helper and _build_weight_vector(). Creates weighted candidates after unstacking, passes them to clustering
  functions. Updated rescale_representatives() call to pass original_data and normalize_column_means directly. Updated compute_accuracy() call to remove norm_data.
  6. src/tsam/pipeline/rescale.py — Changed signature to accept original_data and normalize_column_means directly instead of NormalizedData. Removed the weight compensation
  scale_ub *= weights[column] block.
  7. src/tsam/pipeline/accuracy.py — Removed weight-undo block from reconstruct(), simplified denormalize() call, simplified compute_accuracy() to compare directly without
  weight division.

  Tests (3 files):

  8. test/test_api_equivalence.py — Changed test_with_weights to compare cluster assignments instead of exact DataFrame equality (medoid selection intentionally differs).
  9. test/test_old_new_equivalence.py — Added _SKIP_EQUIVALENCE set and skip_equivalence flag on EquivalenceCase. Tests for representatives, accuracy, and reconstruction skip
   for weighted configs; cluster assignment test still runs.
  10. test/test_golden_regression.py — test_update_golden generates golden from new API for divergent configs. test_old_api_matches_golden skips for divergent configs.
  11. test/data/golden/hierarchical_weighted/testdata.csv — Regenerated from new API.

  Test results

  629 passed, 79 skipped, 0 failed (including test_weightingFactors.py which validates the equal-weight invariant on the old API).

* Update Changelog

* ⏺ Fixed three issues:

  1. src/tsam/config.py — ClusterConfig.weights docstring: removed the stale claim that weights affect rescaling bounds. Now correctly states weights apply to a separate copy
   used only for clustering distance.
  2. docs/source/glossary.md — Section 3 "Normalization & Weighting":
    - Column Weight: updated definition to say "applied to a separate copy", explicitly lists what weights do not affect.
    - Weighted Profile → Weighted Candidates: renamed to match the actual code variable name, updated definition to say "used exclusively as the distance matrix for
  clustering".
  3. docs/source/glossary.md — Section 7 "Rescaling":
    - Rescale Upper Bound: removed "column weight" from derivation, now says "derived from mean-normalization settings only".
  4. docs/source/glossary.md — Naming conventions and ambiguities resolved: weighted_profiles → weighted_candidates in both places.

* Test results: 644 passed, 79 skipped (up from 629 — the 15 new tests in test_weight_decoupling.py all pass)

  PR review items addressed:
  - Bug 1 (partial weight dict): Fixed — _build_weight_vector now emits entries for ALL columns, defaulting unlisted to 1.0
  - Bug 2 (cluster_sorted_periods ignoring weighted_candidates): Fixed — sorts weighted candidates separately and passes representation_candidates
  - print() → warnings.warn(): Fixed in _build_weight_vector
  - _count_occurrences float cast: Fixed int() → float()
  - Stale 3.1.2 changelog: Removed stale weight-rescaling bullet
  - Test gaps: All 15 new tests pass (unit tests for helpers, partial weights, duration curves + weights, deprecated property warning, JSON roundtrip)

  Remaining PR review items not requiring action:
  - to_dict() backward compat: AggregationResult.to_dict() has no from_dict() counterpart, and the rename happened before any release, so no migration path needed
  - column_weights vs weights naming: This is a naming nit — ClusteringResult.column_weights stores the serialized weight tuple, while ClusterConfig.weights is the
  user-facing dict. The two serve different roles so the naming difference is intentional

* Add Release notes

* Convert TimeSeriesAggregation from monolith to thin wrapper over run_pipeline()

Replace ~700 lines of duplicated algorithm code (normalize, cluster, rescale,
reconstruct, accuracy) with delegation to the pipeline. Both old and new APIs
now share a single code path.

- Replace create_typical_periods() body with run_pipeline() call + state extraction
- Replace predict_original_data() and accuracy_indicators() with pipeline result reads
- Add _translate_representation() and _build_pipeline_args() for parameter translation
- Delete _normalize/_unnormalize/_pre_process/_post_process/_add_extreme_periods/
  _rescale_cluster_periods/_cluster_sorted_periods internal methods
- Keep all __init__ validation, properties, index_matching(), and camelCase aliases
- Re-export aggregate_periods and representations for backward compat
- Delete test_preprocess.py (tested removed private methods)
- Remove normalized_time_series access from test_samemean.py
- Clear _SKIP_EQUIVALENCE set (old API now matches new API for weighted configs)
- Regenerate hierarchical_weighted golden CSV for weight-decoupled pipeline

647 passed, 75 skipped

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Merge Changelog

* Merge Changelog

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* Improve code quality across pipeline and representation modules

- Fix if→elif chain in aggregate_periods() to prevent fallthrough bugs
  and add else clause for unknown cluster methods
- Add type hints to representations.py and period_aggregation.py
- Rename 'indice' → 'indices', 'cluster_rep' → 'cluster_representation',
  'a' → 'iteration', '_alt_centers' → '_'
- Remove stale monolith line-number references from all pipeline modules
- DRY up extreme period detection via _detect_extreme() helper
- Precompute rep_values in minmax_mean_representation for performance
- Tighten return types in clustering.py (list→list[np.ndarray], etc.)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Restructure pipeline: unified weights, ClusteringResult.from_pipeline(), phase decomposition

- Add src/tsam/weights.py with validate_weights() consolidating column-existence
  checks and MIN_WEIGHT clamping; wire into api.py and timeseriesaggregation.py
  (old wrapper now rejects invalid weight column names)
- Move _build_clustering_result(), _extract_segment_data(), _infer_resolution()
  from pipeline/__init__.py to ClusteringResult classmethods/staticmethods in
  config.py; refactor apply() to reuse _infer_resolution()
- Decompose run_pipeline() into 4 phase functions (_prepare_data,
  _cluster_and_postprocess, _format_and_reconstruct, _assemble_result) with
  PreparedData, ClusteringOutput, FormattedOutput intermediate dataclasses
- Add TestValidateWeights tests in test_weight_decoupling.py

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: evalSumPeriods crash: strip extra columns before representations() (#29)

Period-sum features (include_period_sums/evalSumPeriods) are appended to
the candidate matrix for clustering distance only. They must not reach
representations() which expects original time-series columns.

Fix: route augmented candidates through weighted_candidates so
cluster_periods() uses them for distance but passes unaugmented
candidates to representations(). Duration-curve clustering skips
period sums entirely (not sortable columns).

Also: minor type cast fix in result.py residuals property.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: fix changelog to note weight decoupling affects both APIs

The legacy TimeSeriesAggregation class now uses the same pipeline
internally, so the weight semantics change applies to both APIs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: pipeline architecture improvements

- Add PipelineConfig dataclass to bundle 13 loose run_pipeline() params
- Add tsam.options module for runtime-tunable constants (rescale_max_iterations,
  rescale_tolerance, min_weight) replacing hardcoded magic numbers
- Rename normalize_column_means → scale_by_column_means (old name still works
  via backward-compat alias with FutureWarning)
- Remove implicit column sorting from normalize() — columns now preserve
  user's original order (old API still sorts explicitly)
- Remove NormalizedData.original_data redundancy — original data now stored
  once in PreparedData
- Fix non-deterministic extreme period reassignment in new_cluster method —
  now picks closest extreme by distance instead of last-wins loop order
- Add warning when column weights are silently ignored for duration-curve
  clustering with include_period_sums=True
- Fix type annotations throughout pipeline types (list → list[np.ndarray],
  dict → dict[str, dict], cluster_order always np.ndarray)
- Add @cached_property to result.assignments and result.plot
- Fix averaging method duplication in period_aggregation.py
- Remove unused _resolve_representation() from config.py
- Remove dead _apply_representation_params() from api.py
- Export tsam.options in __init__.py
- Regenerate golden files for new column order behavior

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add version field to ClusteringResult JSON + document behavioral changes

- ClusteringResult.to_dict() now includes a "version" field recording the
  tsam version that created the clustering. Old JSON files without a version
  field load with version=None for backward compat.
- Document extreme period determinism fix and DC+weights warning in changelog

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* perf: lazy accuracy computation — eliminates 2x benchmark regression

Accuracy indicators (RMSE, MAE, RMSE_duration) are now computed on
first access instead of eagerly during pipeline execution. This removes
a per-column sklearn loop from the hot path of createTypicalPeriods(),
which was the sole cause of a 1.5–2.5x slowdown vs develop on all
benchmark configs.

PipelineResult.accuracy_indicators and AggregationResult.accuracy are
now cached_property fields. The two DataFrames needed for the
computation (_norm_values, _normalized_predicted) are stored as private
fields and the result is cached after first access.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: update optimization_input notebook to use new AggregationResult API

Replace references to removed `result._aggregation` (old TimeSeriesAggregation
object) with the new public API equivalents:
- `result.assignments` instead of `result._aggregation.indexMatching()`
- `result.cluster_representatives.loc[...]` instead of `clusterPeriodDict`

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: eliminate dual candidate arrays, bake weights into candidates early

Remove _weight_candidates() and weighted_candidates field. Weights are
applied directly to candidates via vectorized np.repeat/multiply. The
weight_vector (np.ndarray) is stored on PreparedData for reuse in the
step 5 unweighting. Clustering functions no longer receive a separate
weighted_candidates parameter — they operate on already-weighted candidates.

Weights affect clustering distance, medoid/maxoid selection, and
segmentation boundaries. This is the intended design: "solar matters
100× more" means it matters everywhere. Updated comments throughout
the pipeline to reflect this decision and removed "remove this for
parity" language.

Updated ClusterConfig.weights docstring to document all three effects
and note that mean/distribution/minmaxmean are weight-invariant.

* docs: update weight handling docs for baked-in candidates refactor

Replace all references to weighted_candidates, _weight_candidates(), and
has_period_sums with the new weight_vector/baked-in pattern across
pipeline_guide, pipeline_dataflow, and glossary.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: detect extreme periods in weighted space to match develop

Move unweighting from before to after add_extreme_periods, so extremes
are detected and extracted in weighted space (matching develop's behavior
where normalizedPeriodlyProfiles carries weights). All representatives
(regular + extreme) are then unweighted uniformly before rescale/denorm.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: move unweighting to step 6b (after extremes), add weight-invariance note

Extremes now run in weighted space. Detection (per-column idxmax/idxmin)
is weight-invariant; only new_cluster distance reassignment is affected.
Unweighting moved from step 5 to step 6b so all representatives (regular
+ extreme) are unweighted uniformly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: re-apply v4 content to upstream's mkdocs format

- Add v3→v4 migration section to migration-guide.md
- Add v4.0.0 changelog entry
- Rename cluster_weights → cluster_counts in glossary and migration guide
- Add pipeline_guide.md and pipeline_dataflow.md under Learn nav
- Add pipeline docs to mkdocs.yml navigation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: merge pipeline_dataflow into pipeline_guide, update weights references

- Remove standalone pipeline_dataflow.md (-574 lines)
- Add key dataflow tables as collapsible sections in pipeline_guide.md
- Update weights references to reflect top-level parameter (not ClusterConfig)
- Remove pipeline_dataflow from mkdocs nav

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: remove stale API reference snapshots (auto-generated at build time)

gen_ref_pages.py regenerates these from src/ on every mkdocs build.
The static files referenced old module names and were missing pipeline/.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: restore v3 weighted duration-curve clustering and fix weight roundtrip test

cluster_sorted_periods now sorts and clusters on weighted candidates
(matching v3 behaviour) instead of unweighted profiles. Also fix
test_json_roundtrip_preserves_weights to use .weights instead of
the non-existent .column_weights property.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: make ClusterConfig picklable and use to_dict/from_dict in tuning serialization

ClusterConfig's custom __setattr__ immutability guard prevented pickle
from deserializing instances in worker subprocesses, breaking parallel
find_pareto_front. Added __getstate__/__setstate__ to bypass the guard.
Also replaced dataclasses.asdict() (which failed since ClusterConfig is
not a stdlib dataclass) with .to_dict()/.from_dict() for correct
serialization roundtrips, and removed duplicate :param: docstrings that
caused griffe warnings during mkdocs build.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: hoist weighted profiles into PreparedData and simplify clustering interface

Move weighted DataFrame construction into _prepare_data so it's computed
once and stored in PreparedData. Strip period-sum columns before calling
cluster_periods instead of inside it, and pass n_columns directly to
cluster_sorted_periods instead of the full PeriodProfiles object.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Remove manual Changelog entry

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
@FBumann
Copy link
Copy Markdown
Collaborator Author

FBumann commented Mar 26, 2026

@julian-belina I merged my rewrite into here.
Im open for adjustments and improvements, both to the code structure and to the api.
Ill track remaining work/issues in a milestone https://github.com/FZJ-IEK3-VSA/tsam/milestone/2

@julian-belina
Copy link
Copy Markdown
Collaborator

Thank you for your work, @FBumann! The general structure looks very promising. However, it's a big change. To improve my confidence in the change, I think I need to create some simple examples in which you can manually verify that the method worked as expected. This will be necessary for the core methods in Fine, at least: distribution, representation, and hierarchy.

I'm not sure when I'll have time to build these. Regarding feedback on the API, I will also ask @jo-omoyele and @phil-fzj for their input.

@FBumann
Copy link
Copy Markdown
Collaborator Author

FBumann commented Mar 26, 2026

@julian-belina Thanks! I know its a big change. Im very confident due to the golden regression tests we have!
But manual tests definitely wont hurt, and your confidence in the change is key before merging this.
I wont continue on this PR until you reviewed the current state!

There are only minor follow ups needed imo. All being design decisions we need to discuss AFTER you agree on the PR.

@FBumann
Copy link
Copy Markdown
Collaborator Author

FBumann commented Mar 26, 2026

@julian-belina I wanted to share:

I created a tsam wrapper package that allows to use it with multi-dimensional data using xarray

https://github.com/FBumann/tsam_xarray

@julian-belina
Copy link
Copy Markdown
Collaborator

@FBumann, this looks like a good addition. Perhaps we can integrate it directly into Tsam once V4 is released.

@FBumann
Copy link
Copy Markdown
Collaborator Author

FBumann commented Mar 26, 2026

Im not sure if this is a good idea tbh.
I would keep it separate, as it's a different datamodel, and adding it into tsam would really complicate the package.

  • every method needs to accept multiple datatypes
  • much more validation
  • some things are slightly different conceptually (weights)
  • what about plotting? Much more complex...
  • extra xarray dependency
  • testing grows
  • future development might be limited by the new multi dim layer. And slower.

I would rather link it prominently. As it matures, maybe we can think about including it directly.

@julian-belina
Copy link
Copy Markdown
Collaborator

julian-belina commented Mar 26, 2026

There is no time pressure, but reading from Xarray formats is considerably faster than reading from CSV formats, which would be great for a large energy system model. It's an incentive worth considering, in my opinion.

However, I also see the efforts. We can discuss this after the 4.0 release, once I have a better understanding of the new structure.

@FBumann
Copy link
Copy Markdown
Collaborator Author

FBumann commented Mar 26, 2026

There is no time pressure, but reading from Xarray formats is considerably faster than reading from CSV formats, which would be great for a large energy system model. It's an incentive worth considering, in my opinion.

However, I also see the efforts. We can discuss this after the 4.0 release, once I have a better understanding of the new structure.

If it's about data read and write, switch to .parquet.
This should be faster than xarray with netcdf.

FBumann and others added 4 commits March 30, 2026 15:27
* fix: make LegacyAPIWarning visible by default before v4 removal (#236) (#237)

Change LegacyAPIWarning base class from DeprecationWarning to
FutureWarning. DeprecationWarning is silenced by default in Python
(since 3.2) for library code, so users never saw the deprecation
notice. FutureWarning is always shown.

Also update all warning messages to explicitly mention removal in v4.0.

Closes #236

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: AccuracyMetrics now exposes weighted_rmse, weighted_mae, and weighted_rmse_duration as pre-computed scalars (#238)

* feat: add weighted aggregate metrics to AccuracyMetrics (#228)

Store pre-computed weighted_rmse, weighted_mae, and weighted_rmse_duration
on AccuracyMetrics so callers (including tuning) can read a single scalar
instead of computing ad-hoc. Default weight=1 collapses to unweighted mean.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add weighted aggregate metrics to AccuracyMetrics (#228)

Add three pre-computed scalar fields to AccuracyMetrics:
- weighted_rmse: weighted RMS of per-column RMSE
- weighted_mae: weighted arithmetic mean of per-column MAE
- weighted_rmse_duration: weighted RMS of per-column duration-curve RMSE

RMSE metrics use root-mean-square aggregation (preserves variance
interpretation), MAE uses plain weighted mean. Default weight=1 for
all columns collapses to unweighted aggregation, matching the old
totalAccuracyIndicators() values.

Tuning now reads result.accuracy.weighted_rmse directly instead of
computing ad-hoc via the removed _compute_rmse() helper.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* ci: add codecov coverage reporting (#243)

* CI: add codecov coverage reporting (#242)

Run pytest with --cov on a single matrix leg (self-hosted, Python 3.10)
to avoid overhead on all runs, then upload the XML report to Codecov.
Also adds codecov.yml with project/patch coverage status checks.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* docs: update codecov badge to track develop branch

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* CI: run pytest only once by conditionally adding coverage flags

Use GITHUB_ENV to set --cov flags on the coverage leg, so a single
pytest invocation handles both testing and coverage measurement.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* CI: migrate to v1 reusable workflows and add coverage reporting (#244)

- Migrate all shared workflow references from legacy `_*` workflows to
  their v1 equivalents from FZJ-IEK3-VSA/.github@v1 (cleaner top-level
  paths, consolidated installer variants)
- Remove inline coverage from ci-develop; move to standalone coverage.yaml
  that runs pytest --cov on push to develop and uploads to Codecov
- Replace inline push_to_jugit steps with the shared sync_jugit.yml
  reusable workflow
- Clean up codecov.yml: remove unused patch status block, raise drop
  threshold to 5%

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* ci: use GitHub App token in release workflow to trigger publish (#246)

The default GITHUB_TOKEN cannot trigger downstream workflows (GitHub's
anti-cascade rule). This caused v3.2.1's tag push from release-please
to silently not trigger the Publish workflow, so it never reached PyPI.

Use actions/create-github-app-token to mint a short-lived token from a
GitHub App, which *does* trigger downstream workflows.

Requires two repository secrets:
- RELEASE_APP_ID
- RELEASE_APP_PRIVATE_KEY

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Julian Belina <56728940+julian-belina@users.noreply.github.com>

* chore(deps): Update github-actions (#251)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore(deps): Update dependency numpy to >=1.22.4,<=2.4.4 (#247)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Julian Belina <56728940+julian-belina@users.noreply.github.com>

* chore(deps): Update dependency ruff to <=0.15.8 (#248)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* feat: add disaggregate() method (#245)

* feat: add disaggregate() to expand typical periods back to full time series

Adds `ClusteringResult.disaggregate()` and `AggregationResult.disaggregate()`
to map arbitrary typical-period data (e.g. optimization results) back to the
original time series length using stored cluster assignments.

- Non-segmented: expands periods by cluster order, matches .reconstructed
- Segmented: expands segments to timesteps (NaN between segment starts),
  then expands periods. Users can .ffill() for step-function behavior.
- Survives IO: works after to_json()/from_json() roundtrip
- Extracts _expand_periods helper, shared with predictOriginalData()

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: detect segmented vs timestep input from data, not clustering

disaggregate() now accepts both (cluster, timestep) and (cluster, segment,
duration) input regardless of whether the clustering used segmentation.
Segment expansion only happens when the input actually has segment-level
indices (3+ levels).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: strict input validation — segmented data requires segmented clustering and vice versa

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address review — level=-1 for MultiIndex columns, per-cluster validation, numpy import

- _expand_periods: stack(level=-1) instead of level=1 to correctly handle
  MultiIndex columns (avoids silent data corruption)
- _validate_disaggregate_input: per-cluster segment/timestep count check
  instead of global nunique
- _expand_segments_to_timesteps: dict-based duration lookup by cluster ID
  instead of positional indexing
- Move numpy import to module level
- Update AggregationResult.disaggregate docstring to mention segmented input
- Add test for MultiIndex columns

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: add edge case coverage for disaggregate

- Padded last period (non-divisible length)
- Single cluster (n_clusters=1)
- Extreme periods (append, new_cluster)
- kmeans clustering method
- Segmented NaN at correct positions (segment starts have values)
- Regression: predictOriginalData still matches via shared _expand_periods

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: add disaggregation notebook

Demonstrates:
- Basic disaggregation (matches .reconstructed)
- Disaggregating arbitrary data (optimization results)
- IO roundtrip (save/load clustering, then disaggregate)
- Segmented data with NaN and ffill

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: link disaggregation notebook in mkdocs nav

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: stop tracking clustering.json notebook artifact

Generated by clustering_transfer.ipynb and disaggregation.ipynb at
notebook execution time. Should not be in version control.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Julian Belina <56728940+julian-belina@users.noreply.github.com>

* ci: build docs in Renovate dependency tests (#253)

Adds docs_build_command to the Renovate test job so documentation is
also built when Renovate updates dependencies, catching breakage early.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* ci: renovate regex not matching `<` constraints in environment.yml (#254)

* fix: renovate regex not matching `<` constraints in environment.yml

The custom regex manager only matched `<=` upper bounds, so dependencies
using strict `<` (e.g. mkdocs-jupyter `<0.26`) were silently skipped.
Widen the regex to `<=?` and normalise mkdocs-jupyter to `<=0.25.1`.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: use replace strategy for Python version in setup-python

The global `rangeStrategy: "widen"` caused Renovate to propose
`"3.12 || 3.14"` instead of simply replacing with `"3.14"`.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* ci: update Python to 3.14 in docs and publish jobs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* chore(deps): Update dependency mkdocs-jupyter to >=0.25,<0.27 (#249)

* chore(deps): Update dependency mkdocs-jupyter to >=0.25,<0.27

* Update pyproject.toml

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Julian Belina <56728940+julian-belina@users.noreply.github.com>

* chore(master): release 3.3.0

* fix(ci): gracefully skip GitHub release when already created by release-please (#260)

The publish workflow's `github-release` job conflicted with release-please,
which already creates releases for stable tags. Now the job attempts to create
the release and silently skips if it already exists.

The job is kept because manual prereleases (e.g. `v3.3.0rc1`) are not handled
by release-please — this job provides their changelog via `--generate-notes`.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Julian Belina <56728940+julian-belina@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: fzj-iek3-vsa-release-bot[bot] <272156123+fzj-iek3-vsa-release-bot[bot]@users.noreply.github.com>
# Conflicts:
#	CHANGELOG.md
#	src/tsam/api.py
#	src/tsam/config.py
#	src/tsam/timeseriesaggregation.py
The v4 pipeline bakes weights into candidates (step 2b), matching
the old API's behavior. Weighted configs no longer diverge between
old and new APIs, so equivalence tests should run for them too.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add _weights field to AggregationResult
- Pass clustering weights through _build_aggregation_result
- Lazy accuracy property now uses actual column weights for
  weighted_rmse, weighted_mae, weighted_rmse_duration
- Update test_weight_handling.py from camelCase to snake_case v4 API

Refs: #256

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@FBumann
Copy link
Copy Markdown
Collaborator Author

FBumann commented Apr 21, 2026

@julian-belina Any updates?
#235 #255 #256 #257 wait to merged into here. But only after your initial review!

@FBumann FBumann added this to the v4 milestone Apr 21, 2026
@julian-belina
Copy link
Copy Markdown
Collaborator

@FBumann, it's on my to-do list, but I'm super busy unfortunately. I hope I can get to it by the end of the week or next week. It's definitely not forgotten!

@FBumann
Copy link
Copy Markdown
Collaborator Author

FBumann commented Apr 21, 2026

@julian-belina No worries, just checking back. I wont have time either the next few weeks to tackle those issues.

@l-kotzur
Copy link
Copy Markdown
Collaborator

This looks really promising!

@FBumann
Copy link
Copy Markdown
Collaborator Author

FBumann commented Apr 21, 2026

@l-kotzur great to hear!

Due to the existing sophisticated testing im also very confident it preserves behaviour entirely!

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.

3 participants