Skip to content

Commit 75a2391

Browse files
authored
Feature/398 feature facet plots in results (#419) (#422)
* Feature/398 feature facet plots in results (#419) * Add animation and faceting options to plots * Adjust size of the frame * Utilize plotly express directly * Rmeocve old class * Use plotly express and modify stackgroup afterwards * Add modifications also to animations * Mkae more compact * Remove height stuff * Remove line and make set opacity =0 for area * Integrate faceting and animating into existing with_plotly method * Improve results.py * Improve results.py * Move check if dims are found to plotting.py * Fix usage of indexer * Change selection string with indexer * Change behaviout of parameter "indexing" * Update CHANGELOG.md * Add new selection parameter to plotting methods * deprectae old indexer parameter * deprectae old indexer parameter * Add test * Add test * Add test * Add test * Fix not supportet check for matplotlib * Typo in CHANGELOG.md * Feature/398 feature facet plots in results heatmaps (#418) * Add animation and faceting options to plots * Adjust size of the frame * Utilize plotly express directly * Rmeocve old class * Use plotly express and modify stackgroup afterwards * Add modifications also to animations * Mkae more compact * Remove height stuff * Remove line and make set opacity =0 for area * Integrate faceting and animating into existing with_plotly method * Improve results.py * Improve results.py * Move check if dims are found to plotting.py * Fix usage of indexer * Change selection string with indexer * Change behaviout of parameter "indexing" * Update CHANGELOG.md * Add new selection parameter to plotting methods * deprectae old indexer parameter * deprectae old indexer parameter * Add test * Add test * Add test * Add test * Add heatmap support * Unify to a single heatmap method per engine * Change defaults * readd time reshaping * readd time reshaping * lengthen scenario example * Update * Improve heatmap plotting * Improve heatmap plotting * Moved reshaping to plotting.py * COmbinations are possible! * Improve 'auto'behavioour * Improve 'auto' behavioour * Improve 'auto' behavioour * Allow multiple variables in a heatmap * Update modeule level plot_heatmap() * remove code duplication * Allow Dataset instead of List of DataArrays * Allow Dataset instead of List of DataArrays * Update plot tests * FIx Missing renme in ElementResults.plot_heatmap() * Update API * Feature/398 feature facet plots in results charge state (#417) * Add animation and faceting options to plots * Adjust size of the frame * Utilize plotly express directly * Rmeocve old class * Use plotly express and modify stackgroup afterwards * Add modifications also to animations * Mkae more compact * Remove height stuff * Remove line and make set opacity =0 for area * Integrate faceting and animating into existing with_plotly method * Improve results.py * Improve results.py * Move check if dims are found to plotting.py * Fix usage of indexer * Change selection string with indexer * Change behaviout of parameter "indexing" * Update CHANGELOG.md * Add new selection parameter to plotting methods * deprectae old indexer parameter * deprectae old indexer parameter * Add test * Add test * Add test * Add test * Add heatmap support * Unify to a single heatmap method per engine * Change defaults * readd time reshaping * readd time reshaping * lengthen scenario example * Update * Improve heatmap plotting * Improve heatmap plotting * Moved reshaping to plotting.py * COmbinations are possible! * Improve 'auto'behavioour * Improve 'auto' behavioour * Improve 'auto' behavioour * Allow multiple variables in a heatmap * Update modeule level plot_heatmap() * remove code duplication * Allow Dataset instead of List of DataArrays * Allow Dataset instead of List of DataArrays * Add tests * More examples * Update plot_charge state() * Try 1 * Try 2 * Add more examples * Add more examples * Add smooth line for charge state and use "area" as default * Update scenario_example.py * Update tests * Fix Error handling in plot_heatmap() * Feature/398 feature facet plots in results pie (#421) * Add animation and faceting options to plots * Adjust size of the frame * Utilize plotly express directly * Rmeocve old class * Use plotly express and modify stackgroup afterwards * Add modifications also to animations * Mkae more compact * Remove height stuff * Remove line and make set opacity =0 for area * Integrate faceting and animating into existing with_plotly method * Improve results.py * Improve results.py * Move check if dims are found to plotting.py * Fix usage of indexer * Change selection string with indexer * Change behaviout of parameter "indexing" * Update CHANGELOG.md * Add new selection parameter to plotting methods * deprectae old indexer parameter * deprectae old indexer parameter * Add test * Add test * Add test * Add test * Add heatmap support * Unify to a single heatmap method per engine * Change defaults * readd time reshaping * readd time reshaping * lengthen scenario example * Update * Improve heatmap plotting * Improve heatmap plotting * Moved reshaping to plotting.py * COmbinations are possible! * Improve 'auto'behavioour * Improve 'auto' behavioour * Improve 'auto' behavioour * Allow multiple variables in a heatmap * Update modeule level plot_heatmap() * remove code duplication * Allow Dataset instead of List of DataArrays * Allow Dataset instead of List of DataArrays * Add tests * More examples * Update plot_charge state() * Try 1 * Try 2 * Add more examples * Add more examples * Add smooth line for charge state and use "area" as default * Update scenario_example.py * Update tests * Handle extra dims in pie plots by selecting the first * 6. Optimized time-step check - Replaced pandas Series diff() with NumPy np.diff() for better performance - Changed check from > 0 to > 1 (can't calculate diff with 0 or 1 element) - Converted to seconds first, then to minutes to avoid pandas timedelta conversion issues * Typo * Improve type handling * Update other tests * Handle backwards compatability * Add better error messages if both new and old api are used * Add old api explicitly * Add old api explicitly * Improve consistency and properly deprectae the indexer parameter * Remove amount of new tests * Remove amount of new tests * Fix CONTRIBUTING.md * Remove old test file * Add tests/test_heatmap_reshape.py * Add tests/test_heatmap_reshape.py * Remove unused method * - Implemented dashed line styling for "mixed" variables (variables with both positive and negative values) - Only stack "positive" and "negative" classifications, not "mixed" or "zero" * - Added fill parameter to module-level plot_heatmap function (line 1914) - Added fill parameter to CalculationResults.plot_heatmap method (line 702) - Forwarded fill parameter to both heatmap_with_plotly and heatmap_with_matplotlib functions * - Added np.random.seed(42) for reproducible test results - Added specific size assertions to all tests: - Daily/hourly pattern: 3 days × 24 hours - Weekly/daily pattern: 1 week × 7 days - Irregular data: 25 hours × 60 minutes - Multidimensional: 2 days × 24 hours with preserved scenario dimension * Improve Error Message if too many dims for matplotlib * Improve Error Message if too many dims for matplotlib * Improve Error Message if too many dims for matplotlib * Rename _apply_indexer_to_data() to _apply_selection_to_data() * Bugfix * Update CHANGELOG.md * Catch edge case in with_plotly() * Add strict=True * Improve scenario_example.py * Improve scenario_example.py * Change logging level in essage about time reshape * Update CHANGELOG.md
1 parent 84ab912 commit 75a2391

11 files changed

Lines changed: 1632 additions & 652 deletions

File tree

.github/CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ Thanks for your interest in contributing to FlixOpt! 🚀
1212

1313
2. **Install for Development**
1414
```bash
15-
pip install -e ".[full]"
15+
pip install -e ".[full, dev, docs]"
1616
```
1717

1818
3. **Make Changes & Submit PR**

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,21 @@ If upgrading from v2.x, see the [Migration Guide](https://flixopt.github.io/flix
5454
5555
5656
### ✨ Added
57+
- **Faceting and animation support for plots**: All plotting methods now support `facet_by` and `animate_by` parameters for creating subplot grids and animations with multidimensional data (scenarios, periods, etc.)
58+
- **New `select` parameter**: Added to all plotting methods for flexible data selection using single values, lists, slices, and index arrays
59+
- **Heatmap `fill` parameter**: Added `fill` parameter to heatmap plotting methods to control how missing values are filled after reshaping ('ffill' or 'bfill')
60+
- **Dashed line styling**: Area plots now automatically style "mixed" variables (containing both positive and negative values) with dashed lines, while only stacking purely positive or negative variables
5761
5862
### 💥 Breaking Changes
5963
6064
### ♻️ Changed
65+
- **Selection behavior**: Changed default selection behavior in plotting methods - no longer automatically selects first value for non-time dimensions. Use `select` parameter for explicit selection
66+
- **Improved error messages**: Enhanced error messages when using matplotlib engine with multidimensional data, providing clearer guidance on dimension requirements
67+
- Improved `scenario_example.py`
68+
- Improved error handling in `plot_heatmap()` method for better dimension validation
6169
6270
### 🗑️ Deprecated
71+
- **`indexer` parameter**: The `indexer` parameter in all plotting methods is deprecated in favor of the new `select` parameter with enhanced functionality
6372
6473
### 🔥 Removed
6574
@@ -75,6 +84,7 @@ If upgrading from v2.x, see the [Migration Guide](https://flixopt.github.io/flix
7584
- Improve docs visually with new Material theme and enhanced styling
7685
7786
### 👷 Development
87+
- Renamed `_apply_indexer_to_data()` to `_apply_selection_to_data()` for consistency with new API
7888
7989
### 🚧 Known Issues
8090

examples/04_Scenarios/scenario_example.py

Lines changed: 98 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,80 @@
88
import flixopt as fx
99

1010
if __name__ == '__main__':
11-
# Create datetime array starting from '2020-01-01' for the given time period
12-
timesteps = pd.date_range('2020-01-01', periods=9, freq='h')
11+
# Create datetime array starting from '2020-01-01' for one week
12+
timesteps = pd.date_range('2020-01-01', periods=24 * 7, freq='h')
1313
scenarios = pd.Index(['Base Case', 'High Demand'])
1414
periods = pd.Index([2020, 2021, 2022])
1515

1616
# --- Create Time Series Data ---
17-
# Heat demand profile (e.g., kW) over time and corresponding power prices
18-
heat_demand_per_h = pd.DataFrame(
19-
{'Base Case': [30, 0, 90, 110, 110, 20, 20, 20, 20], 'High Demand': [30, 0, 100, 118, 125, 20, 20, 20, 20]},
20-
index=timesteps,
17+
# Realistic daily patterns: morning/evening peaks, night/midday lows
18+
np.random.seed(42)
19+
n_hours = len(timesteps)
20+
21+
# Heat demand: 24-hour patterns (kW) for Base Case and High Demand scenarios
22+
base_daily_pattern = np.array(
23+
[22, 20, 18, 18, 20, 25, 40, 70, 95, 110, 85, 65, 60, 58, 62, 68, 75, 88, 105, 125, 130, 122, 95, 35]
24+
)
25+
high_daily_pattern = np.array(
26+
[28, 25, 22, 22, 24, 30, 52, 88, 118, 135, 105, 80, 75, 72, 75, 82, 92, 108, 128, 148, 155, 145, 115, 48]
27+
)
28+
29+
# Tile and add variation
30+
base_demand = np.tile(base_daily_pattern, n_hours // 24 + 1)[:n_hours] * (
31+
1 + np.random.uniform(-0.05, 0.05, n_hours)
32+
)
33+
high_demand = np.tile(high_daily_pattern, n_hours // 24 + 1)[:n_hours] * (
34+
1 + np.random.uniform(-0.07, 0.07, n_hours)
35+
)
36+
37+
heat_demand_per_h = pd.DataFrame({'Base Case': base_demand, 'High Demand': high_demand}, index=timesteps)
38+
39+
# Power prices: hourly factors (night low, peak high) and period escalation (2020-2022)
40+
hourly_price_factors = np.array(
41+
[
42+
0.70,
43+
0.65,
44+
0.62,
45+
0.60,
46+
0.62,
47+
0.70,
48+
0.95,
49+
1.15,
50+
1.30,
51+
1.25,
52+
1.10,
53+
1.00,
54+
0.95,
55+
0.90,
56+
0.88,
57+
0.92,
58+
1.00,
59+
1.10,
60+
1.25,
61+
1.40,
62+
1.35,
63+
1.20,
64+
0.95,
65+
0.80,
66+
]
2167
)
22-
power_prices = np.array([0.08, 0.09, 0.10])
68+
period_base_prices = np.array([0.075, 0.095, 0.135]) # €/kWh for 2020, 2021, 2022
2369

24-
flow_system = fx.FlowSystem(timesteps=timesteps, periods=periods, scenarios=scenarios, weights=np.array([0.5, 0.6]))
70+
price_series = np.zeros((n_hours, 3))
71+
for period_idx, base_price in enumerate(period_base_prices):
72+
price_series[:, period_idx] = (
73+
np.tile(hourly_price_factors, n_hours // 24 + 1)[:n_hours]
74+
* base_price
75+
* (1 + np.random.uniform(-0.03, 0.03, n_hours))
76+
)
77+
78+
power_prices = price_series.mean(axis=0)
79+
80+
# Scenario weights: probability of each scenario occurring
81+
# Base Case: 60% probability, High Demand: 40% probability
82+
scenario_weights = np.array([0.6, 0.4])
83+
84+
flow_system = fx.FlowSystem(timesteps=timesteps, periods=periods, scenarios=scenarios, weights=scenario_weights)
2585

2686
# --- Define Energy Buses ---
2787
# These represent nodes, where the used medias are balanced (electricity, heat, and gas)
@@ -35,22 +95,24 @@
3595
description='Kosten',
3696
is_standard=True, # standard effect: no explicit value needed for costs
3797
is_objective=True, # Minimizing costs as the optimization objective
38-
share_from_temporal={'CO2': 0.2},
98+
share_from_temporal={'CO2': 0.2}, # Carbon price: 0.2 €/kg CO2 (e.g., carbon tax)
3999
)
40100

41-
# CO2 emissions effect with an associated cost impact
101+
# CO2 emissions effect with constraint
102+
# Maximum of 1000 kg CO2/hour represents a regulatory or voluntary emissions limit
42103
CO2 = fx.Effect(
43104
label='CO2',
44105
unit='kg',
45106
description='CO2_e-Emissionen',
46-
maximum_per_hour=1000, # Max CO2 emissions per hour
107+
maximum_per_hour=1000, # Regulatory emissions limit: 1000 kg CO2/hour
47108
)
48109

49110
# --- Define Flow System Components ---
50111
# Boiler: Converts fuel (gas) into thermal energy (heat)
112+
# Modern condensing gas boiler with realistic efficiency
51113
boiler = fx.linear_converters.Boiler(
52114
label='Boiler',
53-
eta=0.5,
115+
eta=0.92, # Realistic efficiency for modern condensing gas boiler (92%)
54116
Q_th=fx.Flow(
55117
label='Q_th',
56118
bus='Fernwärme',
@@ -63,27 +125,28 @@
63125
)
64126

65127
# Combined Heat and Power (CHP): Generates both electricity and heat from fuel
128+
# Modern CHP unit with realistic efficiencies (total efficiency ~88%)
66129
chp = fx.linear_converters.CHP(
67130
label='CHP',
68-
eta_th=0.5,
69-
eta_el=0.4,
131+
eta_th=0.48, # Realistic thermal efficiency (48%)
132+
eta_el=0.40, # Realistic electrical efficiency (40%)
70133
P_el=fx.Flow('P_el', bus='Strom', size=60, relative_minimum=5 / 60, on_off_parameters=fx.OnOffParameters()),
71134
Q_th=fx.Flow('Q_th', bus='Fernwärme'),
72135
Q_fu=fx.Flow('Q_fu', bus='Gas'),
73136
)
74137

75-
# Storage: Energy storage system with charging and discharging capabilities
138+
# Storage: Thermal energy storage system with charging and discharging capabilities
139+
# Realistic thermal storage parameters (e.g., insulated hot water tank)
76140
storage = fx.Storage(
77141
label='Storage',
78142
charging=fx.Flow('Q_th_load', bus='Fernwärme', size=1000),
79143
discharging=fx.Flow('Q_th_unload', bus='Fernwärme', size=1000),
80144
capacity_in_flow_hours=fx.InvestParameters(effects_of_investment=20, fixed_size=30, mandatory=True),
81145
initial_charge_state=0, # Initial storage state: empty
82-
relative_maximum_charge_state=np.array([80, 70, 80, 80, 80, 80, 80, 80, 80]) * 0.01,
83-
relative_maximum_final_charge_state=0.8,
84-
eta_charge=0.9,
85-
eta_discharge=1, # Efficiency factors for charging/discharging
86-
relative_loss_per_hour=0.08, # 8% loss per hour. Absolute loss depends on current charge state
146+
relative_maximum_final_charge_state=np.array([0.8, 0.5, 0.1]),
147+
eta_charge=0.95, # Realistic charging efficiency (~95%)
148+
eta_discharge=0.98, # Realistic discharging efficiency (~98%)
149+
relative_loss_per_hour=np.array([0.008, 0.015]), # Realistic thermal losses: 0.8-1.5% per hour
87150
prevent_simultaneous_charge_and_discharge=True, # Prevent charging and discharging at the same time
88151
)
89152

@@ -94,10 +157,22 @@
94157
)
95158

96159
# Gas Source: Gas tariff source with associated costs and CO2 emissions
160+
# Realistic gas prices varying by period (reflecting 2020-2022 energy crisis)
161+
# 2020: 0.04 €/kWh, 2021: 0.06 €/kWh, 2022: 0.11 €/kWh
162+
gas_prices_per_period = np.array([0.04, 0.06, 0.11])
163+
164+
# CO2 emissions factor for natural gas: ~0.202 kg CO2/kWh (realistic value)
165+
gas_co2_emissions = 0.202
166+
97167
gas_source = fx.Source(
98168
label='Gastarif',
99169
outputs=[
100-
fx.Flow(label='Q_Gas', bus='Gas', size=1000, effects_per_flow_hour={costs.label: 0.04, CO2.label: 0.3})
170+
fx.Flow(
171+
label='Q_Gas',
172+
bus='Gas',
173+
size=1000,
174+
effects_per_flow_hour={costs.label: gas_prices_per_period, CO2.label: gas_co2_emissions},
175+
)
101176
],
102177
)
103178

@@ -124,21 +199,14 @@
124199
calculation.results.plot_heatmap('CHP(Q_th)|flow_rate')
125200

126201
# --- Analyze Results ---
127-
calculation.results['Fernwärme'].plot_node_balance_pie()
128202
calculation.results['Fernwärme'].plot_node_balance(mode='stacked_bar')
129-
calculation.results['Storage'].plot_node_balance()
130203
calculation.results.plot_heatmap('CHP(Q_th)|flow_rate')
204+
calculation.results['Storage'].plot_charge_state()
205+
calculation.results['Fernwärme'].plot_node_balance_pie(select={'period': 2020, 'scenario': 'Base Case'})
131206

132207
# Convert the results for the storage component to a dataframe and display
133208
df = calculation.results['Storage'].node_balance_with_charge_state()
134209
print(df)
135210

136-
# Plot charge state using matplotlib
137-
fig, ax = calculation.results['Storage'].plot_charge_state(engine='matplotlib')
138-
# Customize the plot further if needed
139-
ax.set_title('Storage Charge State Over Time')
140-
# Or save the figure
141-
# fig.savefig('storage_charge_state.png')
142-
143211
# Save results to file for later usage
144212
calculation.results.to_file()

0 commit comments

Comments
 (0)