flixopt 2.1.6__py3-none-any.whl → 2.1.8__py3-none-any.whl
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.
Potentially problematic release.
This version of flixopt might be problematic. Click here for more details.
- docs/examples/00-Minimal Example.md +1 -1
- docs/examples/01-Basic Example.md +1 -1
- docs/examples/02-Complex Example.md +1 -1
- docs/examples/index.md +1 -1
- docs/faq/contribute.md +26 -14
- docs/faq/index.md +1 -1
- docs/javascripts/mathjax.js +1 -1
- docs/user-guide/Mathematical Notation/Bus.md +1 -1
- docs/user-guide/Mathematical Notation/Effects, Penalty & Objective.md +21 -21
- docs/user-guide/Mathematical Notation/Flow.md +3 -3
- docs/user-guide/Mathematical Notation/InvestParameters.md +3 -0
- docs/user-guide/Mathematical Notation/LinearConverter.md +5 -5
- docs/user-guide/Mathematical Notation/OnOffParameters.md +3 -0
- docs/user-guide/Mathematical Notation/Piecewise.md +1 -1
- docs/user-guide/Mathematical Notation/Storage.md +2 -2
- docs/user-guide/Mathematical Notation/index.md +1 -1
- docs/user-guide/Mathematical Notation/others.md +1 -1
- docs/user-guide/index.md +2 -2
- flixopt/__init__.py +4 -0
- flixopt/aggregation.py +33 -32
- flixopt/calculation.py +161 -65
- flixopt/components.py +687 -154
- flixopt/config.py +17 -8
- flixopt/core.py +69 -60
- flixopt/effects.py +146 -64
- flixopt/elements.py +297 -110
- flixopt/features.py +78 -71
- flixopt/flow_system.py +72 -50
- flixopt/interface.py +952 -113
- flixopt/io.py +15 -10
- flixopt/linear_converters.py +373 -81
- flixopt/network_app.py +445 -266
- flixopt/plotting.py +215 -87
- flixopt/results.py +382 -209
- flixopt/solvers.py +25 -21
- flixopt/structure.py +41 -39
- flixopt/utils.py +10 -7
- {flixopt-2.1.6.dist-info → flixopt-2.1.8.dist-info}/METADATA +64 -53
- flixopt-2.1.8.dist-info/RECORD +56 -0
- scripts/extract_release_notes.py +5 -5
- scripts/gen_ref_pages.py +1 -1
- flixopt-2.1.6.dist-info/RECORD +0 -54
- {flixopt-2.1.6.dist-info → flixopt-2.1.8.dist-info}/WHEEL +0 -0
- {flixopt-2.1.6.dist-info → flixopt-2.1.8.dist-info}/licenses/LICENSE +0 -0
- {flixopt-2.1.6.dist-info → flixopt-2.1.8.dist-info}/top_level.txt +0 -0
flixopt/config.py
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import logging
|
|
2
4
|
import os
|
|
3
5
|
import types
|
|
4
6
|
from dataclasses import dataclass, fields, is_dataclass
|
|
5
|
-
from typing import Annotated, Literal,
|
|
7
|
+
from typing import Annotated, Literal, get_type_hints
|
|
6
8
|
|
|
7
9
|
import yaml
|
|
8
10
|
from rich.console import Console
|
|
@@ -37,11 +39,15 @@ def dataclass_from_dict_with_validation(cls, data: dict):
|
|
|
37
39
|
if not is_dataclass(cls):
|
|
38
40
|
raise TypeError(f'{cls} must be a dataclass')
|
|
39
41
|
|
|
42
|
+
# Get resolved type hints to handle postponed evaluation
|
|
43
|
+
type_hints = get_type_hints(cls)
|
|
44
|
+
|
|
40
45
|
# Build kwargs for the dataclass constructor
|
|
41
46
|
kwargs = {}
|
|
42
47
|
for field in fields(cls):
|
|
43
48
|
field_name = field.name
|
|
44
|
-
|
|
49
|
+
# Use resolved type from get_type_hints instead of field.type
|
|
50
|
+
field_type = type_hints.get(field_name, field.type)
|
|
45
51
|
field_value = data.get(field_name)
|
|
46
52
|
|
|
47
53
|
# If the field type is a dataclass and the value is a dict, recursively initialize
|
|
@@ -57,7 +63,10 @@ def dataclass_from_dict_with_validation(cls, data: dict):
|
|
|
57
63
|
class ValidatedConfig:
|
|
58
64
|
def __setattr__(self, name, value):
|
|
59
65
|
if field := self.__dataclass_fields__.get(name):
|
|
60
|
-
|
|
66
|
+
# Get resolved type hints to handle postponed evaluation
|
|
67
|
+
type_hints = get_type_hints(self.__class__, include_extras=True)
|
|
68
|
+
field_type = type_hints.get(name, field.type)
|
|
69
|
+
if metadata := getattr(field_type, '__metadata__', None):
|
|
61
70
|
assert metadata[0](value), f'Invalid value passed to {name!r}: {value=}'
|
|
62
71
|
super().__setattr__(name, value)
|
|
63
72
|
|
|
@@ -96,7 +105,7 @@ class CONFIG:
|
|
|
96
105
|
logging: LoggingConfig = None
|
|
97
106
|
|
|
98
107
|
@classmethod
|
|
99
|
-
def load_config(cls, user_config_file:
|
|
108
|
+
def load_config(cls, user_config_file: str | None = None):
|
|
100
109
|
"""
|
|
101
110
|
Initialize configuration using defaults or user-specified file.
|
|
102
111
|
"""
|
|
@@ -104,12 +113,12 @@ class CONFIG:
|
|
|
104
113
|
default_config_path = os.path.join(os.path.dirname(__file__), 'config.yaml')
|
|
105
114
|
|
|
106
115
|
if user_config_file is None:
|
|
107
|
-
with open(default_config_path
|
|
116
|
+
with open(default_config_path) as file:
|
|
108
117
|
new_config = yaml.safe_load(file)
|
|
109
118
|
elif not os.path.exists(user_config_file):
|
|
110
119
|
raise FileNotFoundError(f'Config file not found: {user_config_file}')
|
|
111
120
|
else:
|
|
112
|
-
with open(user_config_file
|
|
121
|
+
with open(user_config_file) as user_file:
|
|
113
122
|
new_config = yaml.safe_load(user_file)
|
|
114
123
|
|
|
115
124
|
# Convert the merged config to ConfigSchema
|
|
@@ -186,7 +195,7 @@ class ColoredMultilineFormater(MultilineFormater):
|
|
|
186
195
|
return '\n'.join(formatted_lines)
|
|
187
196
|
|
|
188
197
|
|
|
189
|
-
def _get_logging_handler(log_file:
|
|
198
|
+
def _get_logging_handler(log_file: str | None = None, use_rich_handler: bool = False) -> logging.Handler:
|
|
190
199
|
"""Returns a logging handler for the given log file."""
|
|
191
200
|
if use_rich_handler and log_file is None:
|
|
192
201
|
# RichHandler for console output
|
|
@@ -225,7 +234,7 @@ def _get_logging_handler(log_file: Optional[str] = None, use_rich_handler: bool
|
|
|
225
234
|
|
|
226
235
|
def setup_logging(
|
|
227
236
|
default_level: Literal['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'] = 'INFO',
|
|
228
|
-
log_file:
|
|
237
|
+
log_file: str | None = 'flixopt.log',
|
|
229
238
|
use_rich_handler: bool = False,
|
|
230
239
|
):
|
|
231
240
|
"""Setup logging configuration"""
|
flixopt/core.py
CHANGED
|
@@ -8,7 +8,8 @@ import json
|
|
|
8
8
|
import logging
|
|
9
9
|
import pathlib
|
|
10
10
|
from collections import Counter
|
|
11
|
-
from
|
|
11
|
+
from collections.abc import Iterator
|
|
12
|
+
from typing import Any, Literal, Optional
|
|
12
13
|
|
|
13
14
|
import numpy as np
|
|
14
15
|
import pandas as pd
|
|
@@ -16,15 +17,12 @@ import xarray as xr
|
|
|
16
17
|
|
|
17
18
|
logger = logging.getLogger('flixopt')
|
|
18
19
|
|
|
19
|
-
Scalar =
|
|
20
|
+
Scalar = int | float
|
|
20
21
|
"""A type representing a single number, either integer or float."""
|
|
21
22
|
|
|
22
|
-
NumericData =
|
|
23
|
+
NumericData = int | float | np.integer | np.floating | np.ndarray | pd.Series | pd.DataFrame | xr.DataArray
|
|
23
24
|
"""Represents any form of numeric data, from simple scalars to complex data structures."""
|
|
24
25
|
|
|
25
|
-
NumericDataTS = Union[NumericData, 'TimeSeriesData']
|
|
26
|
-
"""Represents either standard numeric data or TimeSeriesData."""
|
|
27
|
-
|
|
28
26
|
|
|
29
27
|
class PlausibilityError(Exception):
|
|
30
28
|
"""Error for a failing Plausibility check."""
|
|
@@ -62,17 +60,21 @@ class DataConverter:
|
|
|
62
60
|
return xr.DataArray(data, coords=coords, dims=dims)
|
|
63
61
|
elif isinstance(data, pd.DataFrame):
|
|
64
62
|
if not data.index.equals(timesteps):
|
|
65
|
-
raise ConversionError(
|
|
66
|
-
|
|
67
|
-
|
|
63
|
+
raise ConversionError(
|
|
64
|
+
f"DataFrame index doesn't match timesteps index. "
|
|
65
|
+
f'Its missing the following time steps: {timesteps.difference(data.index)}. '
|
|
66
|
+
f'Some parameters might need an extra timestep at the end.'
|
|
67
|
+
)
|
|
68
68
|
if not len(data.columns) == 1:
|
|
69
69
|
raise ConversionError('DataFrame must have exactly one column')
|
|
70
70
|
return xr.DataArray(data.values.flatten(), coords=coords, dims=dims)
|
|
71
71
|
elif isinstance(data, pd.Series):
|
|
72
72
|
if not data.index.equals(timesteps):
|
|
73
|
-
raise ConversionError(
|
|
74
|
-
|
|
75
|
-
|
|
73
|
+
raise ConversionError(
|
|
74
|
+
f"Series index doesn't match timesteps index. "
|
|
75
|
+
f'Its missing the following time steps: {timesteps.difference(data.index)}. '
|
|
76
|
+
f'Some parameters might need an extra timestep at the end.'
|
|
77
|
+
)
|
|
76
78
|
return xr.DataArray(data.values, coords=coords, dims=dims)
|
|
77
79
|
elif isinstance(data, np.ndarray):
|
|
78
80
|
if data.ndim != 1:
|
|
@@ -97,34 +99,38 @@ class DataConverter:
|
|
|
97
99
|
|
|
98
100
|
|
|
99
101
|
class TimeSeriesData:
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
102
|
+
"""
|
|
103
|
+
TimeSeriesData wraps time series data with aggregation metadata for optimization.
|
|
104
|
+
|
|
105
|
+
This class combines time series data with special characteristics needed for aggregated calculations.
|
|
106
|
+
It allows grouping related time series to prevent overweighting in optimization models.
|
|
107
|
+
|
|
108
|
+
Example:
|
|
109
|
+
When you have multiple solar time series, they should share aggregation weight:
|
|
110
|
+
```python
|
|
111
|
+
solar1 = TimeSeriesData(sol_array_1, agg_group='solar')
|
|
112
|
+
solar2 = TimeSeriesData(sol_array_2, agg_group='solar')
|
|
113
|
+
solar3 = TimeSeriesData(sol_array_3, agg_group='solar')
|
|
114
|
+
# These 3 series share one weight (each gets weight = 1/3 instead of 1)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
data: The timeseries data, which can be a scalar, array, or numpy array.
|
|
119
|
+
agg_group: The group this TimeSeriesData belongs to. agg_weight is split between group members. Default is None.
|
|
120
|
+
agg_weight: The weight for calculation_type 'aggregated', should be between 0 and 1. Default is None.
|
|
121
|
+
|
|
122
|
+
Raises:
|
|
123
|
+
ValueError: If both agg_group and agg_weight are set.
|
|
124
|
+
"""
|
|
118
125
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
"""
|
|
126
|
+
# TODO: Move to Interface.py
|
|
127
|
+
def __init__(self, data: NumericData, agg_group: str | None = None, agg_weight: float | None = None):
|
|
122
128
|
self.data = data
|
|
123
129
|
self.agg_group = agg_group
|
|
124
130
|
self.agg_weight = agg_weight
|
|
125
131
|
if (agg_group is not None) and (agg_weight is not None):
|
|
126
132
|
raise ValueError('Either <agg_group> or explicit <agg_weigth> can be used. Not both!')
|
|
127
|
-
self.label:
|
|
133
|
+
self.label: str | None = None
|
|
128
134
|
|
|
129
135
|
def __repr__(self):
|
|
130
136
|
# Get the constructor arguments and their current values
|
|
@@ -139,6 +145,10 @@ class TimeSeriesData:
|
|
|
139
145
|
return str(self.data)
|
|
140
146
|
|
|
141
147
|
|
|
148
|
+
NumericDataTS = NumericData | TimeSeriesData
|
|
149
|
+
"""Represents either standard numeric data or TimeSeriesData."""
|
|
150
|
+
|
|
151
|
+
|
|
142
152
|
class TimeSeries:
|
|
143
153
|
"""
|
|
144
154
|
A class representing time series data with active and stored states.
|
|
@@ -159,8 +169,8 @@ class TimeSeries:
|
|
|
159
169
|
data: NumericData,
|
|
160
170
|
name: str,
|
|
161
171
|
timesteps: pd.DatetimeIndex,
|
|
162
|
-
aggregation_weight:
|
|
163
|
-
aggregation_group:
|
|
172
|
+
aggregation_weight: float | None = None,
|
|
173
|
+
aggregation_group: str | None = None,
|
|
164
174
|
needs_extra_timestep: bool = False,
|
|
165
175
|
) -> 'TimeSeries':
|
|
166
176
|
"""
|
|
@@ -186,7 +196,7 @@ class TimeSeries:
|
|
|
186
196
|
)
|
|
187
197
|
|
|
188
198
|
@classmethod
|
|
189
|
-
def from_json(cls, data:
|
|
199
|
+
def from_json(cls, data: dict[str, Any] | None = None, path: str | None = None) -> 'TimeSeries':
|
|
190
200
|
"""
|
|
191
201
|
Load a TimeSeries from a dictionary or json file.
|
|
192
202
|
|
|
@@ -204,7 +214,7 @@ class TimeSeries:
|
|
|
204
214
|
raise ValueError("Exactly one of 'path' or 'data' must be provided")
|
|
205
215
|
|
|
206
216
|
if path is not None:
|
|
207
|
-
with open(path
|
|
217
|
+
with open(path) as f:
|
|
208
218
|
data = json.load(f)
|
|
209
219
|
|
|
210
220
|
# Convert ISO date strings to datetime objects
|
|
@@ -223,8 +233,8 @@ class TimeSeries:
|
|
|
223
233
|
self,
|
|
224
234
|
data: xr.DataArray,
|
|
225
235
|
name: str,
|
|
226
|
-
aggregation_weight:
|
|
227
|
-
aggregation_group:
|
|
236
|
+
aggregation_weight: float | None = None,
|
|
237
|
+
aggregation_group: str | None = None,
|
|
228
238
|
needs_extra_timestep: bool = False,
|
|
229
239
|
):
|
|
230
240
|
"""
|
|
@@ -270,7 +280,7 @@ class TimeSeries:
|
|
|
270
280
|
self._stored_data = self._backup.copy(deep=True)
|
|
271
281
|
self.reset()
|
|
272
282
|
|
|
273
|
-
def to_json(self, path:
|
|
283
|
+
def to_json(self, path: pathlib.Path | None = None) -> dict[str, Any]:
|
|
274
284
|
"""
|
|
275
285
|
Save the TimeSeries to a dictionary or JSON file.
|
|
276
286
|
|
|
@@ -326,7 +336,7 @@ class TimeSeries:
|
|
|
326
336
|
return self._active_timesteps
|
|
327
337
|
|
|
328
338
|
@active_timesteps.setter
|
|
329
|
-
def active_timesteps(self, timesteps:
|
|
339
|
+
def active_timesteps(self, timesteps: pd.DatetimeIndex | None):
|
|
330
340
|
"""
|
|
331
341
|
Set active_timesteps and refresh active_data.
|
|
332
342
|
|
|
@@ -538,8 +548,8 @@ class TimeSeriesCollection:
|
|
|
538
548
|
def __init__(
|
|
539
549
|
self,
|
|
540
550
|
timesteps: pd.DatetimeIndex,
|
|
541
|
-
hours_of_last_timestep:
|
|
542
|
-
hours_of_previous_timesteps:
|
|
551
|
+
hours_of_last_timestep: float | None = None,
|
|
552
|
+
hours_of_previous_timesteps: float | np.ndarray | None = None,
|
|
543
553
|
):
|
|
544
554
|
"""
|
|
545
555
|
Args:
|
|
@@ -567,22 +577,22 @@ class TimeSeriesCollection:
|
|
|
567
577
|
self._active_hours_per_timestep = None
|
|
568
578
|
|
|
569
579
|
# Dictionary of time series by name
|
|
570
|
-
self.time_series_data:
|
|
580
|
+
self.time_series_data: dict[str, TimeSeries] = {}
|
|
571
581
|
|
|
572
582
|
# Aggregation
|
|
573
|
-
self.group_weights:
|
|
574
|
-
self.weights:
|
|
583
|
+
self.group_weights: dict[str, float] = {}
|
|
584
|
+
self.weights: dict[str, float] = {}
|
|
575
585
|
|
|
576
586
|
@classmethod
|
|
577
587
|
def with_uniform_timesteps(
|
|
578
|
-
cls, start_time: pd.Timestamp, periods: int, freq: str, hours_per_step:
|
|
588
|
+
cls, start_time: pd.Timestamp, periods: int, freq: str, hours_per_step: float | None = None
|
|
579
589
|
) -> 'TimeSeriesCollection':
|
|
580
590
|
"""Create a collection with uniform timesteps."""
|
|
581
591
|
timesteps = pd.date_range(start_time, periods=periods, freq=freq, name='time')
|
|
582
592
|
return cls(timesteps, hours_of_previous_timesteps=hours_per_step)
|
|
583
593
|
|
|
584
594
|
def create_time_series(
|
|
585
|
-
self, data:
|
|
595
|
+
self, data: NumericData | TimeSeriesData, name: str, needs_extra_timestep: bool = False
|
|
586
596
|
) -> TimeSeries:
|
|
587
597
|
"""
|
|
588
598
|
Creates a TimeSeries from the given data and adds it to the collection.
|
|
@@ -591,7 +601,6 @@ class TimeSeriesCollection:
|
|
|
591
601
|
data: The data to create the TimeSeries from.
|
|
592
602
|
name: The name of the TimeSeries.
|
|
593
603
|
needs_extra_timestep: Whether to create an additional timestep at the end of the timesteps.
|
|
594
|
-
The data to create the TimeSeries from.
|
|
595
604
|
|
|
596
605
|
Returns:
|
|
597
606
|
The created TimeSeries.
|
|
@@ -626,7 +635,7 @@ class TimeSeriesCollection:
|
|
|
626
635
|
|
|
627
636
|
return time_series
|
|
628
637
|
|
|
629
|
-
def calculate_aggregation_weights(self) ->
|
|
638
|
+
def calculate_aggregation_weights(self) -> dict[str, float]:
|
|
630
639
|
"""Calculate and return aggregation weights for all time series."""
|
|
631
640
|
self.group_weights = self._calculate_group_weights()
|
|
632
641
|
self.weights = self._calculate_weights()
|
|
@@ -636,7 +645,7 @@ class TimeSeriesCollection:
|
|
|
636
645
|
|
|
637
646
|
return self.weights
|
|
638
647
|
|
|
639
|
-
def activate_timesteps(self, active_timesteps:
|
|
648
|
+
def activate_timesteps(self, active_timesteps: pd.DatetimeIndex | None = None):
|
|
640
649
|
"""
|
|
641
650
|
Update active timesteps for the collection and all time series.
|
|
642
651
|
If no arguments are provided, the active timesteps are reset.
|
|
@@ -812,7 +821,7 @@ class TimeSeriesCollection:
|
|
|
812
821
|
|
|
813
822
|
@staticmethod
|
|
814
823
|
def _create_timesteps_with_extra(
|
|
815
|
-
timesteps: pd.DatetimeIndex, hours_of_last_timestep:
|
|
824
|
+
timesteps: pd.DatetimeIndex, hours_of_last_timestep: float | None
|
|
816
825
|
) -> pd.DatetimeIndex:
|
|
817
826
|
"""Create timesteps with an extra step at the end."""
|
|
818
827
|
if hours_of_last_timestep is not None:
|
|
@@ -827,8 +836,8 @@ class TimeSeriesCollection:
|
|
|
827
836
|
|
|
828
837
|
@staticmethod
|
|
829
838
|
def _calculate_hours_of_previous_timesteps(
|
|
830
|
-
timesteps: pd.DatetimeIndex, hours_of_previous_timesteps:
|
|
831
|
-
) ->
|
|
839
|
+
timesteps: pd.DatetimeIndex, hours_of_previous_timesteps: float | np.ndarray | None
|
|
840
|
+
) -> float | np.ndarray:
|
|
832
841
|
"""Calculate duration of regular timesteps."""
|
|
833
842
|
if hours_of_previous_timesteps is not None:
|
|
834
843
|
return hours_of_previous_timesteps
|
|
@@ -847,7 +856,7 @@ class TimeSeriesCollection:
|
|
|
847
856
|
data=hours_per_step, coords={'time': timesteps_extra[:-1]}, dims=('time',), name='hours_per_step'
|
|
848
857
|
)
|
|
849
858
|
|
|
850
|
-
def _calculate_group_weights(self) ->
|
|
859
|
+
def _calculate_group_weights(self) -> dict[str, float]:
|
|
851
860
|
"""Calculate weights for aggregation groups."""
|
|
852
861
|
# Count series in each group
|
|
853
862
|
groups = [ts.aggregation_group for ts in self.time_series_data.values() if ts.aggregation_group is not None]
|
|
@@ -856,7 +865,7 @@ class TimeSeriesCollection:
|
|
|
856
865
|
# Calculate weight for each group (1/count)
|
|
857
866
|
return {group: 1 / count for group, count in group_counts.items()}
|
|
858
867
|
|
|
859
|
-
def _calculate_weights(self) ->
|
|
868
|
+
def _calculate_weights(self) -> dict[str, float]:
|
|
860
869
|
"""Calculate weights for all time series."""
|
|
861
870
|
# Calculate weight for each time series
|
|
862
871
|
weights = {}
|
|
@@ -898,7 +907,7 @@ class TimeSeriesCollection:
|
|
|
898
907
|
"""Get the number of TimeSeries in the collection."""
|
|
899
908
|
return len(self.time_series_data)
|
|
900
909
|
|
|
901
|
-
def __contains__(self, item:
|
|
910
|
+
def __contains__(self, item: str | TimeSeries) -> bool:
|
|
902
911
|
"""Check if a TimeSeries exists in the collection."""
|
|
903
912
|
if isinstance(item, str):
|
|
904
913
|
return item in self.time_series_data
|
|
@@ -907,12 +916,12 @@ class TimeSeriesCollection:
|
|
|
907
916
|
return False
|
|
908
917
|
|
|
909
918
|
@property
|
|
910
|
-
def non_constants(self) ->
|
|
919
|
+
def non_constants(self) -> list[TimeSeries]:
|
|
911
920
|
"""Get time series with varying values."""
|
|
912
921
|
return [ts for ts in self.time_series_data.values() if not ts.all_equal]
|
|
913
922
|
|
|
914
923
|
@property
|
|
915
|
-
def constants(self) ->
|
|
924
|
+
def constants(self) -> list[TimeSeries]:
|
|
916
925
|
"""Get time series with constant values."""
|
|
917
926
|
return [ts for ts in self.time_series_data.values() if ts.all_equal]
|
|
918
927
|
|