flixopt 2.1.0__py3-none-any.whl → 2.2.0b0__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.

@@ -0,0 +1,55 @@
1
+ # Release v2.2.0
2
+
3
+ **Release Date:** YYYY-MM-DD
4
+
5
+ ## What's New
6
+
7
+ ### Scenarios
8
+ Scenarios are a new feature of flixopt. They can be used to model uncertainties in the flow system, such as:
9
+ * Different demand profiles
10
+ * Different price forecasts
11
+ * Different weather conditions
12
+ * Different climate conditions
13
+ The might also be used to model an evolving system with multiple investment periods. Each **scenario** might be a new year, a new month, or a new day, with a different set of investment decisions to take.
14
+
15
+ The weighted sum of the total objective effect of each scenario is used as the objective of the optimization.
16
+
17
+ #### Investments and scenarios
18
+ Scenarios allow for more flexibility in investment decisions.
19
+ You can decide to allow different investment decisions for each scenario, or to allow a single investment decision for a subset of all scnarios, while not allowing for an invest in others.
20
+ This enables the following use cases:
21
+ * Find the best investment decision for each scenario individually
22
+ * Find the best overall investment decision for possible scenarios (robust decision-making)
23
+ * Find the best overall investment decision for a subset of all scenarios
24
+
25
+ The last one might be useful if you want to model a system with multiple investment periods, where one investment decision is made for more than one scenario.
26
+ This might occur when scenarios represent years or months, while an investment decision influences the system for multiple years or months.
27
+
28
+
29
+ ## Other new features
30
+ * Balanced storage - Storage charging and discharging sizes can now be forced to be equal in when optimizing their size.
31
+ * Feature 2 - Description
32
+
33
+ ## Improvements
34
+
35
+ * Improvement 1 - Description
36
+ * Improvement 2 - Description
37
+
38
+ ## Bug Fixes
39
+
40
+ * Fixed issue with X
41
+ * Resolved problem with Y
42
+
43
+ ## Breaking Changes
44
+
45
+ * Change 1 - Migration instructions
46
+ * Change 2 - Migration instructions
47
+
48
+ ## Deprecations
49
+
50
+ * Feature X will be removed in v{next_version}
51
+
52
+ ## Dependencies
53
+
54
+ * Added dependency X v1.2.3
55
+ * Updated dependency Y to v2.0.0
@@ -0,0 +1,115 @@
1
+ # Investments
2
+
3
+ ## Current state
4
+ $$
5
+ \beta_{\text{invest}} \cdot \text{max}(\epsilon, \text V^{\text L}) \leq V \leq \beta_{\text{invest}} \cdot \text V^{\text U}
6
+ $$
7
+ With:
8
+ - $V$ = size
9
+ - $V^{\text L}$ = minimum size
10
+ - $V^{\text U}$ = maximum size
11
+ - $\epsilon$ = epsilon, a small number (such as $1e^{-5}$)
12
+ - $\beta_{invest} \in {0,1}$ = wether the size is invested or not
13
+
14
+ _Please edit the use cases as needed_
15
+ ## Quickfix 1: Optimize the single best size overall
16
+ ### Single variable
17
+ This is already possible and should be, as this is a needed use case
18
+ An additional factor to when the size is actually available might me practical (Which indicates the (fixed) time of investment)
19
+ ## Math
20
+ $$
21
+ V(p) = V * a(p)
22
+ $$
23
+ with:
24
+ - $V$ = size
25
+ - $a(p)$ = factor for availlability per period
26
+
27
+ Factor $a(p)$ is simply multiplied with relative minimum or maximum(t). This is already possible by doing this yourself.
28
+ Effectively, the relative minimum or maximum are altered before using the same constraiints as before.
29
+ THis might lead to some issues regariding minimum_load factor, or others, as the size is not 0 in a scenario where the component cant produce.
30
+ **Therefore this might not be the best choice. See (#Variable per Scenario)
31
+
32
+ ## Variable per Scenario
33
+ - **size** and **invest** as a variable per period $V(s)$ and $\beta_{invest}(s)$
34
+ - with scenario $s \in S$
35
+
36
+ ### Usecase 1: Optimize the size for each Scenario independently
37
+ Restrictions are seperatly for each scenario
38
+ No changes needed. This could be the default behaviour.
39
+
40
+ ### Usecase 2: Optimize ONE size for ALL scenarios
41
+ The size is the same globally, but not a scalar, but a variable per scenario $V(s)$
42
+ #### 2a: The same size in all scenarios
43
+ $$
44
+ V(s) = V(s') \quad \forall s,s' \in S
45
+ $$
46
+
47
+ With:
48
+ - $V(s)$ and $V(s')$ = size
49
+ - $S$ = set of scenarios
50
+
51
+ #### 2b: The same size, but can be 0 prior to the first increment
52
+ - Find the Optimal time of investment.
53
+ - Force an investment in a certain scenario (parameter optional as a list/array ob booleans)
54
+ - Combine optional and minimum/maximum size to force an investment inside a range if scenarios
55
+
56
+ $$
57
+ \beta_{\text{invest}}(s) \leq \beta_{\text{invest}}(s+1) \quad \forall s \in \{1,2,\ldots,S-1\}
58
+ $$
59
+
60
+ $$
61
+ V(s') - V(s) \leq M \cdot (2 - \beta_{\text{invest}}(s) - \beta_{\text{invest}}(s')) \quad \forall s, s' \in S
62
+ $$
63
+ $$
64
+ V(s') - V(s) \geq M \cdot (2 - \beta_{\text{invest}}(s) - \beta_{\text{invest}}(s')) \quad \forall s, s' \in S
65
+ $$
66
+
67
+ This could be the default behaviour. (which would be consistent with other variables)
68
+
69
+
70
+ ### Switch
71
+
72
+ $$
73
+ \begin{aligned}
74
+ & \text{SWITCH}_s \in \{0,1\} \quad \forall s \in \{1,2,\ldots,S\} \\
75
+ & \sum_{s=1}^{S} \text{SWITCH}_s = 1 \\
76
+ & \beta_{\text{invest}}(s) = \sum_{s'=1}^{s} \text{SWITCH}_{s'} \quad \forall s \in \{1,2,\ldots,S\} \\
77
+ \end{aligned}
78
+ $$
79
+
80
+ $$
81
+ \begin{aligned}
82
+ & V(s) \leq V_{\text{actual}} \quad \forall s \in \{1,2,\ldots,S\} \\
83
+ & V(s) \geq V_{\text{actual}} - M \cdot (1 - \beta_{\text{invest}}(s)) \quad \forall s \in \{1,2,\ldots,S\}
84
+ \end{aligned}
85
+ $$
86
+
87
+
88
+
89
+
90
+ ### Usecase 3: Find the best scenario to increment the size (Timing of the investment)
91
+ The size can only increment once (based on a starting point). This allows to optimize the timing of an investment.
92
+ #### Math
93
+ Treat $\beta_{invest}$ like an ON/OFF variable, and introduce a SwitchOn, that can only be active once.
94
+
95
+ *Thoughts:*
96
+ - Treating $\beta_{invest}$ like an ON/OFF variable suggest using the already presentconstraints linked to On/OffModel
97
+ - The timing could be constraint to be first in scenario x, or last in scenario y
98
+ - Restrict the number of consecutive scenarios
99
+ THis might needs the OnOffModel to be more generic (HOURS). Further, the span between scenarios needs to be weighted (like dt_in_hours), or the scenarios need to be measureable (integers)
100
+
101
+
102
+ ### Others
103
+
104
+ #### Usecase 4: Only increase/decrease the size
105
+ Start from a certain size. For each scenario, the size can increase, but never decrease. (Or the other way around).
106
+ This would mean that a size expansion is possible,
107
+
108
+ #### Usecase 5: Restrict the increment in size per scenario
109
+ Restrict how much the size can increase/decrease for in scenario, based on the prior scenario.
110
+
111
+
112
+
113
+
114
+
115
+ Many more are possible
flixopt/calculation.py CHANGED
@@ -12,6 +12,7 @@ import logging
12
12
  import math
13
13
  import pathlib
14
14
  import timeit
15
+ import warnings
15
16
  from typing import Any, Dict, List, Optional, Union
16
17
 
17
18
  import numpy as np
@@ -43,20 +44,32 @@ class Calculation:
43
44
  self,
44
45
  name: str,
45
46
  flow_system: FlowSystem,
46
- active_timesteps: Optional[pd.DatetimeIndex] = None,
47
+ selected_timesteps: Optional[pd.DatetimeIndex] = None,
48
+ selected_scenarios: Optional[pd.Index] = None,
47
49
  folder: Optional[pathlib.Path] = None,
50
+ active_timesteps: Optional[pd.DatetimeIndex] = None,
48
51
  ):
49
52
  """
50
53
  Args:
51
54
  name: name of calculation
52
55
  flow_system: flow_system which should be calculated
53
- active_timesteps: list with indices, which should be used for calculation. If None, then all timesteps are used.
56
+ selected_timesteps: timesteps which should be used for calculation. If None, then all timesteps are used.
57
+ selected_scenarios: scenarios which should be used for calculation. If None, then all scenarios are used.
54
58
  folder: folder where results should be saved. If None, then the current working directory is used.
59
+ active_timesteps: Deprecated. Use selected_timesteps instead.
55
60
  """
61
+ if active_timesteps is not None:
62
+ warnings.warn(
63
+ 'active_timesteps is deprecated. Use selected_timesteps instead.',
64
+ DeprecationWarning,
65
+ stacklevel=2,
66
+ )
67
+ selected_timesteps = active_timesteps
56
68
  self.name = name
57
69
  self.flow_system = flow_system
58
70
  self.model: Optional[SystemModel] = None
59
- self.active_timesteps = active_timesteps
71
+ self.selected_timesteps = selected_timesteps
72
+ self.selected_scenarios = selected_scenarios
60
73
 
61
74
  self.durations = {'modeling': 0.0, 'solving': 0.0, 'saving': 0.0}
62
75
  self.folder = pathlib.Path.cwd() / 'results' if folder is None else pathlib.Path(folder)
@@ -74,47 +87,49 @@ class Calculation:
74
87
  def main_results(self) -> Dict[str, Union[Scalar, Dict]]:
75
88
  from flixopt.features import InvestmentModel
76
89
 
77
- return {
90
+ main_results = {
78
91
  'Objective': self.model.objective.value,
79
- 'Penalty': float(self.model.effects.penalty.total.solution.values),
92
+ 'Penalty': self.model.effects.penalty.total.solution.values,
80
93
  'Effects': {
81
94
  f'{effect.label} [{effect.unit}]': {
82
- 'operation': float(effect.model.operation.total.solution.values),
83
- 'invest': float(effect.model.invest.total.solution.values),
84
- 'total': float(effect.model.total.solution.values),
95
+ 'operation': effect.model.operation.total.solution.values,
96
+ 'invest': effect.model.invest.total.solution.values,
97
+ 'total': effect.model.total.solution.values,
85
98
  }
86
99
  for effect in self.flow_system.effects
87
100
  },
88
101
  'Invest-Decisions': {
89
102
  'Invested': {
90
- model.label_of_element: float(model.size.solution)
103
+ model.label_of_element: model.size.solution
91
104
  for component in self.flow_system.components.values()
92
105
  for model in component.model.all_sub_models
93
- if isinstance(model, InvestmentModel) and float(model.size.solution) >= CONFIG.modeling.EPSILON
106
+ if isinstance(model, InvestmentModel) and model.size.solution.max() >= CONFIG.modeling.EPSILON
94
107
  },
95
108
  'Not invested': {
96
- model.label_of_element: float(model.size.solution)
109
+ model.label_of_element: model.size.solution
97
110
  for component in self.flow_system.components.values()
98
111
  for model in component.model.all_sub_models
99
- if isinstance(model, InvestmentModel) and float(model.size.solution) < CONFIG.modeling.EPSILON
112
+ if isinstance(model, InvestmentModel) and model.size.solution.max() < CONFIG.modeling.EPSILON
100
113
  },
101
114
  },
102
115
  'Buses with excess': [
103
116
  {
104
117
  bus.label_full: {
105
- 'input': float(np.sum(bus.model.excess_input.solution.values)),
106
- 'output': float(np.sum(bus.model.excess_output.solution.values)),
118
+ 'input': bus.model.excess_input.solution.sum('time'),
119
+ 'output': bus.model.excess_output.solution.sum('time'),
107
120
  }
108
121
  }
109
122
  for bus in self.flow_system.buses.values()
110
123
  if bus.with_excess
111
124
  and (
112
- float(np.sum(bus.model.excess_input.solution.values)) > 1e-3
113
- or float(np.sum(bus.model.excess_output.solution.values)) > 1e-3
125
+ bus.model.excess_input.solution.sum() > 1e-3
126
+ or bus.model.excess_output.solution.sum() > 1e-3
114
127
  )
115
128
  ],
116
129
  }
117
130
 
131
+ return utils.round_floats(main_results)
132
+
118
133
  @property
119
134
  def summary(self):
120
135
  return {
@@ -128,6 +143,15 @@ class Calculation:
128
143
  'Config': CONFIG.to_dict(),
129
144
  }
130
145
 
146
+ @property
147
+ def active_timesteps(self) -> pd.DatetimeIndex:
148
+ warnings.warn(
149
+ 'active_timesteps is deprecated. Use selected_timesteps instead.',
150
+ DeprecationWarning,
151
+ stacklevel=2,
152
+ )
153
+ return self.selected_timesteps
154
+
131
155
 
132
156
  class FullCalculation(Calculation):
133
157
  """
@@ -183,8 +207,8 @@ class FullCalculation(Calculation):
183
207
 
184
208
  def _activate_time_series(self):
185
209
  self.flow_system.transform_data()
186
- self.flow_system.time_series_collection.activate_timesteps(
187
- active_timesteps=self.active_timesteps,
210
+ self.flow_system.time_series_collection.set_selection(
211
+ timesteps=self.selected_timesteps, scenarios=self.selected_scenarios
188
212
  )
189
213
 
190
214
 
@@ -199,7 +223,7 @@ class AggregatedCalculation(FullCalculation):
199
223
  flow_system: FlowSystem,
200
224
  aggregation_parameters: AggregationParameters,
201
225
  components_to_clusterize: Optional[List[Component]] = None,
202
- active_timesteps: Optional[pd.DatetimeIndex] = None,
226
+ selected_timesteps: Optional[pd.DatetimeIndex] = None,
203
227
  folder: Optional[pathlib.Path] = None,
204
228
  ):
205
229
  """
@@ -213,11 +237,13 @@ class AggregatedCalculation(FullCalculation):
213
237
  components_to_clusterize: List of Components to perform aggregation on. If None, then all components are aggregated.
214
238
  This means, teh variables in the components are equalized to each other, according to the typical periods
215
239
  computed in the DataAggregation
216
- active_timesteps: pd.DatetimeIndex or None
240
+ selected_timesteps: pd.DatetimeIndex or None
217
241
  list with indices, which should be used for calculation. If None, then all timesteps are used.
218
242
  folder: folder where results should be saved. If None, then the current working directory is used.
219
243
  """
220
- super().__init__(name, flow_system, active_timesteps, folder=folder)
244
+ if flow_system.time_series_collection.scenarios is not None:
245
+ raise ValueError('Aggregation is not supported for scenarios yet. Please use FullCalculation instead.')
246
+ super().__init__(name, flow_system, selected_timesteps, folder=folder)
221
247
  self.aggregation_parameters = aggregation_parameters
222
248
  self.components_to_clusterize = components_to_clusterize
223
249
  self.aggregation = None
@@ -272,9 +298,9 @@ class AggregatedCalculation(FullCalculation):
272
298
 
273
299
  # Aggregation - creation of aggregated timeseries:
274
300
  self.aggregation = Aggregation(
275
- original_data=self.flow_system.time_series_collection.to_dataframe(
276
- include_extra_timestep=False
277
- ), # Exclude last row (NaN)
301
+ original_data=self.flow_system.time_series_collection.as_dataset(
302
+ with_extra_timestep=False, with_constants=False
303
+ ).to_dataframe(),
278
304
  hours_per_time_step=float(dt_min),
279
305
  hours_per_period=self.aggregation_parameters.hours_per_period,
280
306
  nr_of_periods=self.aggregation_parameters.nr_of_periods,
@@ -286,9 +312,11 @@ class AggregatedCalculation(FullCalculation):
286
312
  self.aggregation.cluster()
287
313
  self.aggregation.plot(show=True, save=self.folder / 'aggregation.html')
288
314
  if self.aggregation_parameters.aggregate_data_and_fix_non_binary_vars:
289
- self.flow_system.time_series_collection.insert_new_data(
290
- self.aggregation.aggregated_data, include_extra_timestep=False
291
- )
315
+ for col in self.aggregation.aggregated_data.columns:
316
+ data = self.aggregation.aggregated_data[col].values
317
+ if col in self.flow_system.time_series_collection._has_extra_timestep:
318
+ data = np.append(data, data[-1])
319
+ self.flow_system.time_series_collection.update_time_series(col, data)
292
320
  self.durations['aggregation'] = round(timeit.default_timer() - t_start_agg, 2)
293
321
 
294
322
 
@@ -327,13 +355,13 @@ class SegmentedCalculation(Calculation):
327
355
  self.nr_of_previous_values = nr_of_previous_values
328
356
  self.sub_calculations: List[FullCalculation] = []
329
357
 
330
- self.all_timesteps = self.flow_system.time_series_collection.all_timesteps
331
- self.all_timesteps_extra = self.flow_system.time_series_collection.all_timesteps_extra
358
+ self.all_timesteps = self.flow_system.time_series_collection._full_timesteps
359
+ self.all_timesteps_extra = self.flow_system.time_series_collection._full_timesteps_extra
332
360
 
333
361
  self.segment_names = [
334
362
  f'Segment_{i + 1}' for i in range(math.ceil(len(self.all_timesteps) / self.timesteps_per_segment))
335
363
  ]
336
- self.active_timesteps_per_segment = self._calculate_timesteps_of_segment()
364
+ self.selected_timesteps_per_segment = self._calculate_timesteps_of_segment()
337
365
 
338
366
  assert timesteps_per_segment > 2, 'The Segment length must be greater 2, due to unwanted internal side effects'
339
367
  assert self.timesteps_per_segment_with_overlap <= len(self.all_timesteps), (
@@ -359,7 +387,7 @@ class SegmentedCalculation(Calculation):
359
387
  logger.info(f'{" Segmented Solving ":#^80}')
360
388
 
361
389
  for i, (segment_name, timesteps_of_segment) in enumerate(
362
- zip(self.segment_names, self.active_timesteps_per_segment, strict=False)
390
+ zip(self.segment_names, self.selected_timesteps_per_segment, strict=False)
363
391
  ):
364
392
  if self.sub_calculations:
365
393
  self._transfer_start_values(i)
@@ -370,7 +398,7 @@ class SegmentedCalculation(Calculation):
370
398
  )
371
399
 
372
400
  calculation = FullCalculation(
373
- f'{self.name}-{segment_name}', self.flow_system, active_timesteps=timesteps_of_segment
401
+ f'{self.name}-{segment_name}', self.flow_system, selected_timesteps=timesteps_of_segment
374
402
  )
375
403
  self.sub_calculations.append(calculation)
376
404
  calculation.do_modeling()
@@ -404,9 +432,9 @@ class SegmentedCalculation(Calculation):
404
432
  This function gets the last values of the previous solved segment and
405
433
  inserts them as start values for the next segment
406
434
  """
407
- timesteps_of_prior_segment = self.active_timesteps_per_segment[segment_index - 1]
435
+ timesteps_of_prior_segment = self.selected_timesteps_per_segment[segment_index - 1]
408
436
 
409
- start = self.active_timesteps_per_segment[segment_index][0]
437
+ start = self.selected_timesteps_per_segment[segment_index][0]
410
438
  start_previous_values = timesteps_of_prior_segment[self.timesteps_per_segment - self.nr_of_previous_values]
411
439
  end_previous_values = timesteps_of_prior_segment[self.timesteps_per_segment - 1]
412
440
 
@@ -435,12 +463,12 @@ class SegmentedCalculation(Calculation):
435
463
  comp.initial_charge_state = self._original_start_values[comp.label_full]
436
464
 
437
465
  def _calculate_timesteps_of_segment(self) -> List[pd.DatetimeIndex]:
438
- active_timesteps_per_segment = []
466
+ selected_timesteps_per_segment = []
439
467
  for i, _ in enumerate(self.segment_names):
440
468
  start = self.timesteps_per_segment * i
441
469
  end = min(start + self.timesteps_per_segment_with_overlap, len(self.all_timesteps))
442
- active_timesteps_per_segment.append(self.all_timesteps[start:end])
443
- return active_timesteps_per_segment
470
+ selected_timesteps_per_segment.append(self.all_timesteps[start:end])
471
+ return selected_timesteps_per_segment
444
472
 
445
473
  @property
446
474
  def timesteps_per_segment_with_overlap(self):