Skip to content

Conversation

@lucysbrokenwings
Copy link

@lucysbrokenwings lucysbrokenwings commented Oct 4, 2025

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

    • Introduced a Demand-Side Management (DSM) Sink for flexible heat/demand modeling with time-shifting, losses, penalties and related model behavior.
    • Added DSM-aware results features: detection and Plotly-backed plotting of DSM charge states, rates and adjusted load.
    • Exposed DSM Sink in the public API.
  • Documentation

    • Added a minimal DSM example demonstrating setup, optimization, result analysis, plotting, and CSV export.
  • Style

    • Minor public API refinements to streamline DSM access and visualization.

fkeller added 30 commits May 19, 2025 12:37
…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
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 4, 2025

Walkthrough

Adds 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

Cohort / File(s) Summary
Example: Minimal DSM system
examples/00_Minmal/minimal_example_DSM.py
New example script building a time-indexed FlowSystem with two boilers, a DSM sink, a gas source, and cost objective; runs a FullCalculation solved by Gurobi; extracts cost/balance timeseries; plots and exports DSM results; prints calculation summary.
Public exports update
flixopt/__init__.py, flixopt/commons.py
Re-exports DSMSink by adding it to package-level imports and __all__, making DSMSink publicly available via flixopt and flixopt.commons.
DSM component and model
flixopt/components.py
Adds DSMSink class and DSMSinkModel with variables for positive/negative charge states and rates, state-evolution constraints with losses, flow/demand relations, cumulated surplus/deficit limits, forward/backward time-shift handling, penalties, exclusivity/prevent-simultaneous usage options, data transformation, and plausibility checks; imports extended for state/exclusivity features.
Results and plotting for DSM
flixopt/results.py
Adds ComponentResults.is_DSM_sink property and plot_DSM_sink method (Plotly-based) for DSM visualization; imports plotly.express and includes plotting/export options.

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
Loading
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)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

I thump my paws—new sinks delight,
Shifting warmth from day to night.
Surplus tucked, deficits trimmed light,
Timesteps hop in ordered flight.
Charts and constraints, tidy and bright. 🐇🔥

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Description Check ⚠️ Warning The pull request description provides a high-level overview of the new DSMSink class and related example changes but does not adhere to the repository’s description template as it lacks the Type of Change section, Related Issues, Testing information, and has no checklist entries. Please update the PR description to include the required sections from the template by specifying the Type of Change, filling out related issues, confirming testing steps, and completing the checklist items to ensure compliance with the repository standards.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The title succinctly and accurately describes the primary change, namely the introduction of a Demand Side Management Sink component, using clear and specific terminology without unnecessary detail or noise.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 96bad7c and 8c4c118.

📒 Files selected for processing (1)
  • examples/00_Minmal/minimal_example_DSM.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • examples/00_Minmal/minimal_example_DSM.py

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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

📥 Commits

Reviewing files that changed from the base of the PR and between 4b2b31f and 96bad7c.

📒 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)

Comment on lines +584 to +647
# 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,
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

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.

Suggested change
# 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>
@FBumann
Copy link
Member

FBumann commented Oct 5, 2025

Hi @lucysbrokenwings,
thanks a lot for your contribution.
We will review this as soon as possible.
Would you be available to discuss possible changes and/or renaming or parameters?

@lucysbrokenwings
Copy link
Author

Yes, of course.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants