flixopt 2.1.7__py3-none-any.whl → 2.1.9__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.

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, Optional
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
- field_type = field.type
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
- if metadata := getattr(field.type, '__metadata__', None):
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: Optional[str] = None):
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, 'r') as file:
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, 'r') as user_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: Optional[str] = None, use_rich_handler: bool = False) -> logging.Handler:
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: Optional[str] = 'flixopt.log',
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 typing import Any, Dict, Iterator, List, Literal, Optional, Tuple, Union
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 = Union[int, float]
20
+ Scalar = int | float
20
21
  """A type representing a single number, either integer or float."""
21
22
 
22
- NumericData = Union[int, float, np.integer, np.floating, np.ndarray, pd.Series, pd.DataFrame, xr.DataArray]
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
- # TODO: Move to Interface.py
105
- def __init__(self, data: NumericData, agg_group: Optional[str] = None, agg_weight: Optional[float] = None):
106
- """
107
- timeseries class for transmit timeseries AND special characteristics of timeseries,
108
- i.g. to define weights needed in calculation_type 'aggregated'
109
- EXAMPLE solar:
110
- you have several solar timeseries. These should not be overweighted
111
- compared to the remaining timeseries (i.g. heat load, price)!
112
- fixed_relative_profile_solar1 = TimeSeriesData(sol_array_1, type = 'solar')
113
- fixed_relative_profile_solar2 = TimeSeriesData(sol_array_2, type = 'solar')
114
- fixed_relative_profile_solar3 = TimeSeriesData(sol_array_3, type = 'solar')
115
- --> this 3 series of same type share one weight, i.e. internally assigned each weight = 1/3
116
- (instead of standard weight = 1)
117
-
118
- Args:
119
- data: The timeseries data, which can be a scalar, array, or numpy array.
120
- agg_group: The group this TimeSeriesData is a part of. agg_weight is split between members of a group. Default is None.
121
- agg_weight: The weight for calculation_type 'aggregated', should be between 0 and 1. Default is None.
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
- Raises:
124
- Exception: If both agg_group and agg_weight are set, an exception is raised.
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: Optional[str] = None
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: Optional[float] = None,
167
- aggregation_group: Optional[str] = None,
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: Optional[Dict[str, Any]] = None, path: Optional[str] = None) -> 'TimeSeries':
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, 'r') as f:
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: Optional[float] = None,
231
- aggregation_group: Optional[str] = None,
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: Optional[pathlib.Path] = None) -> Dict[str, Any]:
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: Optional[pd.DatetimeIndex]):
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: Optional[float] = None,
546
- hours_of_previous_timesteps: Optional[Union[float, np.ndarray]] = None,
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: Dict[str, TimeSeries] = {}
580
+ self.time_series_data: dict[str, TimeSeries] = {}
575
581
 
576
582
  # Aggregation
577
- self.group_weights: Dict[str, float] = {}
578
- self.weights: Dict[str, float] = {}
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: Optional[float] = None
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: Union[NumericData, TimeSeriesData], name: str, needs_extra_timestep: bool = False
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) -> Dict[str, float]:
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: Optional[pd.DatetimeIndex] = None):
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: Optional[float]
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: Optional[Union[float, np.ndarray]]
835
- ) -> Union[float, np.ndarray]:
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) -> Dict[str, float]:
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) -> Dict[str, float]:
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: Union[str, TimeSeries]) -> bool:
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) -> List[TimeSeries]:
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) -> List[TimeSeries]:
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