flixopt 2.1.0__tar.gz → 2.2.0b0__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.

Potentially problematic release.


This version of flixopt might be problematic. Click here for more details.

Files changed (106) hide show
  1. {flixopt-2.1.0 → flixopt-2.2.0b0}/.github/workflows/deploy-docs.yaml +5 -3
  2. {flixopt-2.1.0 → flixopt-2.2.0b0}/PKG-INFO +1 -1
  3. flixopt-2.2.0b0/docs/release-notes/v2.2.0.md +55 -0
  4. flixopt-2.2.0b0/docs/user-guide/Mathematical Notation/Investment.md +115 -0
  5. {flixopt-2.1.0 → flixopt-2.2.0b0}/examples/03_Calculation_types/example_calculation_types.py +5 -5
  6. flixopt-2.2.0b0/examples/04_Scenarios/scenario_example.py +125 -0
  7. {flixopt-2.1.0 → flixopt-2.2.0b0}/flixopt/calculation.py +65 -37
  8. {flixopt-2.1.0 → flixopt-2.2.0b0}/flixopt/components.py +119 -74
  9. flixopt-2.2.0b0/flixopt/core.py +1485 -0
  10. {flixopt-2.1.0 → flixopt-2.2.0b0}/flixopt/effects.py +269 -65
  11. {flixopt-2.1.0 → flixopt-2.2.0b0}/flixopt/elements.py +83 -52
  12. {flixopt-2.1.0 → flixopt-2.2.0b0}/flixopt/features.py +134 -85
  13. {flixopt-2.1.0 → flixopt-2.2.0b0}/flixopt/flow_system.py +99 -16
  14. {flixopt-2.1.0 → flixopt-2.2.0b0}/flixopt/interface.py +142 -51
  15. {flixopt-2.1.0 → flixopt-2.2.0b0}/flixopt/io.py +56 -27
  16. {flixopt-2.1.0 → flixopt-2.2.0b0}/flixopt/linear_converters.py +3 -3
  17. {flixopt-2.1.0 → flixopt-2.2.0b0}/flixopt/plotting.py +34 -16
  18. flixopt-2.2.0b0/flixopt/results.py +1596 -0
  19. {flixopt-2.1.0 → flixopt-2.2.0b0}/flixopt/structure.py +64 -10
  20. {flixopt-2.1.0 → flixopt-2.2.0b0}/flixopt/utils.py +6 -9
  21. {flixopt-2.1.0 → flixopt-2.2.0b0}/flixopt.egg-info/PKG-INFO +1 -1
  22. {flixopt-2.1.0 → flixopt-2.2.0b0}/flixopt.egg-info/SOURCES.txt +6 -1
  23. {flixopt-2.1.0 → flixopt-2.2.0b0}/flixopt.egg-info/top_level.txt +0 -1
  24. {flixopt-2.1.0 → flixopt-2.2.0b0}/tests/conftest.py +75 -1
  25. {flixopt-2.1.0 → flixopt-2.2.0b0}/tests/run_all_tests.py +1 -1
  26. flixopt-2.2.0b0/tests/test_cycle_detection.py +226 -0
  27. flixopt-2.2.0b0/tests/test_dataconverter.py +756 -0
  28. {flixopt-2.1.0 → flixopt-2.2.0b0}/tests/test_effect.py +84 -2
  29. flixopt-2.2.0b0/tests/test_effects_shares_summation.py +236 -0
  30. {flixopt-2.1.0 → flixopt-2.2.0b0}/tests/test_flow.py +137 -2
  31. {flixopt-2.1.0 → flixopt-2.2.0b0}/tests/test_io.py +4 -1
  32. {flixopt-2.1.0 → flixopt-2.2.0b0}/tests/test_plots.py +4 -4
  33. {flixopt-2.1.0 → flixopt-2.2.0b0}/tests/test_results_plots.py +1 -6
  34. flixopt-2.2.0b0/tests/test_scenarios.py +332 -0
  35. {flixopt-2.1.0 → flixopt-2.2.0b0}/tests/test_storage.py +1 -1
  36. flixopt-2.2.0b0/tests/test_timeseries.py +746 -0
  37. flixopt-2.1.0/flixopt/core.py +0 -970
  38. flixopt-2.1.0/flixopt/results.py +0 -898
  39. flixopt-2.1.0/site/release-notes/_template.txt +0 -32
  40. flixopt-2.1.0/tests/test_dataconverter.py +0 -113
  41. flixopt-2.1.0/tests/test_timeseries.py +0 -605
  42. {flixopt-2.1.0 → flixopt-2.2.0b0}/.github/workflows/python-app.yaml +0 -0
  43. {flixopt-2.1.0 → flixopt-2.2.0b0}/.gitignore +0 -0
  44. {flixopt-2.1.0 → flixopt-2.2.0b0}/LICENSE +0 -0
  45. {flixopt-2.1.0 → flixopt-2.2.0b0}/README.md +0 -0
  46. {flixopt-2.1.0 → flixopt-2.2.0b0}/docs/SUMMARY.md +0 -0
  47. {flixopt-2.1.0 → flixopt-2.2.0b0}/docs/contribute.md +0 -0
  48. {flixopt-2.1.0 → flixopt-2.2.0b0}/docs/examples/00-Minimal Example.md +0 -0
  49. {flixopt-2.1.0 → flixopt-2.2.0b0}/docs/examples/01-Basic Example.md +0 -0
  50. {flixopt-2.1.0 → flixopt-2.2.0b0}/docs/examples/02-Complex Example.md +0 -0
  51. {flixopt-2.1.0 → flixopt-2.2.0b0}/docs/examples/03-Calculation Modes.md +0 -0
  52. {flixopt-2.1.0 → flixopt-2.2.0b0}/docs/examples/index.md +0 -0
  53. {flixopt-2.1.0 → flixopt-2.2.0b0}/docs/faq/contribute.md +0 -0
  54. {flixopt-2.1.0 → flixopt-2.2.0b0}/docs/faq/index.md +0 -0
  55. {flixopt-2.1.0 → flixopt-2.2.0b0}/docs/getting-started.md +0 -0
  56. {flixopt-2.1.0 → flixopt-2.2.0b0}/docs/images/architecture_flixOpt-pre2.0.0.png +0 -0
  57. {flixopt-2.1.0 → flixopt-2.2.0b0}/docs/images/architecture_flixOpt.png +0 -0
  58. {flixopt-2.1.0 → flixopt-2.2.0b0}/docs/images/flixopt-icon.svg +0 -0
  59. {flixopt-2.1.0 → flixopt-2.2.0b0}/docs/index.md +0 -0
  60. {flixopt-2.1.0 → flixopt-2.2.0b0}/docs/javascripts/mathjax.js +0 -0
  61. {flixopt-2.1.0 → flixopt-2.2.0b0}/docs/release-notes/_template.txt +0 -0
  62. {flixopt-2.1.0 → flixopt-2.2.0b0}/docs/release-notes/index.md +0 -0
  63. {flixopt-2.1.0 → flixopt-2.2.0b0}/docs/release-notes/v2.0.0.md +0 -0
  64. {flixopt-2.1.0 → flixopt-2.2.0b0}/docs/release-notes/v2.0.1.md +0 -0
  65. {flixopt-2.1.0 → flixopt-2.2.0b0}/docs/release-notes/v2.1.0.md +0 -0
  66. {flixopt-2.1.0 → flixopt-2.2.0b0}/docs/user-guide/Mathematical Notation/Bus.md +0 -0
  67. {flixopt-2.1.0 → flixopt-2.2.0b0}/docs/user-guide/Mathematical Notation/Effects, Penalty & Objective.md +0 -0
  68. {flixopt-2.1.0 → flixopt-2.2.0b0}/docs/user-guide/Mathematical Notation/Flow.md +0 -0
  69. {flixopt-2.1.0 → flixopt-2.2.0b0}/docs/user-guide/Mathematical Notation/LinearConverter.md +0 -0
  70. {flixopt-2.1.0 → flixopt-2.2.0b0}/docs/user-guide/Mathematical Notation/Piecewise.md +0 -0
  71. {flixopt-2.1.0 → flixopt-2.2.0b0}/docs/user-guide/Mathematical Notation/Storage.md +0 -0
  72. {flixopt-2.1.0 → flixopt-2.2.0b0}/docs/user-guide/Mathematical Notation/index.md +0 -0
  73. {flixopt-2.1.0 → flixopt-2.2.0b0}/docs/user-guide/Mathematical Notation/others.md +0 -0
  74. {flixopt-2.1.0 → flixopt-2.2.0b0}/docs/user-guide/index.md +0 -0
  75. {flixopt-2.1.0 → flixopt-2.2.0b0}/examples/00_Minmal/minimal_example.py +0 -0
  76. {flixopt-2.1.0 → flixopt-2.2.0b0}/examples/01_Simple/simple_example.py +0 -0
  77. {flixopt-2.1.0 → flixopt-2.2.0b0}/examples/02_Complex/complex_example.py +0 -0
  78. {flixopt-2.1.0 → flixopt-2.2.0b0}/examples/02_Complex/complex_example_results.py +0 -0
  79. {flixopt-2.1.0 → flixopt-2.2.0b0}/examples/03_Calculation_types/Zeitreihen2020.csv +0 -0
  80. {flixopt-2.1.0 → flixopt-2.2.0b0}/flixopt/__init__.py +0 -0
  81. {flixopt-2.1.0 → flixopt-2.2.0b0}/flixopt/aggregation.py +0 -0
  82. {flixopt-2.1.0 → flixopt-2.2.0b0}/flixopt/commons.py +0 -0
  83. {flixopt-2.1.0 → flixopt-2.2.0b0}/flixopt/config.py +0 -0
  84. {flixopt-2.1.0 → flixopt-2.2.0b0}/flixopt/config.yaml +0 -0
  85. {flixopt-2.1.0 → flixopt-2.2.0b0}/flixopt/solvers.py +0 -0
  86. {flixopt-2.1.0 → flixopt-2.2.0b0}/flixopt.egg-info/dependency_links.txt +0 -0
  87. {flixopt-2.1.0 → flixopt-2.2.0b0}/flixopt.egg-info/requires.txt +0 -0
  88. {flixopt-2.1.0 → flixopt-2.2.0b0}/mkdocs.yml +0 -0
  89. {flixopt-2.1.0 → flixopt-2.2.0b0}/pics/architecture_flixOpt-pre2.0.0.png +0 -0
  90. {flixopt-2.1.0 → flixopt-2.2.0b0}/pics/architecture_flixOpt.png +0 -0
  91. {flixopt-2.1.0 → flixopt-2.2.0b0}/pics/flixOpt_plotting.jpg +0 -0
  92. {flixopt-2.1.0 → flixopt-2.2.0b0}/pics/flixopt-icon.svg +0 -0
  93. {flixopt-2.1.0 → flixopt-2.2.0b0}/pics/pics.pptx +0 -0
  94. {flixopt-2.1.0 → flixopt-2.2.0b0}/pyproject.toml +0 -0
  95. {flixopt-2.1.0 → flixopt-2.2.0b0}/scripts/gen_ref_pages.py +0 -0
  96. {flixopt-2.1.0 → flixopt-2.2.0b0}/setup.cfg +0 -0
  97. {flixopt-2.1.0 → flixopt-2.2.0b0}/tests/__init__.py +0 -0
  98. {flixopt-2.1.0 → flixopt-2.2.0b0}/tests/ressources/Zeitreihen2020.csv +0 -0
  99. {flixopt-2.1.0 → flixopt-2.2.0b0}/tests/test_bus.py +0 -0
  100. {flixopt-2.1.0 → flixopt-2.2.0b0}/tests/test_component.py +0 -0
  101. {flixopt-2.1.0 → flixopt-2.2.0b0}/tests/test_examples.py +0 -0
  102. {flixopt-2.1.0 → flixopt-2.2.0b0}/tests/test_functional.py +0 -0
  103. {flixopt-2.1.0 → flixopt-2.2.0b0}/tests/test_integration.py +0 -0
  104. {flixopt-2.1.0 → flixopt-2.2.0b0}/tests/test_linear_converter.py +0 -0
  105. {flixopt-2.1.0 → flixopt-2.2.0b0}/tests/test_on_hours_computation.py +0 -0
  106. {flixopt-2.1.0 → flixopt-2.2.0b0}/tests/todos.txt +0 -0
@@ -1,9 +1,11 @@
1
- name: Documentation
1
+ name: Deploy Stable Documentation
2
2
 
3
3
  on:
4
4
  release:
5
- types: [created] # Automatically deploy docs on release
6
- workflow_dispatch: # Allow manual triggering
5
+ types: [published]
6
+ tags:
7
+ # Only match stable version patterns (no pre-release identifiers)
8
+ - 'v[0-9]+.[0-9]+.[0-9]+'
7
9
 
8
10
  jobs:
9
11
  deploy-docs:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flixopt
3
- Version: 2.1.0
3
+ Version: 2.2.0b0
4
4
  Summary: Vector based energy and material flow optimization framework in Python.
5
5
  Author-email: "Chair of Building Energy Systems and Heat Supply, TU Dresden" <peter.stange@tu-dresden.de>, Felix Bumann <felixbumann387@gmail.com>, Felix Panitz <baumbude@googlemail.com>, Peter Stange <peter.stange@tu-dresden.de>
6
6
  Maintainer-email: Felix Bumann <felixbumann387@gmail.com>, Peter Stange <peter.stange@tu-dresden.de>
@@ -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
@@ -194,34 +194,34 @@ if __name__ == '__main__':
194
194
  # --- Plotting for comparison ---
195
195
  fx.plotting.with_plotly(
196
196
  get_solutions(calculations, 'Speicher|charge_state').to_dataframe(),
197
- mode='line',
197
+ style='line',
198
198
  title='Charge State Comparison',
199
199
  ylabel='Charge state',
200
200
  ).write_html('results/Charge State.html')
201
201
 
202
202
  fx.plotting.with_plotly(
203
203
  get_solutions(calculations, 'BHKW2(Q_th)|flow_rate').to_dataframe(),
204
- mode='line',
204
+ style='line',
205
205
  title='BHKW2(Q_th) Flow Rate Comparison',
206
206
  ylabel='Flow rate',
207
207
  ).write_html('results/BHKW2 Thermal Power.html')
208
208
 
209
209
  fx.plotting.with_plotly(
210
210
  get_solutions(calculations, 'costs(operation)|total_per_timestep').to_dataframe(),
211
- mode='line',
211
+ style='line',
212
212
  title='Operation Cost Comparison',
213
213
  ylabel='Costs [€]',
214
214
  ).write_html('results/Operation Costs.html')
215
215
 
216
216
  fx.plotting.with_plotly(
217
217
  pd.DataFrame(get_solutions(calculations, 'costs(operation)|total_per_timestep').to_dataframe().sum()).T,
218
- mode='bar',
218
+ style='stacked_bar',
219
219
  title='Total Cost Comparison',
220
220
  ylabel='Costs [€]',
221
221
  ).update_layout(barmode='group').write_html('results/Total Costs.html')
222
222
 
223
223
  fx.plotting.with_plotly(
224
- pd.DataFrame([calc.durations for calc in calculations], index=[calc.name for calc in calculations]), 'bar'
224
+ pd.DataFrame([calc.durations for calc in calculations], index=[calc.name for calc in calculations]), 'stacked_bar'
225
225
  ).update_layout(title='Duration Comparison', xaxis_title='Calculation type', yaxis_title='Time (s)').write_html(
226
226
  'results/Speed Comparison.html'
227
227
  )
@@ -0,0 +1,125 @@
1
+ """
2
+ This script shows how to use the flixopt framework to model a simple energy system.
3
+ """
4
+
5
+ import numpy as np
6
+ import pandas as pd
7
+ from rich.pretty import pprint # Used for pretty printing
8
+
9
+ import flixopt as fx
10
+
11
+ if __name__ == '__main__':
12
+ # Create datetime array starting from '2020-01-01' for the given time period
13
+ timesteps = pd.date_range('2020-01-01', periods=9, freq='h')
14
+ scenarios = pd.Index(['Base Case', 'High Demand'])
15
+
16
+ # --- Create Time Series Data ---
17
+ # Heat demand profile (e.g., kW) over time and corresponding power prices
18
+ heat_demand_per_h = pd.DataFrame({'Base Case':[30, 0, 90, 110, 110, 20, 20, 20, 20],
19
+ 'High Demand':[30, 0, 100, 118, 125, 20, 20, 20, 20]}, index=timesteps)
20
+ power_prices = np.array([0.08, 0.09])
21
+
22
+ flow_system = fx.FlowSystem(timesteps=timesteps, scenarios=scenarios, scenario_weights=np.array([0.5, 0.6]))
23
+
24
+ # --- Define Energy Buses ---
25
+ # These represent nodes, where the used medias are balanced (electricity, heat, and gas)
26
+ flow_system.add_elements(fx.Bus(label='Strom'), fx.Bus(label='Fernwärme'), fx.Bus(label='Gas'))
27
+
28
+ # --- Define Effects (Objective and CO2 Emissions) ---
29
+ # Cost effect: used as the optimization objective --> minimizing costs
30
+ costs = fx.Effect(
31
+ label='costs',
32
+ unit='€',
33
+ description='Kosten',
34
+ is_standard=True, # standard effect: no explicit value needed for costs
35
+ is_objective=True, # Minimizing costs as the optimization objective
36
+ )
37
+
38
+ # CO2 emissions effect with an associated cost impact
39
+ CO2 = fx.Effect(
40
+ label='CO2',
41
+ unit='kg',
42
+ description='CO2_e-Emissionen',
43
+ specific_share_to_other_effects_operation={costs.label: 0.2},
44
+ maximum_operation_per_hour=1000, # Max CO2 emissions per hour
45
+ )
46
+
47
+ # --- Define Flow System Components ---
48
+ # Boiler: Converts fuel (gas) into thermal energy (heat)
49
+ boiler = fx.linear_converters.Boiler(
50
+ label='Boiler',
51
+ eta=0.5,
52
+ Q_th=fx.Flow(label='Q_th', bus='Fernwärme', size=50, relative_minimum=0.1, relative_maximum=1, on_off_parameters=fx.OnOffParameters()),
53
+ Q_fu=fx.Flow(label='Q_fu', bus='Gas'),
54
+ )
55
+
56
+ # Combined Heat and Power (CHP): Generates both electricity and heat from fuel
57
+ chp = fx.linear_converters.CHP(
58
+ label='CHP',
59
+ eta_th=0.5,
60
+ eta_el=0.4,
61
+ P_el=fx.Flow('P_el', bus='Strom', size=60, relative_minimum=5 / 60, on_off_parameters=fx.OnOffParameters()),
62
+ Q_th=fx.Flow('Q_th', bus='Fernwärme'),
63
+ Q_fu=fx.Flow('Q_fu', bus='Gas'),
64
+ )
65
+
66
+ # Storage: Energy storage system with charging and discharging capabilities
67
+ storage = fx.Storage(
68
+ label='Storage',
69
+ charging=fx.Flow('Q_th_load', bus='Fernwärme', size=1000),
70
+ discharging=fx.Flow('Q_th_unload', bus='Fernwärme', size=1000),
71
+ capacity_in_flow_hours=fx.InvestParameters(fix_effects=20, fixed_size=30, optional=False),
72
+ initial_charge_state=0, # Initial storage state: empty
73
+ relative_maximum_charge_state=np.array([80, 70, 80, 80, 80, 80, 80, 80, 80, 80]) * 0.01,
74
+ eta_charge=0.9,
75
+ eta_discharge=1, # Efficiency factors for charging/discharging
76
+ relative_loss_per_hour=0.08, # 8% loss per hour. Absolute loss depends on current charge state
77
+ prevent_simultaneous_charge_and_discharge=True, # Prevent charging and discharging at the same time
78
+ )
79
+
80
+ # Heat Demand Sink: Represents a fixed heat demand profile
81
+ heat_sink = fx.Sink(
82
+ label='Heat Demand',
83
+ sink=fx.Flow(label='Q_th_Last', bus='Fernwärme', size=1, fixed_relative_profile=heat_demand_per_h),
84
+ )
85
+
86
+ # Gas Source: Gas tariff source with associated costs and CO2 emissions
87
+ gas_source = fx.Source(
88
+ label='Gastarif',
89
+ source=fx.Flow(label='Q_Gas', bus='Gas', size=1000, effects_per_flow_hour={costs.label: 0.04, CO2.label: 0.3}),
90
+ )
91
+
92
+ # Power Sink: Represents the export of electricity to the grid
93
+ power_sink = fx.Sink(
94
+ label='Einspeisung', sink=fx.Flow(label='P_el', bus='Strom', effects_per_flow_hour=-1 * power_prices)
95
+ )
96
+
97
+ # --- Build the Flow System ---
98
+ # Add all defined components and effects to the flow system
99
+ flow_system.add_elements(costs, CO2, boiler, storage, chp, heat_sink, gas_source, power_sink)
100
+
101
+ # Visualize the flow system for validation purposes
102
+ flow_system.plot_network(show=True)
103
+
104
+ # --- Define and Run Calculation ---
105
+ # Create a calculation object to model the Flow System
106
+ calculation = fx.FullCalculation(name='Sim1', flow_system=flow_system)
107
+ calculation.do_modeling() # Translate the model to a solvable form, creating equations and Variables
108
+
109
+ # --- Solve the Calculation and Save Results ---
110
+ calculation.solve(fx.solvers.HighsSolver(mip_gap=0, time_limit_seconds=30))
111
+
112
+ # --- Analyze Results ---
113
+ calculation.results['Fernwärme'].plot_node_balance_pie()
114
+ calculation.results['Fernwärme'].plot_node_balance(style='stacked_bar')
115
+ calculation.results['Storage'].plot_node_balance()
116
+ calculation.results.plot_heatmap('CHP(Q_th)|flow_rate')
117
+
118
+ # Convert the results for the storage component to a dataframe and display
119
+ df = calculation.results['Storage'].node_balance_with_charge_state()
120
+ print(df)
121
+ calculation.results['Storage'].plot_charge_state(engine='matplotlib')
122
+
123
+ # Save results to file for later usage
124
+ calculation.results.to_file()
125
+ fig, ax = calculation.results['Storage'].plot_charge_state(engine='matplotlib')
@@ -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):