flixopt 2.1.7__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/user-guide/Mathematical Notation/Effects, Penalty & Objective.md +8 -8
- 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 +3 -3
- docs/user-guide/Mathematical Notation/OnOffParameters.md +3 -0
- docs/user-guide/Mathematical Notation/Storage.md +1 -1
- flixopt/aggregation.py +33 -32
- flixopt/calculation.py +158 -58
- flixopt/components.py +673 -150
- flixopt/config.py +17 -8
- flixopt/core.py +59 -54
- flixopt/effects.py +144 -63
- flixopt/elements.py +292 -107
- flixopt/features.py +61 -58
- flixopt/flow_system.py +69 -48
- flixopt/interface.py +952 -113
- flixopt/io.py +15 -10
- flixopt/linear_converters.py +373 -81
- flixopt/network_app.py +75 -39
- flixopt/plotting.py +215 -87
- flixopt/results.py +382 -209
- flixopt/solvers.py +25 -21
- flixopt/structure.py +41 -37
- flixopt/utils.py +10 -7
- {flixopt-2.1.7.dist-info → flixopt-2.1.8.dist-info}/METADATA +46 -42
- {flixopt-2.1.7.dist-info → flixopt-2.1.8.dist-info}/RECORD +30 -28
- scripts/gen_ref_pages.py +1 -1
- {flixopt-2.1.7.dist-info → flixopt-2.1.8.dist-info}/WHEEL +0 -0
- {flixopt-2.1.7.dist-info → flixopt-2.1.8.dist-info}/licenses/LICENSE +0 -0
- {flixopt-2.1.7.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."""
|
|
@@ -101,34 +99,38 @@ class DataConverter:
|
|
|
101
99
|
|
|
102
100
|
|
|
103
101
|
class TimeSeriesData:
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
+
"""
|
|
122
125
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
"""
|
|
126
|
+
# TODO: Move to Interface.py
|
|
127
|
+
def __init__(self, data: NumericData, agg_group: str | None = None, agg_weight: float | None = None):
|
|
126
128
|
self.data = data
|
|
127
129
|
self.agg_group = agg_group
|
|
128
130
|
self.agg_weight = agg_weight
|
|
129
131
|
if (agg_group is not None) and (agg_weight is not None):
|
|
130
132
|
raise ValueError('Either <agg_group> or explicit <agg_weigth> can be used. Not both!')
|
|
131
|
-
self.label:
|
|
133
|
+
self.label: str | None = None
|
|
132
134
|
|
|
133
135
|
def __repr__(self):
|
|
134
136
|
# Get the constructor arguments and their current values
|
|
@@ -143,6 +145,10 @@ class TimeSeriesData:
|
|
|
143
145
|
return str(self.data)
|
|
144
146
|
|
|
145
147
|
|
|
148
|
+
NumericDataTS = NumericData | TimeSeriesData
|
|
149
|
+
"""Represents either standard numeric data or TimeSeriesData."""
|
|
150
|
+
|
|
151
|
+
|
|
146
152
|
class TimeSeries:
|
|
147
153
|
"""
|
|
148
154
|
A class representing time series data with active and stored states.
|
|
@@ -163,8 +169,8 @@ class TimeSeries:
|
|
|
163
169
|
data: NumericData,
|
|
164
170
|
name: str,
|
|
165
171
|
timesteps: pd.DatetimeIndex,
|
|
166
|
-
aggregation_weight:
|
|
167
|
-
aggregation_group:
|
|
172
|
+
aggregation_weight: float | None = None,
|
|
173
|
+
aggregation_group: str | None = None,
|
|
168
174
|
needs_extra_timestep: bool = False,
|
|
169
175
|
) -> 'TimeSeries':
|
|
170
176
|
"""
|
|
@@ -190,7 +196,7 @@ class TimeSeries:
|
|
|
190
196
|
)
|
|
191
197
|
|
|
192
198
|
@classmethod
|
|
193
|
-
def from_json(cls, data:
|
|
199
|
+
def from_json(cls, data: dict[str, Any] | None = None, path: str | None = None) -> 'TimeSeries':
|
|
194
200
|
"""
|
|
195
201
|
Load a TimeSeries from a dictionary or json file.
|
|
196
202
|
|
|
@@ -208,7 +214,7 @@ class TimeSeries:
|
|
|
208
214
|
raise ValueError("Exactly one of 'path' or 'data' must be provided")
|
|
209
215
|
|
|
210
216
|
if path is not None:
|
|
211
|
-
with open(path
|
|
217
|
+
with open(path) as f:
|
|
212
218
|
data = json.load(f)
|
|
213
219
|
|
|
214
220
|
# Convert ISO date strings to datetime objects
|
|
@@ -227,8 +233,8 @@ class TimeSeries:
|
|
|
227
233
|
self,
|
|
228
234
|
data: xr.DataArray,
|
|
229
235
|
name: str,
|
|
230
|
-
aggregation_weight:
|
|
231
|
-
aggregation_group:
|
|
236
|
+
aggregation_weight: float | None = None,
|
|
237
|
+
aggregation_group: str | None = None,
|
|
232
238
|
needs_extra_timestep: bool = False,
|
|
233
239
|
):
|
|
234
240
|
"""
|
|
@@ -274,7 +280,7 @@ class TimeSeries:
|
|
|
274
280
|
self._stored_data = self._backup.copy(deep=True)
|
|
275
281
|
self.reset()
|
|
276
282
|
|
|
277
|
-
def to_json(self, path:
|
|
283
|
+
def to_json(self, path: pathlib.Path | None = None) -> dict[str, Any]:
|
|
278
284
|
"""
|
|
279
285
|
Save the TimeSeries to a dictionary or JSON file.
|
|
280
286
|
|
|
@@ -330,7 +336,7 @@ class TimeSeries:
|
|
|
330
336
|
return self._active_timesteps
|
|
331
337
|
|
|
332
338
|
@active_timesteps.setter
|
|
333
|
-
def active_timesteps(self, timesteps:
|
|
339
|
+
def active_timesteps(self, timesteps: pd.DatetimeIndex | None):
|
|
334
340
|
"""
|
|
335
341
|
Set active_timesteps and refresh active_data.
|
|
336
342
|
|
|
@@ -542,8 +548,8 @@ class TimeSeriesCollection:
|
|
|
542
548
|
def __init__(
|
|
543
549
|
self,
|
|
544
550
|
timesteps: pd.DatetimeIndex,
|
|
545
|
-
hours_of_last_timestep:
|
|
546
|
-
hours_of_previous_timesteps:
|
|
551
|
+
hours_of_last_timestep: float | None = None,
|
|
552
|
+
hours_of_previous_timesteps: float | np.ndarray | None = None,
|
|
547
553
|
):
|
|
548
554
|
"""
|
|
549
555
|
Args:
|
|
@@ -571,22 +577,22 @@ class TimeSeriesCollection:
|
|
|
571
577
|
self._active_hours_per_timestep = None
|
|
572
578
|
|
|
573
579
|
# Dictionary of time series by name
|
|
574
|
-
self.time_series_data:
|
|
580
|
+
self.time_series_data: dict[str, TimeSeries] = {}
|
|
575
581
|
|
|
576
582
|
# Aggregation
|
|
577
|
-
self.group_weights:
|
|
578
|
-
self.weights:
|
|
583
|
+
self.group_weights: dict[str, float] = {}
|
|
584
|
+
self.weights: dict[str, float] = {}
|
|
579
585
|
|
|
580
586
|
@classmethod
|
|
581
587
|
def with_uniform_timesteps(
|
|
582
|
-
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
|
|
583
589
|
) -> 'TimeSeriesCollection':
|
|
584
590
|
"""Create a collection with uniform timesteps."""
|
|
585
591
|
timesteps = pd.date_range(start_time, periods=periods, freq=freq, name='time')
|
|
586
592
|
return cls(timesteps, hours_of_previous_timesteps=hours_per_step)
|
|
587
593
|
|
|
588
594
|
def create_time_series(
|
|
589
|
-
self, data:
|
|
595
|
+
self, data: NumericData | TimeSeriesData, name: str, needs_extra_timestep: bool = False
|
|
590
596
|
) -> TimeSeries:
|
|
591
597
|
"""
|
|
592
598
|
Creates a TimeSeries from the given data and adds it to the collection.
|
|
@@ -595,7 +601,6 @@ class TimeSeriesCollection:
|
|
|
595
601
|
data: The data to create the TimeSeries from.
|
|
596
602
|
name: The name of the TimeSeries.
|
|
597
603
|
needs_extra_timestep: Whether to create an additional timestep at the end of the timesteps.
|
|
598
|
-
The data to create the TimeSeries from.
|
|
599
604
|
|
|
600
605
|
Returns:
|
|
601
606
|
The created TimeSeries.
|
|
@@ -630,7 +635,7 @@ class TimeSeriesCollection:
|
|
|
630
635
|
|
|
631
636
|
return time_series
|
|
632
637
|
|
|
633
|
-
def calculate_aggregation_weights(self) ->
|
|
638
|
+
def calculate_aggregation_weights(self) -> dict[str, float]:
|
|
634
639
|
"""Calculate and return aggregation weights for all time series."""
|
|
635
640
|
self.group_weights = self._calculate_group_weights()
|
|
636
641
|
self.weights = self._calculate_weights()
|
|
@@ -640,7 +645,7 @@ class TimeSeriesCollection:
|
|
|
640
645
|
|
|
641
646
|
return self.weights
|
|
642
647
|
|
|
643
|
-
def activate_timesteps(self, active_timesteps:
|
|
648
|
+
def activate_timesteps(self, active_timesteps: pd.DatetimeIndex | None = None):
|
|
644
649
|
"""
|
|
645
650
|
Update active timesteps for the collection and all time series.
|
|
646
651
|
If no arguments are provided, the active timesteps are reset.
|
|
@@ -816,7 +821,7 @@ class TimeSeriesCollection:
|
|
|
816
821
|
|
|
817
822
|
@staticmethod
|
|
818
823
|
def _create_timesteps_with_extra(
|
|
819
|
-
timesteps: pd.DatetimeIndex, hours_of_last_timestep:
|
|
824
|
+
timesteps: pd.DatetimeIndex, hours_of_last_timestep: float | None
|
|
820
825
|
) -> pd.DatetimeIndex:
|
|
821
826
|
"""Create timesteps with an extra step at the end."""
|
|
822
827
|
if hours_of_last_timestep is not None:
|
|
@@ -831,8 +836,8 @@ class TimeSeriesCollection:
|
|
|
831
836
|
|
|
832
837
|
@staticmethod
|
|
833
838
|
def _calculate_hours_of_previous_timesteps(
|
|
834
|
-
timesteps: pd.DatetimeIndex, hours_of_previous_timesteps:
|
|
835
|
-
) ->
|
|
839
|
+
timesteps: pd.DatetimeIndex, hours_of_previous_timesteps: float | np.ndarray | None
|
|
840
|
+
) -> float | np.ndarray:
|
|
836
841
|
"""Calculate duration of regular timesteps."""
|
|
837
842
|
if hours_of_previous_timesteps is not None:
|
|
838
843
|
return hours_of_previous_timesteps
|
|
@@ -851,7 +856,7 @@ class TimeSeriesCollection:
|
|
|
851
856
|
data=hours_per_step, coords={'time': timesteps_extra[:-1]}, dims=('time',), name='hours_per_step'
|
|
852
857
|
)
|
|
853
858
|
|
|
854
|
-
def _calculate_group_weights(self) ->
|
|
859
|
+
def _calculate_group_weights(self) -> dict[str, float]:
|
|
855
860
|
"""Calculate weights for aggregation groups."""
|
|
856
861
|
# Count series in each group
|
|
857
862
|
groups = [ts.aggregation_group for ts in self.time_series_data.values() if ts.aggregation_group is not None]
|
|
@@ -860,7 +865,7 @@ class TimeSeriesCollection:
|
|
|
860
865
|
# Calculate weight for each group (1/count)
|
|
861
866
|
return {group: 1 / count for group, count in group_counts.items()}
|
|
862
867
|
|
|
863
|
-
def _calculate_weights(self) ->
|
|
868
|
+
def _calculate_weights(self) -> dict[str, float]:
|
|
864
869
|
"""Calculate weights for all time series."""
|
|
865
870
|
# Calculate weight for each time series
|
|
866
871
|
weights = {}
|
|
@@ -902,7 +907,7 @@ class TimeSeriesCollection:
|
|
|
902
907
|
"""Get the number of TimeSeries in the collection."""
|
|
903
908
|
return len(self.time_series_data)
|
|
904
909
|
|
|
905
|
-
def __contains__(self, item:
|
|
910
|
+
def __contains__(self, item: str | TimeSeries) -> bool:
|
|
906
911
|
"""Check if a TimeSeries exists in the collection."""
|
|
907
912
|
if isinstance(item, str):
|
|
908
913
|
return item in self.time_series_data
|
|
@@ -911,12 +916,12 @@ class TimeSeriesCollection:
|
|
|
911
916
|
return False
|
|
912
917
|
|
|
913
918
|
@property
|
|
914
|
-
def non_constants(self) ->
|
|
919
|
+
def non_constants(self) -> list[TimeSeries]:
|
|
915
920
|
"""Get time series with varying values."""
|
|
916
921
|
return [ts for ts in self.time_series_data.values() if not ts.all_equal]
|
|
917
922
|
|
|
918
923
|
@property
|
|
919
|
-
def constants(self) ->
|
|
924
|
+
def constants(self) -> list[TimeSeries]:
|
|
920
925
|
"""Get time series with constant values."""
|
|
921
926
|
return [ts for ts in self.time_series_data.values() if ts.all_equal]
|
|
922
927
|
|