tsam 3.0.0__tar.gz → 3.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. {tsam-3.0.0/src/tsam.egg-info → tsam-3.1.0}/PKG-INFO +10 -11
  2. {tsam-3.0.0 → tsam-3.1.0}/README.md +2 -2
  3. {tsam-3.0.0 → tsam-3.1.0}/environment.yml +2 -2
  4. {tsam-3.0.0 → tsam-3.1.0}/pyproject.toml +5 -5
  5. {tsam-3.0.0 → tsam-3.1.0}/src/tsam/api.py +1 -0
  6. {tsam-3.0.0 → tsam-3.1.0}/src/tsam/config.py +42 -3
  7. {tsam-3.0.0 → tsam-3.1.0}/src/tsam/timeseriesaggregation.py +85 -5
  8. {tsam-3.0.0 → tsam-3.1.0/src/tsam.egg-info}/PKG-INFO +10 -11
  9. {tsam-3.0.0 → tsam-3.1.0}/src/tsam.egg-info/requires.txt +2 -3
  10. tsam-3.1.0/test/test_extremePeriods.py +237 -0
  11. tsam-3.0.0/test/test_extremePeriods.py +0 -87
  12. {tsam-3.0.0 → tsam-3.1.0}/LICENSE.txt +0 -0
  13. {tsam-3.0.0 → tsam-3.1.0}/MANIFEST.in +0 -0
  14. {tsam-3.0.0 → tsam-3.1.0}/setup.cfg +0 -0
  15. {tsam-3.0.0 → tsam-3.1.0}/src/tsam/__init__.py +0 -0
  16. {tsam-3.0.0 → tsam-3.1.0}/src/tsam/exceptions.py +0 -0
  17. {tsam-3.0.0 → tsam-3.1.0}/src/tsam/hyperparametertuning.py +0 -0
  18. {tsam-3.0.0 → tsam-3.1.0}/src/tsam/periodAggregation.py +0 -0
  19. {tsam-3.0.0 → tsam-3.1.0}/src/tsam/plot.py +0 -0
  20. {tsam-3.0.0 → tsam-3.1.0}/src/tsam/py.typed +0 -0
  21. {tsam-3.0.0 → tsam-3.1.0}/src/tsam/representations.py +0 -0
  22. {tsam-3.0.0 → tsam-3.1.0}/src/tsam/result.py +0 -0
  23. {tsam-3.0.0 → tsam-3.1.0}/src/tsam/tuning.py +0 -0
  24. {tsam-3.0.0 → tsam-3.1.0}/src/tsam/utils/__init__.py +0 -0
  25. {tsam-3.0.0 → tsam-3.1.0}/src/tsam/utils/durationRepresentation.py +0 -0
  26. {tsam-3.0.0 → tsam-3.1.0}/src/tsam/utils/k_maxoids.py +0 -0
  27. {tsam-3.0.0 → tsam-3.1.0}/src/tsam/utils/k_medoids_contiguity.py +0 -0
  28. {tsam-3.0.0 → tsam-3.1.0}/src/tsam/utils/k_medoids_exact.py +0 -0
  29. {tsam-3.0.0 → tsam-3.1.0}/src/tsam/utils/segmentation.py +0 -0
  30. {tsam-3.0.0 → tsam-3.1.0}/src/tsam.egg-info/SOURCES.txt +0 -0
  31. {tsam-3.0.0 → tsam-3.1.0}/src/tsam.egg-info/dependency_links.txt +0 -0
  32. {tsam-3.0.0 → tsam-3.1.0}/src/tsam.egg-info/top_level.txt +0 -0
  33. {tsam-3.0.0 → tsam-3.1.0}/test/test_accuracyIndicators.py +0 -0
  34. {tsam-3.0.0 → tsam-3.1.0}/test/test_adjacent_periods.py +0 -0
  35. {tsam-3.0.0 → tsam-3.1.0}/test/test_aggregate_hiearchical.py +0 -0
  36. {tsam-3.0.0 → tsam-3.1.0}/test/test_api_equivalence.py +0 -0
  37. {tsam-3.0.0 → tsam-3.1.0}/test/test_assert_raises.py +0 -0
  38. {tsam-3.0.0 → tsam-3.1.0}/test/test_averaging.py +0 -0
  39. {tsam-3.0.0 → tsam-3.1.0}/test/test_cluster_order.py +0 -0
  40. {tsam-3.0.0 → tsam-3.1.0}/test/test_clustering_e2e.py +0 -0
  41. {tsam-3.0.0 → tsam-3.1.0}/test/test_durationCurve.py +0 -0
  42. {tsam-3.0.0 → tsam-3.1.0}/test/test_durationRepresentation.py +0 -0
  43. {tsam-3.0.0 → tsam-3.1.0}/test/test_hierarchical.py +0 -0
  44. {tsam-3.0.0 → tsam-3.1.0}/test/test_hypertuneAggregation.py +0 -0
  45. {tsam-3.0.0 → tsam-3.1.0}/test/test_k_maxoids.py +0 -0
  46. {tsam-3.0.0 → tsam-3.1.0}/test/test_k_medoids.py +0 -0
  47. {tsam-3.0.0 → tsam-3.1.0}/test/test_k_medoids_contiguity.py +0 -0
  48. {tsam-3.0.0 → tsam-3.1.0}/test/test_minmaxRepresentation.py +0 -0
  49. {tsam-3.0.0 → tsam-3.1.0}/test/test_new_api.py +0 -0
  50. {tsam-3.0.0 → tsam-3.1.0}/test/test_preprocess.py +0 -0
  51. {tsam-3.0.0 → tsam-3.1.0}/test/test_properties.py +0 -0
  52. {tsam-3.0.0 → tsam-3.1.0}/test/test_reconstruct_samemean_segmentation.py +0 -0
  53. {tsam-3.0.0 → tsam-3.1.0}/test/test_samemean.py +0 -0
  54. {tsam-3.0.0 → tsam-3.1.0}/test/test_segmentation.py +0 -0
  55. {tsam-3.0.0 → tsam-3.1.0}/test/test_subhourlyResolution.py +0 -0
  56. {tsam-3.0.0 → tsam-3.1.0}/test/test_subhourly_periods.py +0 -0
  57. {tsam-3.0.0 → tsam-3.1.0}/test/test_weightingFactors.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tsam
3
- Version: 3.0.0
3
+ Version: 3.1.0
4
4
  Summary: Time series aggregation module (tsam) to create typical periods
5
5
  Author-email: Leander Kotzur <leander.kotzur@googlemail.com>, Maximilian Hoffmann <maximilian.hoffmann@julumni.fz-juelich.de>
6
6
  Maintainer-email: Julian Belina <j.belina@fz-juelich.de>
@@ -49,14 +49,8 @@ Requires-Dist: pandas<=3.0.0,>=2.2.0
49
49
  Requires-Dist: numpy<=2.4.1,>=1.22.4
50
50
  Requires-Dist: pyomo<=6.95,>=6.4.8
51
51
  Requires-Dist: networkx<=3.6.1,>=2.5
52
- Requires-Dist: tqdm<=4.67.1,>=4.21.0
52
+ Requires-Dist: tqdm<=4.67.2,>=4.21.0
53
53
  Requires-Dist: highspy<=1.12.0,>=1.7.2
54
- Provides-Extra: plot
55
- Requires-Dist: plotly>=5.0.0; extra == "plot"
56
- Provides-Extra: notebooks
57
- Requires-Dist: notebook>=7.5.0; extra == "notebooks"
58
- Requires-Dist: plotly>=5.0.0; extra == "notebooks"
59
- Requires-Dist: matplotlib; extra == "notebooks"
60
54
  Provides-Extra: develop
61
55
  Requires-Dist: pytest; extra == "develop"
62
56
  Requires-Dist: pytest-cov; extra == "develop"
@@ -65,6 +59,7 @@ Requires-Dist: codecov; extra == "develop"
65
59
  Requires-Dist: sphinx; extra == "develop"
66
60
  Requires-Dist: sphinx-autobuild; extra == "develop"
67
61
  Requires-Dist: sphinx_book_theme; extra == "develop"
62
+ Requires-Dist: nbsphinx; extra == "develop"
68
63
  Requires-Dist: twine; extra == "develop"
69
64
  Requires-Dist: nbval; extra == "develop"
70
65
  Requires-Dist: ruff; extra == "develop"
@@ -73,7 +68,11 @@ Requires-Dist: pandas-stubs; extra == "develop"
73
68
  Requires-Dist: pre-commit; extra == "develop"
74
69
  Requires-Dist: plotly>=5.0.0; extra == "develop"
75
70
  Requires-Dist: notebook>=7.5.0; extra == "develop"
76
- Requires-Dist: matplotlib; extra == "develop"
71
+ Provides-Extra: plot
72
+ Requires-Dist: plotly>=5.0.0; extra == "plot"
73
+ Provides-Extra: notebooks
74
+ Requires-Dist: notebook>=7.5.0; extra == "notebooks"
75
+ Requires-Dist: plotly>=5.0.0; extra == "notebooks"
77
76
  Dynamic: license-file
78
77
 
79
78
  [![Version](https://img.shields.io/pypi/v/tsam.svg)](https://pypi.python.org/pypi/tsam) [![Conda Version](https://img.shields.io/conda/vn/conda-forge/tsam.svg)](https://anaconda.org/conda-forge/tsam) [![Documentation Status](https://readthedocs.org/projects/tsam/badge/?version=latest)](https://tsam.readthedocs.io/en/latest/) [![PyPI - License](https://img.shields.io/pypi/l/tsam)]((https://github.com/FZJ-IEK3-VSA/tsam/blob/master/LICENSE.txt)) [![codecov](https://codecov.io/gh/FZJ-IEK3-VSA/tsam/branch/master/graph/badge.svg)](https://codecov.io/gh/FZJ-IEK3-VSA/tsam)
@@ -217,9 +216,9 @@ cluster_representatives = aggregation.createTypicalPeriods()
217
216
  ### Detailed examples
218
217
  Detailed examples can be found at:/docs/source/examples_notebooks/
219
218
 
220
- A [**first example**](/docs/source/examples_notebooks/aggregation_example.ipynb) shows the capabilites of tsam as jupyter notebook.
219
+ A [**quickstart example**](/docs/source/examples_notebooks/quickstart.ipynb) shows the capabilities of tsam as a Jupyter notebook.
221
220
 
222
- A [**second example**](/docs/source/examples_notebooks/aggregation_optiinput.ipynb) shows in more detail how to access the relevant aggregation results required for paramtrizing e.g. an optimization.
221
+ A [**second example**](/docs/source/examples_notebooks/optimization_input.ipynb) shows in more detail how to access the relevant aggregation results required for parameterizing e.g. an optimization.
223
222
 
224
223
  The example time series are based on a department [publication](https://www.mdpi.com/1996-1073/10/3/361) and the [test reference years of the DWD](https://www.dwd.de/DE/leistungen/testreferenzjahre/testreferenzjahre.html).
225
224
 
@@ -139,9 +139,9 @@ cluster_representatives = aggregation.createTypicalPeriods()
139
139
  ### Detailed examples
140
140
  Detailed examples can be found at:/docs/source/examples_notebooks/
141
141
 
142
- A [**first example**](/docs/source/examples_notebooks/aggregation_example.ipynb) shows the capabilites of tsam as jupyter notebook.
142
+ A [**quickstart example**](/docs/source/examples_notebooks/quickstart.ipynb) shows the capabilities of tsam as a Jupyter notebook.
143
143
 
144
- A [**second example**](/docs/source/examples_notebooks/aggregation_optiinput.ipynb) shows in more detail how to access the relevant aggregation results required for paramtrizing e.g. an optimization.
144
+ A [**second example**](/docs/source/examples_notebooks/optimization_input.ipynb) shows in more detail how to access the relevant aggregation results required for parameterizing e.g. an optimization.
145
145
 
146
146
  The example time series are based on a department [publication](https://www.mdpi.com/1996-1073/10/3/361) and the [test reference years of the DWD](https://www.dwd.de/DE/leistungen/testreferenzjahre/testreferenzjahre.html).
147
147
 
@@ -10,9 +10,8 @@ dependencies:
10
10
  - numpy >=1.22.4,<=2.4.1
11
11
  - pyomo >=6.4.3,<=6.95
12
12
  - networkx >=2.5,<=3.6.1
13
- - tqdm >=4.21.0,<=4.67.1
13
+ - tqdm >=4.21.0,<=4.67.2
14
14
  - highspy >=1.7.2,<=1.12.0
15
- - matplotlib
16
15
  - plotly >=5.0.0
17
16
  # Testing
18
17
  - pytest
@@ -23,6 +22,7 @@ dependencies:
23
22
  # Documentation
24
23
  - sphinx
25
24
  - sphinx-autobuild
25
+ - nbsphinx
26
26
  - sphinx-book-theme
27
27
  # Linting and formatting
28
28
  - ruff
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
 
6
6
  [project]
7
7
  name = "tsam"
8
- version = "3.0.0"
8
+ version = "3.1.0"
9
9
  description = "Time series aggregation module (tsam) to create typical periods"
10
10
  authors = [
11
11
  { name = "Leander Kotzur", email = "leander.kotzur@googlemail.com" },
@@ -37,15 +37,13 @@ dependencies = [
37
37
  "numpy>=1.22.4,<=2.4.1",
38
38
  "pyomo>=6.4.8,<=6.95",
39
39
  "networkx>=2.5,<=3.6.1",
40
- "tqdm>=4.21.0,<=4.67.1",
40
+ "tqdm>=4.21.0,<=4.67.2",
41
41
  "highspy>=1.7.2,<=1.12.0",
42
42
  ]
43
43
 
44
44
  requires-python = ">=3.10,<3.15"
45
45
 
46
46
  [project.optional-dependencies]
47
- plot = ["plotly>=5.0.0"]
48
- notebooks = ["notebook>=7.5.0", "plotly>=5.0.0", "matplotlib"]
49
47
  develop = [
50
48
  "pytest",
51
49
  "pytest-cov",
@@ -54,6 +52,7 @@ develop = [
54
52
  "sphinx",
55
53
  "sphinx-autobuild",
56
54
  "sphinx_book_theme",
55
+ "nbsphinx",
57
56
  "twine",
58
57
  "nbval",
59
58
  "ruff",
@@ -62,8 +61,9 @@ develop = [
62
61
  "pre-commit",
63
62
  "plotly>=5.0.0",
64
63
  "notebook>=7.5.0",
65
- "matplotlib",
66
64
  ]
65
+ plot = ["plotly>=5.0.0"]
66
+ notebooks = ["notebook>=7.5.0", "plotly>=5.0.0"]
67
67
 
68
68
  [tool.setuptools.packages.find]
69
69
  where = ["src"]
@@ -536,6 +536,7 @@ def _build_old_params(
536
536
  params["addPeakMin"] = extremes.min_value
537
537
  params["addMeanMax"] = extremes.max_period
538
538
  params["addMeanMin"] = extremes.min_period
539
+ params["extremePreserveNumClusters"] = extremes._effective_preserve_n_clusters
539
540
  else:
540
541
  params["extremePeriodMethod"] = "None"
541
542
 
@@ -606,9 +606,10 @@ class ClusteringResult:
606
606
  ):
607
607
  warnings.warn(
608
608
  "The 'replace' extreme method creates a hybrid cluster representation "
609
- "(some columns from the medoid, some from the extreme period) that cannot "
610
- "be perfectly reproduced during transfer. The transferred result will use "
611
- "the medoid representation for all columns instead of the hybrid values. "
609
+ "(some columns from the cluster representative, some from the extreme period) "
610
+ "that cannot be perfectly reproduced during transfer. The transferred result "
611
+ "will use the stored cluster center periods directly, without the extreme "
612
+ "value injection that was applied during the original aggregation. "
612
613
  "For exact transfer, use 'append' or 'new_cluster' extreme methods.",
613
614
  UserWarning,
614
615
  stacklevel=2,
@@ -785,6 +786,18 @@ class ExtremeConfig:
785
786
  min_period : list[str], optional
786
787
  Column names where the period with minimum total should be preserved.
787
788
  Example: ["wind_generation"] to preserve lowest wind day.
789
+
790
+ preserve_n_clusters : bool, optional
791
+ Whether extreme periods count toward n_clusters.
792
+ - True: Extremes are included in n_clusters
793
+ (e.g., n_clusters=10 with 2 extremes = 8 from clustering + 2 extremes)
794
+ - False: Extremes are added on top of n_clusters (old api behaviour)
795
+ (e.g., n_clusters=10 + 2 extremes = 12 final clusters)
796
+ Only affects "append" or "new_cluster" methods ("replace" never changes n_clusters).
797
+
798
+ .. deprecated::
799
+ The default will change from False to True in a future release.
800
+ Set explicitly to silence the FutureWarning.
788
801
  """
789
802
 
790
803
  method: ExtremeMethod = "append"
@@ -792,6 +805,18 @@ class ExtremeConfig:
792
805
  min_value: list[str] = field(default_factory=list)
793
806
  max_period: list[str] = field(default_factory=list)
794
807
  min_period: list[str] = field(default_factory=list)
808
+ preserve_n_clusters: bool | None = None
809
+
810
+ def __post_init__(self) -> None:
811
+ """Emit FutureWarning if preserve_n_clusters is not explicitly set."""
812
+ if self.preserve_n_clusters is None and self.has_extremes():
813
+ warnings.warn(
814
+ "preserve_n_clusters currently defaults to False to match behaviour of the old api, "
815
+ "but will default to True in a future release. Set preserve_n_clusters explicitly "
816
+ "to silence this warning.",
817
+ FutureWarning,
818
+ stacklevel=3,
819
+ )
795
820
 
796
821
  def has_extremes(self) -> bool:
797
822
  """Check if any extreme periods are configured."""
@@ -799,6 +824,17 @@ class ExtremeConfig:
799
824
  self.max_value or self.min_value or self.max_period or self.min_period
800
825
  )
801
826
 
827
+ @property
828
+ def _effective_preserve_n_clusters(self) -> bool:
829
+ """Get the effective value for preserve_n_clusters.
830
+
831
+ Returns False if not explicitly set (current default behavior).
832
+ In a future release, the default will change to True.
833
+ """
834
+ if self.preserve_n_clusters is None:
835
+ return False # Current default, will change to True in future
836
+ return self.preserve_n_clusters
837
+
802
838
  def to_dict(self) -> dict[str, Any]:
803
839
  """Convert to dictionary for JSON serialization."""
804
840
  result: dict[str, Any] = {}
@@ -812,6 +848,8 @@ class ExtremeConfig:
812
848
  result["max_period"] = self.max_period
813
849
  if self.min_period:
814
850
  result["min_period"] = self.min_period
851
+ if self.preserve_n_clusters is not None:
852
+ result["preserve_n_clusters"] = self.preserve_n_clusters
815
853
  return result
816
854
 
817
855
  @classmethod
@@ -823,6 +861,7 @@ class ExtremeConfig:
823
861
  min_value=data.get("min_value", []),
824
862
  max_period=data.get("max_period", []),
825
863
  min_period=data.get("min_period", []),
864
+ preserve_n_clusters=data.get("preserve_n_clusters"),
826
865
  )
827
866
 
828
867
 
@@ -132,6 +132,7 @@ class TimeSeriesAggregation:
132
132
  weightDict=None,
133
133
  segmentation=False,
134
134
  extremePeriodMethod="None",
135
+ extremePreserveNumClusters=False,
135
136
  representationMethod=None,
136
137
  representationDict=None,
137
138
  distributionPeriodWise=True,
@@ -318,6 +319,8 @@ class TimeSeriesAggregation:
318
319
 
319
320
  self.extremePeriodMethod = extremePeriodMethod
320
321
 
322
+ self.extremePreserveNumClusters = extremePreserveNumClusters
323
+
321
324
  self.evalSumPeriods = evalSumPeriods
322
325
 
323
326
  self.sortValues = sortValues
@@ -683,6 +686,46 @@ class TimeSeriesAggregation:
683
686
 
684
687
  return unnormalizedTimeSeries
685
688
 
689
+ def _countExtremePeriods(self, groupedSeries):
690
+ """
691
+ Count unique extreme periods without modifying any state.
692
+
693
+ Used by extremePreserveNumClusters to determine how many clusters
694
+ to reserve for extreme periods before clustering.
695
+
696
+ Note: The extreme-finding logic (idxmax/idxmin on peak/mean) must
697
+ stay in sync with _addExtremePeriods. This is intentionally separate
698
+ because _addExtremePeriods also filters out periods that are already
699
+ cluster centers (not known at count time).
700
+ """
701
+ extremePeriodIndices = set()
702
+
703
+ # Only iterate over columns that are actually in extreme lists
704
+ extreme_columns = (
705
+ set(self.addPeakMax)
706
+ | set(self.addPeakMin)
707
+ | set(self.addMeanMax)
708
+ | set(self.addMeanMin)
709
+ )
710
+
711
+ for column in extreme_columns:
712
+ col_data = groupedSeries[column]
713
+
714
+ if column in self.addPeakMax:
715
+ extremePeriodIndices.add(col_data.max(axis=1).idxmax())
716
+ if column in self.addPeakMin:
717
+ extremePeriodIndices.add(col_data.min(axis=1).idxmin())
718
+
719
+ # Compute mean only once if needed for either addMeanMax or addMeanMin
720
+ if column in self.addMeanMax or column in self.addMeanMin:
721
+ mean_series = col_data.mean(axis=1)
722
+ if column in self.addMeanMax:
723
+ extremePeriodIndices.add(mean_series.idxmax())
724
+ if column in self.addMeanMin:
725
+ extremePeriodIndices.add(mean_series.idxmin())
726
+
727
+ return len(extremePeriodIndices)
728
+
686
729
  def _addExtremePeriods(
687
730
  self,
688
731
  groupedSeries,
@@ -983,7 +1026,7 @@ class TimeSeriesAggregation:
983
1026
  # Reshape back to 2D: (n_clusters, n_cols * n_timesteps)
984
1027
  return arr.reshape(n_clusters, -1)
985
1028
 
986
- def _clusterSortedPeriods(self, candidates, n_init=20):
1029
+ def _clusterSortedPeriods(self, candidates, n_init=20, n_clusters=None):
987
1030
  """
988
1031
  Runs the clustering algorithms for the sorted profiles within the period
989
1032
  instead of the original profiles. (Duration curve clustering)
@@ -1001,13 +1044,16 @@ class TimeSeriesAggregation:
1001
1044
  n_periods, -1
1002
1045
  )
1003
1046
 
1047
+ if n_clusters is None:
1048
+ n_clusters = self.noTypicalPeriods
1049
+
1004
1050
  (
1005
1051
  _altClusterCenters,
1006
1052
  self.clusterCenterIndices,
1007
1053
  clusterOrders_C,
1008
1054
  ) = aggregatePeriods(
1009
1055
  sortedClusterValues,
1010
- n_clusters=self.noTypicalPeriods,
1056
+ n_clusters=n_clusters,
1011
1057
  n_iter=30,
1012
1058
  solver=self.solver,
1013
1059
  clusterMethod=self.clusterMethod,
@@ -1052,6 +1098,41 @@ class TimeSeriesAggregation:
1052
1098
  """
1053
1099
  self._preProcessTimeSeries()
1054
1100
 
1101
+ # Warn if extremePreserveNumClusters is ignored due to predefined cluster order
1102
+ if (
1103
+ self.predefClusterOrder is not None
1104
+ and self.extremePreserveNumClusters
1105
+ and self.extremePeriodMethod not in ("None", "replace_cluster_center")
1106
+ ):
1107
+ warnings.warn(
1108
+ "extremePreserveNumClusters=True is ignored when predefClusterOrder "
1109
+ "is set. Extreme periods will be appended via _addExtremePeriods "
1110
+ "without reserving clusters upfront. To avoid this warning, set "
1111
+ "extremePreserveNumClusters=False or remove predefClusterOrder.",
1112
+ UserWarning,
1113
+ stacklevel=2,
1114
+ )
1115
+
1116
+ # Count extreme periods upfront if include_in_count is True
1117
+ # Note: replace_cluster_center doesn't add new clusters, so skip
1118
+ n_extremes = 0
1119
+ if (
1120
+ self.extremePreserveNumClusters
1121
+ and self.extremePeriodMethod not in ("None", "replace_cluster_center")
1122
+ and self.predefClusterOrder is None # Don't count for predefined
1123
+ ):
1124
+ n_extremes = self._countExtremePeriods(self.normalizedPeriodlyProfiles)
1125
+
1126
+ if self.noTypicalPeriods <= n_extremes:
1127
+ raise ValueError(
1128
+ f"n_clusters ({self.noTypicalPeriods}) must be greater than "
1129
+ f"the number of extreme periods ({n_extremes}) when "
1130
+ "preserve_n_clusters=True"
1131
+ )
1132
+
1133
+ # Compute effective number of clusters for the clustering algorithm
1134
+ effective_n_clusters = self.noTypicalPeriods - n_extremes
1135
+
1055
1136
  # check for additional cluster parameters
1056
1137
  if self.evalSumPeriods:
1057
1138
  evaluationValues = (
@@ -1096,7 +1177,7 @@ class TimeSeriesAggregation:
1096
1177
  self._clusterOrder,
1097
1178
  ) = aggregatePeriods(
1098
1179
  candidates,
1099
- n_clusters=self.noTypicalPeriods,
1180
+ n_clusters=effective_n_clusters,
1100
1181
  n_iter=100,
1101
1182
  solver=self.solver,
1102
1183
  clusterMethod=self.clusterMethod,
@@ -1107,7 +1188,7 @@ class TimeSeriesAggregation:
1107
1188
  )
1108
1189
  else:
1109
1190
  self.clusterCenters, self._clusterOrder = self._clusterSortedPeriods(
1110
- candidates
1191
+ candidates, n_clusters=effective_n_clusters
1111
1192
  )
1112
1193
  self.clusteringDuration = time.time() - cluster_duration
1113
1194
 
@@ -1117,7 +1198,6 @@ class TimeSeriesAggregation:
1117
1198
  self.clusterPeriods.append(cluster_center[:delClusterParams])
1118
1199
 
1119
1200
  if not self.extremePeriodMethod == "None":
1120
- # overwrite clusterPeriods and clusterOrder
1121
1201
  (
1122
1202
  self.clusterPeriods,
1123
1203
  self._clusterOrder,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tsam
3
- Version: 3.0.0
3
+ Version: 3.1.0
4
4
  Summary: Time series aggregation module (tsam) to create typical periods
5
5
  Author-email: Leander Kotzur <leander.kotzur@googlemail.com>, Maximilian Hoffmann <maximilian.hoffmann@julumni.fz-juelich.de>
6
6
  Maintainer-email: Julian Belina <j.belina@fz-juelich.de>
@@ -49,14 +49,8 @@ Requires-Dist: pandas<=3.0.0,>=2.2.0
49
49
  Requires-Dist: numpy<=2.4.1,>=1.22.4
50
50
  Requires-Dist: pyomo<=6.95,>=6.4.8
51
51
  Requires-Dist: networkx<=3.6.1,>=2.5
52
- Requires-Dist: tqdm<=4.67.1,>=4.21.0
52
+ Requires-Dist: tqdm<=4.67.2,>=4.21.0
53
53
  Requires-Dist: highspy<=1.12.0,>=1.7.2
54
- Provides-Extra: plot
55
- Requires-Dist: plotly>=5.0.0; extra == "plot"
56
- Provides-Extra: notebooks
57
- Requires-Dist: notebook>=7.5.0; extra == "notebooks"
58
- Requires-Dist: plotly>=5.0.0; extra == "notebooks"
59
- Requires-Dist: matplotlib; extra == "notebooks"
60
54
  Provides-Extra: develop
61
55
  Requires-Dist: pytest; extra == "develop"
62
56
  Requires-Dist: pytest-cov; extra == "develop"
@@ -65,6 +59,7 @@ Requires-Dist: codecov; extra == "develop"
65
59
  Requires-Dist: sphinx; extra == "develop"
66
60
  Requires-Dist: sphinx-autobuild; extra == "develop"
67
61
  Requires-Dist: sphinx_book_theme; extra == "develop"
62
+ Requires-Dist: nbsphinx; extra == "develop"
68
63
  Requires-Dist: twine; extra == "develop"
69
64
  Requires-Dist: nbval; extra == "develop"
70
65
  Requires-Dist: ruff; extra == "develop"
@@ -73,7 +68,11 @@ Requires-Dist: pandas-stubs; extra == "develop"
73
68
  Requires-Dist: pre-commit; extra == "develop"
74
69
  Requires-Dist: plotly>=5.0.0; extra == "develop"
75
70
  Requires-Dist: notebook>=7.5.0; extra == "develop"
76
- Requires-Dist: matplotlib; extra == "develop"
71
+ Provides-Extra: plot
72
+ Requires-Dist: plotly>=5.0.0; extra == "plot"
73
+ Provides-Extra: notebooks
74
+ Requires-Dist: notebook>=7.5.0; extra == "notebooks"
75
+ Requires-Dist: plotly>=5.0.0; extra == "notebooks"
77
76
  Dynamic: license-file
78
77
 
79
78
  [![Version](https://img.shields.io/pypi/v/tsam.svg)](https://pypi.python.org/pypi/tsam) [![Conda Version](https://img.shields.io/conda/vn/conda-forge/tsam.svg)](https://anaconda.org/conda-forge/tsam) [![Documentation Status](https://readthedocs.org/projects/tsam/badge/?version=latest)](https://tsam.readthedocs.io/en/latest/) [![PyPI - License](https://img.shields.io/pypi/l/tsam)]((https://github.com/FZJ-IEK3-VSA/tsam/blob/master/LICENSE.txt)) [![codecov](https://codecov.io/gh/FZJ-IEK3-VSA/tsam/branch/master/graph/badge.svg)](https://codecov.io/gh/FZJ-IEK3-VSA/tsam)
@@ -217,9 +216,9 @@ cluster_representatives = aggregation.createTypicalPeriods()
217
216
  ### Detailed examples
218
217
  Detailed examples can be found at:/docs/source/examples_notebooks/
219
218
 
220
- A [**first example**](/docs/source/examples_notebooks/aggregation_example.ipynb) shows the capabilites of tsam as jupyter notebook.
219
+ A [**quickstart example**](/docs/source/examples_notebooks/quickstart.ipynb) shows the capabilities of tsam as a Jupyter notebook.
221
220
 
222
- A [**second example**](/docs/source/examples_notebooks/aggregation_optiinput.ipynb) shows in more detail how to access the relevant aggregation results required for paramtrizing e.g. an optimization.
221
+ A [**second example**](/docs/source/examples_notebooks/optimization_input.ipynb) shows in more detail how to access the relevant aggregation results required for parameterizing e.g. an optimization.
223
222
 
224
223
  The example time series are based on a department [publication](https://www.mdpi.com/1996-1073/10/3/361) and the [test reference years of the DWD](https://www.dwd.de/DE/leistungen/testreferenzjahre/testreferenzjahre.html).
225
224
 
@@ -3,7 +3,7 @@ pandas<=3.0.0,>=2.2.0
3
3
  numpy<=2.4.1,>=1.22.4
4
4
  pyomo<=6.95,>=6.4.8
5
5
  networkx<=3.6.1,>=2.5
6
- tqdm<=4.67.1,>=4.21.0
6
+ tqdm<=4.67.2,>=4.21.0
7
7
  highspy<=1.12.0,>=1.7.2
8
8
 
9
9
  [develop]
@@ -14,6 +14,7 @@ codecov
14
14
  sphinx
15
15
  sphinx-autobuild
16
16
  sphinx_book_theme
17
+ nbsphinx
17
18
  twine
18
19
  nbval
19
20
  ruff
@@ -22,12 +23,10 @@ pandas-stubs
22
23
  pre-commit
23
24
  plotly>=5.0.0
24
25
  notebook>=7.5.0
25
- matplotlib
26
26
 
27
27
  [notebooks]
28
28
  notebook>=7.5.0
29
29
  plotly>=5.0.0
30
- matplotlib
31
30
 
32
31
  [plot]
33
32
  plotly>=5.0.0
@@ -0,0 +1,237 @@
1
+ import warnings
2
+
3
+ import numpy as np
4
+ import pandas as pd
5
+ import pytest
6
+
7
+ import tsam
8
+ import tsam.timeseriesaggregation as tsam_legacy
9
+ from conftest import TESTDATA_CSV
10
+ from tsam.config import ExtremeConfig
11
+
12
+
13
+ def test_extremePeriods():
14
+ hoursPerPeriod = 24
15
+
16
+ noTypicalPeriods = 8
17
+
18
+ raw = pd.read_csv(TESTDATA_CSV, index_col=0)
19
+
20
+ aggregation1 = tsam_legacy.TimeSeriesAggregation(
21
+ raw,
22
+ noTypicalPeriods=noTypicalPeriods,
23
+ hoursPerPeriod=hoursPerPeriod,
24
+ clusterMethod="hierarchical",
25
+ rescaleClusterPeriods=False,
26
+ extremePeriodMethod="new_cluster_center",
27
+ addPeakMax=["GHI"],
28
+ )
29
+
30
+ aggregation2 = tsam_legacy.TimeSeriesAggregation(
31
+ raw,
32
+ noTypicalPeriods=noTypicalPeriods,
33
+ hoursPerPeriod=hoursPerPeriod,
34
+ clusterMethod="hierarchical",
35
+ rescaleClusterPeriods=False,
36
+ extremePeriodMethod="append",
37
+ addPeakMax=["GHI"],
38
+ )
39
+
40
+ aggregation3 = tsam_legacy.TimeSeriesAggregation(
41
+ raw,
42
+ noTypicalPeriods=noTypicalPeriods,
43
+ hoursPerPeriod=hoursPerPeriod,
44
+ clusterMethod="hierarchical",
45
+ rescaleClusterPeriods=False,
46
+ extremePeriodMethod="replace_cluster_center",
47
+ addPeakMax=["GHI"],
48
+ )
49
+
50
+ # make sure that the RMSE for new cluster centers (reassigning points to the exxtreme point if the distance to it is
51
+ # smaller)is bigger than for appending just one extreme period
52
+ np.testing.assert_array_less(
53
+ aggregation1.accuracyIndicators().loc["GHI", "RMSE"],
54
+ aggregation2.accuracyIndicators().loc["GHI", "RMSE"],
55
+ )
56
+
57
+ # make sure that the RMSE for appending the extreme period is smaller than for replacing the cluster center by the
58
+ # extreme period (conservative assumption)
59
+ np.testing.assert_array_less(
60
+ aggregation2.accuracyIndicators().loc["GHI", "RMSE"],
61
+ aggregation3.accuracyIndicators().loc["GHI", "RMSE"],
62
+ )
63
+
64
+ # check if addMeanMax and addMeanMin are working
65
+ aggregation4 = tsam_legacy.TimeSeriesAggregation(
66
+ raw,
67
+ noTypicalPeriods=noTypicalPeriods,
68
+ hoursPerPeriod=hoursPerPeriod,
69
+ clusterMethod="hierarchical",
70
+ rescaleClusterPeriods=False,
71
+ extremePeriodMethod="append",
72
+ addMeanMax=["GHI"],
73
+ addMeanMin=["GHI"],
74
+ )
75
+
76
+ origData = aggregation4.predictOriginalData()
77
+
78
+ np.testing.assert_array_almost_equal(
79
+ raw.groupby(np.arange(len(raw)) // 24).mean().max().loc["GHI"],
80
+ origData.groupby(np.arange(len(origData)) // 24).mean().max().loc["GHI"],
81
+ decimal=6,
82
+ )
83
+
84
+ np.testing.assert_array_almost_equal(
85
+ raw.groupby(np.arange(len(raw)) // 24).mean().min().loc["GHI"],
86
+ origData.groupby(np.arange(len(origData)) // 24).mean().min().loc["GHI"],
87
+ decimal=6,
88
+ )
89
+
90
+
91
+ def test_preserve_n_clusters_exact_clusters_append():
92
+ """Final n_clusters equals requested when preserve_n_clusters=True with append method."""
93
+ raw = pd.read_csv(TESTDATA_CSV, index_col=0)
94
+
95
+ n_clusters = 10
96
+ result = tsam.aggregate(
97
+ raw,
98
+ n_clusters=n_clusters,
99
+ extremes=ExtremeConfig(
100
+ method="append",
101
+ max_value=["GHI"],
102
+ min_value=["T"],
103
+ preserve_n_clusters=True,
104
+ ),
105
+ )
106
+
107
+ # With preserve_n_clusters=True, final cluster count should equal n_clusters
108
+ assert result.n_clusters == n_clusters
109
+
110
+
111
+ def test_preserve_n_clusters_exact_clusters_new_cluster():
112
+ """Final n_clusters equals requested when preserve_n_clusters=True with new_cluster method."""
113
+ raw = pd.read_csv(TESTDATA_CSV, index_col=0)
114
+
115
+ n_clusters = 10
116
+ result = tsam.aggregate(
117
+ raw,
118
+ n_clusters=n_clusters,
119
+ extremes=ExtremeConfig(
120
+ method="new_cluster",
121
+ max_value=["GHI"],
122
+ preserve_n_clusters=True,
123
+ ),
124
+ )
125
+
126
+ # With preserve_n_clusters=True, final cluster count should equal n_clusters
127
+ assert result.n_clusters == n_clusters
128
+
129
+
130
+ def test_preserve_n_clusters_false_adds_extra_clusters():
131
+ """Default behavior: extremes are added on top of n_clusters."""
132
+ raw = pd.read_csv(TESTDATA_CSV, index_col=0)
133
+
134
+ n_clusters = 10
135
+ result = tsam.aggregate(
136
+ raw,
137
+ n_clusters=n_clusters,
138
+ extremes=ExtremeConfig(
139
+ method="append",
140
+ max_value=["GHI"],
141
+ min_value=["T"],
142
+ preserve_n_clusters=False, # Default
143
+ ),
144
+ )
145
+
146
+ # With preserve_n_clusters=False (default), extremes are added on top
147
+ # So final count should be > n_clusters (n_clusters + n_extremes)
148
+ assert result.n_clusters > n_clusters
149
+
150
+
151
+ def test_preserve_n_clusters_validation_error():
152
+ """Error if n_clusters <= n_extremes when preserve_n_clusters=True."""
153
+ raw = pd.read_csv(TESTDATA_CSV, index_col=0)
154
+
155
+ with pytest.raises(ValueError, match="must be greater than"):
156
+ tsam.aggregate(
157
+ raw,
158
+ n_clusters=2,
159
+ extremes=ExtremeConfig(
160
+ max_value=["GHI", "T", "Wind"], # 3 extremes
161
+ preserve_n_clusters=True,
162
+ ),
163
+ )
164
+
165
+
166
+ def test_preserve_n_clusters_preserves_extremes():
167
+ """Extreme values are still preserved with preserve_n_clusters=True."""
168
+ raw = pd.read_csv(TESTDATA_CSV, index_col=0)
169
+
170
+ result = tsam.aggregate(
171
+ raw,
172
+ n_clusters=10,
173
+ extremes=ExtremeConfig(
174
+ method="append",
175
+ max_value=["GHI"],
176
+ preserve_n_clusters=True,
177
+ ),
178
+ preserve_column_means=False, # Don't rescale to check raw extreme preservation
179
+ )
180
+
181
+ # The maximum GHI value should be preserved in the typical periods
182
+ orig_max = raw["GHI"].max()
183
+ typical_max = result.cluster_representatives["GHI"].max()
184
+
185
+ np.testing.assert_almost_equal(orig_max, typical_max, decimal=5)
186
+
187
+
188
+ def test_preserve_n_clusters_serialization():
189
+ """ExtremeConfig with preserve_n_clusters serializes correctly."""
190
+ config = ExtremeConfig(
191
+ method="append",
192
+ max_value=["Load"],
193
+ preserve_n_clusters=True,
194
+ )
195
+
196
+ d = config.to_dict()
197
+ assert d["preserve_n_clusters"] is True
198
+
199
+ config2 = ExtremeConfig.from_dict(d)
200
+ assert config2.preserve_n_clusters is True
201
+
202
+
203
+ def test_preserve_n_clusters_default_none_with_future_warning():
204
+ """Default value of preserve_n_clusters is None with FutureWarning."""
205
+ # Creating ExtremeConfig with extremes but without explicit preserve_n_clusters
206
+ # should emit a FutureWarning
207
+ with pytest.warns(FutureWarning, match="preserve_n_clusters currently defaults"):
208
+ config = ExtremeConfig(max_value=["Load"])
209
+
210
+ # The raw value should be None
211
+ assert config.preserve_n_clusters is None
212
+
213
+ # But effective value should be False (current default behavior)
214
+ assert config._effective_preserve_n_clusters is False
215
+
216
+ # to_dict should not include it when None
217
+ d = config.to_dict()
218
+ assert "preserve_n_clusters" not in d
219
+
220
+
221
+ def test_preserve_n_clusters_explicit_false_no_warning():
222
+ """Setting preserve_n_clusters=False explicitly should not warn."""
223
+ # No warning when explicitly set
224
+ with warnings.catch_warnings():
225
+ warnings.simplefilter("error", FutureWarning)
226
+ config = ExtremeConfig(max_value=["Load"], preserve_n_clusters=False)
227
+
228
+ assert config.preserve_n_clusters is False
229
+ assert config._effective_preserve_n_clusters is False
230
+
231
+ # to_dict should include it when explicitly False
232
+ d = config.to_dict()
233
+ assert d["preserve_n_clusters"] is False
234
+
235
+
236
+ if __name__ == "__main__":
237
+ test_extremePeriods()
@@ -1,87 +0,0 @@
1
- import numpy as np
2
- import pandas as pd
3
-
4
- import tsam.timeseriesaggregation as tsam
5
- from conftest import TESTDATA_CSV
6
-
7
-
8
- def test_extremePeriods():
9
- hoursPerPeriod = 24
10
-
11
- noTypicalPeriods = 8
12
-
13
- raw = pd.read_csv(TESTDATA_CSV, index_col=0)
14
-
15
- aggregation1 = tsam.TimeSeriesAggregation(
16
- raw,
17
- noTypicalPeriods=noTypicalPeriods,
18
- hoursPerPeriod=hoursPerPeriod,
19
- clusterMethod="hierarchical",
20
- rescaleClusterPeriods=False,
21
- extremePeriodMethod="new_cluster_center",
22
- addPeakMax=["GHI"],
23
- )
24
-
25
- aggregation2 = tsam.TimeSeriesAggregation(
26
- raw,
27
- noTypicalPeriods=noTypicalPeriods,
28
- hoursPerPeriod=hoursPerPeriod,
29
- clusterMethod="hierarchical",
30
- rescaleClusterPeriods=False,
31
- extremePeriodMethod="append",
32
- addPeakMax=["GHI"],
33
- )
34
-
35
- aggregation3 = tsam.TimeSeriesAggregation(
36
- raw,
37
- noTypicalPeriods=noTypicalPeriods,
38
- hoursPerPeriod=hoursPerPeriod,
39
- clusterMethod="hierarchical",
40
- rescaleClusterPeriods=False,
41
- extremePeriodMethod="replace_cluster_center",
42
- addPeakMax=["GHI"],
43
- )
44
-
45
- # make sure that the RMSE for new cluster centers (reassigning points to the exxtreme point if the distance to it is
46
- # smaller)is bigger than for appending just one extreme period
47
- np.testing.assert_array_less(
48
- aggregation1.accuracyIndicators().loc["GHI", "RMSE"],
49
- aggregation2.accuracyIndicators().loc["GHI", "RMSE"],
50
- )
51
-
52
- # make sure that the RMSE for appending the extreme period is smaller than for replacing the cluster center by the
53
- # extreme period (conservative assumption)
54
- np.testing.assert_array_less(
55
- aggregation2.accuracyIndicators().loc["GHI", "RMSE"],
56
- aggregation3.accuracyIndicators().loc["GHI", "RMSE"],
57
- )
58
-
59
- # check if addMeanMax and addMeanMin are working
60
- aggregation4 = tsam.TimeSeriesAggregation(
61
- raw,
62
- noTypicalPeriods=noTypicalPeriods,
63
- hoursPerPeriod=hoursPerPeriod,
64
- clusterMethod="hierarchical",
65
- rescaleClusterPeriods=False,
66
- extremePeriodMethod="append",
67
- addMeanMax=["GHI"],
68
- addMeanMin=["GHI"],
69
- )
70
-
71
- origData = aggregation4.predictOriginalData()
72
-
73
- np.testing.assert_array_almost_equal(
74
- raw.groupby(np.arange(len(raw)) // 24).mean().max().loc["GHI"],
75
- origData.groupby(np.arange(len(origData)) // 24).mean().max().loc["GHI"],
76
- decimal=6,
77
- )
78
-
79
- np.testing.assert_array_almost_equal(
80
- raw.groupby(np.arange(len(raw)) // 24).mean().min().loc["GHI"],
81
- origData.groupby(np.arange(len(origData)) // 24).mean().min().loc["GHI"],
82
- decimal=6,
83
- )
84
-
85
-
86
- if __name__ == "__main__":
87
- test_extremePeriods()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes