-
Notifications
You must be signed in to change notification settings - Fork 9
Demand Side Management Sink #376
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
…alty costs for DSMSinkVS to fix a bug; added extra timestep to said penalty costs
…n of timeshift DSM Sink class and changed application of penalty costs from surplus to deficits
…ome work on forward and backward timeshift limits
… they are now always set to 0. this could be reverted later however a change of the implementation of the timeshift limit would be necessary
…of relative bounds
…ed with the unified DSMSink class
…nges to native DSM diagrams
…hanges to plausibility checks
WalkthroughAdds a new DSMSink component and DSMSinkModel with DSM constraints and penalties, exposes DSMSink in package exports, extends ComponentResults with DSM detection and plotting, and adds a minimal example script demonstrating setup, solve, and result analysis for a DSM-enabled system. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User
participant Script as minimal_example_DSM.py
participant FlowSys as FlowSystem
participant DSMSink as DSMSink/DSMSinkModel
participant Solver as GurobiSolver
participant Results as ComponentResults
participant Plot as Plotting
User->>Script: run
Script->>FlowSys: create time index, buses, objective
Script->>FlowSys: add Boilers, Gas Source
Script->>DSMSink: configure DSM params (demand, limits, shifts, penalties)
DSMSink->>FlowSys: register component & model
Script->>Solver: run FullCalculation
Solver-->>FlowSys: return optimal solution
FlowSys-->>Results: assemble results (costs, balances, DSM vars)
Script->>Results: access timeseries, export DSM dataset
Script->>Plot: call plot_DSM_sink / plot balances
Note over DSMSink,Solver: DSM constraints enforce states, rates, losses, time-shifts
sequenceDiagram
autonumber
participant DSM as DSMSinkModel
participant Vars as DSM_Variables
participant Constr as DSM_Constraints
participant Cost as Objective
DSM->>Vars: define charge_state+/charge_state-
DSM->>Vars: define charge_rate+/charge_rate-
DSM->>Constr: add state-evolution (with loss rates)
DSM->>Constr: link flow = demand ± rates (apply bounds)
DSM->>Constr: add cumulated surplus/deficit limits
DSM->>Constr: enforce forward/backward time-shift windows
DSM->>Constr: add exclusivity/prevent-simultaneous constraints (optional)
DSM->>Cost: attach penalty costs for states/rates (if provided)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
examples/00_Minmal/minimal_example_DSM.py(1 hunks)flixopt/__init__.py(1 hunks)flixopt/commons.py(2 hunks)flixopt/components.py(3 hunks)flixopt/results.py(3 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
flixopt/__init__.py (1)
flixopt/components.py (1)
DSMSink(641-932)
flixopt/results.py (3)
flixopt/core.py (1)
to_dataframe(725-754)flixopt/plotting.py (2)
with_plotly(210-343)export_figure(1290-1340)flixopt/elements.py (2)
ComponentModel(712-762)Component(29-116)
flixopt/components.py (5)
flixopt/config.py (1)
CONFIG(89-144)flixopt/core.py (4)
TimeSeries(138-523)isel(376-377)create_time_series(580-623)active_data(345-347)flixopt/features.py (13)
StateModel(196-357)PreventSimultaneousUsageModel(1078-1118)do_modeling(45-74)do_modeling(243-287)do_modeling(383-441)do_modeling(484-566)do_modeling(664-722)do_modeling(790-826)do_modeling(860-913)do_modeling(949-982)do_modeling(1043-1075)do_modeling(1111-1118)on(746-747)flixopt/structure.py (12)
label(383-384)create_model(274-275)SystemModel(46-107)hours_per_step(94-95)label_full(278-279)label_full(387-393)do_modeling(62-70)do_modeling(330-331)add(333-357)coords(102-103)coords_extra(106-107)variables(426-427)flixopt/flow_system.py (2)
create_model(323-327)create_time_series(274-298)
flixopt/commons.py (1)
flixopt/components.py (1)
DSMSink(641-932)
examples/00_Minmal/minimal_example_DSM.py (5)
flixopt/flow_system.py (2)
FlowSystem(30-409)add_elements(119-143)flixopt/elements.py (2)
Bus(93-130)Flow(147-307)flixopt/effects.py (1)
Effect(27-111)flixopt/components.py (5)
DSMSink(641-932)do_modeling(349-382)do_modeling(408-446)do_modeling(459-515)do_modeling(953-1085)flixopt/results.py (3)
plot_node_balance(384-424)plot_DSM_sink(557-647)node_balance(501-521)
| # Get the node balance and the initial demand | ||
| node_balance = - self.node_balance(with_last_timestep=True).to_dataframe() | ||
| initial_demand = self._calculation_results.flow_system[f'{self.label}|initial_demand'].to_dataframe(name='initial_demand') | ||
|
|
||
| # Get surplus and deficit from the solution | ||
| surplus = self.solution[f'{self.label}|positive_charge_rate'].to_dataframe() | ||
| deficit = self.solution[f'{self.label}|negative_charge_rate'].to_dataframe() | ||
|
|
||
| # Substract surplus from node blance | ||
| node_balance[f'{self.inputs[0]}'] = node_balance[f'{self.inputs[0]}'].values.flatten() - surplus[f'{self.label}|positive_charge_rate'].values.flatten() | ||
|
|
||
| # Merge dataframes into one | ||
| data = pd.concat([node_balance, surplus, deficit], axis='columns') | ||
|
|
||
| # Create figure with area plot for node balance | ||
| fig = plotting.with_plotly( | ||
| data, | ||
| mode='area', | ||
| colors=colors, | ||
| title=f'DSM sink behaviour for {self.label}', | ||
| xlabel='Time in h' | ||
| ) | ||
|
|
||
| # Add initial demand | ||
| fig.add_trace( | ||
| plotly.graph_objs.Scatter( | ||
| x=initial_demand.index, | ||
| y=initial_demand['initial_demand'], | ||
| name='Initial Demand', | ||
| line=dict(dash='dash', color='black', shape='hv'), # 'hv' for horizontal-vertical steps | ||
| mode='lines' | ||
| ) | ||
| ) | ||
|
|
||
| # Get colors for the cumulated flow | ||
| color_samples = plotly.express.colors.sample_colorscale(colors, 8) | ||
| cumulated_color = color_samples[3] # Use another color from viridis for cumulated flow | ||
|
|
||
| # Get cumulated flow deviation from the model | ||
| cumulated_flow = pd.DataFrame(0, index=surplus.index, columns=['cumulated_flow']) | ||
| cumulated_flow['cumulated_flow'] = ( | ||
| self.solution[f'{self.label}|positive_charge_state'].values.flatten() + | ||
| self.solution[f'{self.label}|negative_charge_state'].values.flatten() | ||
| ) | ||
|
|
||
| # Add cumulated flow deviation as diamonds on secondary y-axis | ||
| fig.add_trace( | ||
| plotly.graph_objs.Scatter( | ||
| x=cumulated_flow.index, | ||
| y=cumulated_flow.values.flatten(), | ||
| name='Virtual Charge State', | ||
| line=dict(width=3, color='black'), | ||
| mode='lines' | ||
| ) | ||
| ) | ||
|
|
||
| return plotting.export_figure( | ||
| fig, | ||
| default_path=self._calculation_results.folder / f'{self.label} (DSM sink)', | ||
| default_filetype='.html', | ||
| user_path=None if isinstance(save, bool) else pathlib.Path(save), | ||
| show=show, | ||
| save=True if save else False, | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix length mismatches in DSM plot assembly
plot_DSM_sink currently mixes data indexed on timesteps_extra (node balance, charge states) with arrays indexed on timesteps (charge rates). On Line 593 you subtract surplus[...] (length T) from node_balance[...] (length T + 1), and on Lines 623‑626 you assign a (T + 1) array (charge states) into a DataFrame indexed by the T regular timesteps. Both operations raise ValueError: Length of values does not match length of index, so the plot always crashes instead of rendering. Align everything on the same index (e.g. drop the extra timestep when working with per-step rates, or reindex the rates onto timesteps_extra) before combining the frames.
A minimal fix is to keep per-step data on the regular timetable and only use the extra timestep for the cumulated state:
- node_balance = - self.node_balance(with_last_timestep=True).to_dataframe()
+ node_balance = - self.node_balance(with_last_timestep=False).to_dataframe()
…
- node_balance[f'{self.inputs[0]}'] = (
- node_balance[f'{self.inputs[0]}'].values.flatten()
- - surplus[f'{self.label}|positive_charge_rate'].values.flatten()
- )
+ node_balance.loc[surplus.index, f'{self.inputs[0]}'] = (
+ node_balance.loc[surplus.index, f'{self.inputs[0]}']
+ - surplus[f'{self.label}|positive_charge_rate']
+ )
…
- cumulated_flow = pd.DataFrame(0, index=surplus.index, columns=['cumulated_flow'])
- cumulated_flow['cumulated_flow'] = (
- self.solution[f'{self.label}|positive_charge_state'].values.flatten()
- + self.solution[f'{self.label}|negative_charge_state'].values.flatten()
- )
+ cumulated_flow = (
+ self.solution[f'{self.label}|positive_charge_state']
+ + self.solution[f'{self.label}|negative_charge_state']
+ ).to_dataframe(name='Virtual Charge State')Any equivalent alignment that prevents the shape mismatch and keeps the indices consistent works.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| # Get the node balance and the initial demand | |
| node_balance = - self.node_balance(with_last_timestep=True).to_dataframe() | |
| initial_demand = self._calculation_results.flow_system[f'{self.label}|initial_demand'].to_dataframe(name='initial_demand') | |
| # Get surplus and deficit from the solution | |
| surplus = self.solution[f'{self.label}|positive_charge_rate'].to_dataframe() | |
| deficit = self.solution[f'{self.label}|negative_charge_rate'].to_dataframe() | |
| # Substract surplus from node blance | |
| node_balance[f'{self.inputs[0]}'] = node_balance[f'{self.inputs[0]}'].values.flatten() - surplus[f'{self.label}|positive_charge_rate'].values.flatten() | |
| # Merge dataframes into one | |
| data = pd.concat([node_balance, surplus, deficit], axis='columns') | |
| # Create figure with area plot for node balance | |
| fig = plotting.with_plotly( | |
| data, | |
| mode='area', | |
| colors=colors, | |
| title=f'DSM sink behaviour for {self.label}', | |
| xlabel='Time in h' | |
| ) | |
| # Add initial demand | |
| fig.add_trace( | |
| plotly.graph_objs.Scatter( | |
| x=initial_demand.index, | |
| y=initial_demand['initial_demand'], | |
| name='Initial Demand', | |
| line=dict(dash='dash', color='black', shape='hv'), # 'hv' for horizontal-vertical steps | |
| mode='lines' | |
| ) | |
| ) | |
| # Get colors for the cumulated flow | |
| color_samples = plotly.express.colors.sample_colorscale(colors, 8) | |
| cumulated_color = color_samples[3] # Use another color from viridis for cumulated flow | |
| # Get cumulated flow deviation from the model | |
| cumulated_flow = pd.DataFrame(0, index=surplus.index, columns=['cumulated_flow']) | |
| cumulated_flow['cumulated_flow'] = ( | |
| self.solution[f'{self.label}|positive_charge_state'].values.flatten() + | |
| self.solution[f'{self.label}|negative_charge_state'].values.flatten() | |
| ) | |
| # Add cumulated flow deviation as diamonds on secondary y-axis | |
| fig.add_trace( | |
| plotly.graph_objs.Scatter( | |
| x=cumulated_flow.index, | |
| y=cumulated_flow.values.flatten(), | |
| name='Virtual Charge State', | |
| line=dict(width=3, color='black'), | |
| mode='lines' | |
| ) | |
| ) | |
| return plotting.export_figure( | |
| fig, | |
| default_path=self._calculation_results.folder / f'{self.label} (DSM sink)', | |
| default_filetype='.html', | |
| user_path=None if isinstance(save, bool) else pathlib.Path(save), | |
| show=show, | |
| save=True if save else False, | |
| ) | |
| # Get the node balance and the initial demand | |
| node_balance = - self.node_balance(with_last_timestep=False).to_dataframe() | |
| initial_demand = self._calculation_results.flow_system[f'{self.label}|initial_demand'].to_dataframe(name='initial_demand') | |
| # Get surplus and deficit from the solution | |
| surplus = self.solution[f'{self.label}|positive_charge_rate'].to_dataframe() | |
| deficit = self.solution[f'{self.label}|negative_charge_rate'].to_dataframe() | |
| # Subtract surplus from node balance (align on regular timesteps) | |
| node_balance.loc[surplus.index, f'{self.inputs[0]}'] = ( | |
| node_balance.loc[surplus.index, f'{self.inputs[0]}'] | |
| - surplus[f'{self.label}|positive_charge_rate'] | |
| ) | |
| # Merge dataframes into one | |
| data = pd.concat([node_balance, surplus, deficit], axis='columns') | |
| # Create figure with area plot for node balance | |
| fig = plotting.with_plotly( | |
| data, | |
| mode='area', | |
| colors=colors, | |
| title=f'DSM sink behaviour for {self.label}', | |
| xlabel='Time in h' | |
| ) | |
| # Add initial demand | |
| fig.add_trace( | |
| plotly.graph_objs.Scatter( | |
| x=initial_demand.index, | |
| y=initial_demand['initial_demand'], | |
| name='Initial Demand', | |
| line=dict(dash='dash', color='black', shape='hv'), | |
| mode='lines' | |
| ) | |
| ) | |
| # Get colors for the cumulated flow | |
| color_samples = plotly.express.colors.sample_colorscale(colors, 8) | |
| cumulated_color = color_samples[3] | |
| # Get cumulated flow deviation from the model (using the extra timestep) | |
| cumulated_flow = ( | |
| self.solution[f'{self.label}|positive_charge_state'] | |
| self.solution[f'{self.label}|negative_charge_state'] | |
| ).to_dataframe(name='Virtual Charge State') | |
| # Add cumulated flow as a line on a secondary y-axis | |
| fig.add_trace( | |
| plotly.graph_objs.Scatter( | |
| x=cumulated_flow.index, | |
| y=cumulated_flow.values.flatten(), | |
| name='Virtual Charge State', | |
| line=dict(width=3, color='black'), | |
| mode='lines' | |
| ) | |
| ) | |
| return plotting.export_figure( | |
| fig, | |
| default_path=self._calculation_results.folder / f'{self.label} (DSM sink)', | |
| default_filetype='.html', | |
| user_path=None if isinstance(save, bool) else pathlib.Path(save), | |
| show=show, | |
| save=True if save else False, | |
| ) |
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
|
Hi @lucysbrokenwings, |
|
Yes, of course. |
This branch adds a new DSMSink class, that may be used for modelling demand side management applications. The main changes can be found in components.py, where the core of the new class lies.
Additionally there is a new minimal example for the usage of the DSM sink (minimal_example_DSM.py) and a new diagram in results.py for plotting essential information of DSM sink elements.
Lastly some necessary changes to the dependencies were made (e.g. in __init__.py).
For the full documentation read the thesis "Implementation and application of a demand-side-management model in operational optimization".
Summary by CodeRabbit
New Features
Documentation
Style