Repository navigation
Feature request: WEC average power flows as Sankey diagrams #416
Copy link
Copy link
Open
Labels
Description
I think MHKiT would be a good platform to host a function that creates Sankey average power flow diagrams for WECs like this:
This is the code to generate the figure:
from matplotlib.sankey import Sankey
from typing import Tuple
from matplotlib.figure import Figure
from matplotlib.axes import Axes
import matplotlib.pyplot as plt
def power_flow_colors():
"""
Define and return a dictionary of colors to represent different stages of the power flow through a WEC.
The function creates a dictionary where each key corresponds to a specific stage of power flow,
and each value is a tuple representing an RGBA color. The colors are derived from the 'viridis'
colormap, which is perceptually uniform.
Returns:
dict: A dictionary containing the following stages of power flow and their associated colors:
- 'exc': Color for excitation power (RGBA: (0.267004, 0.004874, 0.329415, 1.0))
- 'rad': Color for radiated power (RGBA: (0.229739, 0.322361, 0.545706, 1.0))
- 'abs': Color for absorbed power (RGBA: (0.127568, 0.566949, 0.550556, 1.0))
- 'use': Color for useful power (RGBA: (0.369214, 0.788888, 0.382914, 1.0))
- 'elec': Color for electrical power (RGBA: (0.974417, 0.90359, 0.130215, 0.5))
Example:
colors = power_flow_colors()
print(colors['exc']) # Output: (0.267004, 0.004874, 0.329415, 1.0)
"""
clrs = {'exc': (0.267004, 0.004874, 0.329415, 1.0), #viridis(0.0)
'rad': (0.229739, 0.322361, 0.545706, 1.0), #viridis(0.25)
'abs': (0.127568, 0.566949, 0.550556, 1.0), #viridis(0.5)
'use': (0.369214, 0.788888, 0.382914, 1.0), #viridis(0.75)
'elec': (0.974417, 0.90359, 0.130215, 0.5), #viridis(0.99)
}
return clrs
def plot_power_flow(power_flows: dict[str, float],
plot_reference: bool = True,
axes_title: str = '',
axes: Axes = None,
return_fig_and_axes: bool = False
)-> Tuple[Figure, Axes]:
"""Plot power flow through a Wave Energy Converter (WEC) as a Sankey diagram.
This function visualizes the power flow through a WEC by creating a Sankey diagram.
If the model does not include mechanical and electrical components, customization of this function will be necessary.
Parameters
----------
power_flows : dict[str, float]
A dictionary containing power flow values produced by, for example,
:py:func:`wecopttool.utilities.calculate_power_flows`.
Required keys include:
- 'Optimal Excitation'
- 'Deficit Excitation'
- 'Excitation'
- 'Deficit Radiated'
- 'Deficit Absrobed'
- 'Radiated'
- 'Absorbed'
- 'Electrical'
- 'Useful'
- 'PTO Loss Mechanical'
- 'PTO Loss Electrical'
plot_reference : bool, optional
If True, the optimal absorbed reference powers will be plotted. Default is True.
axes_title : str, optional
A string to display as the title over the Sankey diagram. Default is an empty string.
axes : Axes, optional
A Matplotlib Axes object where the Sankey diagram will be drawn. If None, a new figure and axes will be created. Default is None.
return_fig_and_axes : bool, optional
If True, the function will return the Figure and Axes objects. Default is False.
Returns
-------
tuple[Figure, Axes] or None
A tuple containing the Matplotlib Figure and Axes objects if `return_fig_and_axes` is True.
Otherwise, returns None.
Example
-------
power_flows = {
'Optimal Excitation': 100,
'Deficit Excitation': 30,
'Excitation': 70,
'Deficit Radiated': 20,
'Deficit Absorbed': 10,
'Radiated': 30,
'Absorbed': 40,
'Electrical': 30,
'Useful': 35,
'PTO Loss Mechanical': 5,
'PTO Loss Electrical': 5
}
plot_power_flow(power_flows, axes_title='Power Flow Diagram')
"""
if axes is None:
fig, axes = plt.subplots(nrows = 1, ncols= 1,
tight_layout=True,
figsize= [8, 4])
clrs = power_flow_colors()
len_trunk = 1.0
if plot_reference:
sankey = Sankey(ax=axes,
scale= 1/power_flows['Optimal Excitation'],
offset= 0,
format = '%.1f',
shoulder = 0.02,
tolerance=1e-03*power_flows['Optimal Excitation'],
unit = 'W')
sankey.add(flows=[power_flows['Optimal Excitation'],
-1*power_flows['Deficit Excitation'],
-1*power_flows['Excitation']],
labels = [' Optimal \n Excitation ',
'Deficit \n Excitation',
'Excitation'],
orientations=[0, 0, 0],#arrow directions,
pathlengths = [0.15,0.15,0.15],
trunklength = len_trunk,
edgecolor = 'None',
facecolor = clrs['exc'],
alpha = 0.1,
label = 'Reference',
)
n_diagrams = 1
init_diag = 0
if power_flows['Deficit Excitation'] > 0.1:
sankey.add(flows=[power_flows['Deficit Excitation'],
-1*power_flows['Deficit Radiated'],
-1*power_flows['Deficit Absorbed'],],
labels = ['XX Deficit Exc',
'Deficit \n Radiated',
'Deficit \n Absorbed', ],
prior= (0),
connect=(1,0),
orientations=[0, 1, 0],#arrow directions,
pathlengths = [0.15,0.01,0.15],
trunklength = len_trunk,
edgecolor = 'None',
facecolor = clrs['rad'],
alpha = 0.3, #viridis(0.2)
label = 'Reference',
)
n_diagrams = n_diagrams +1
else:
sankey = Sankey(ax=axes,
scale= 1/power_flows['Excitation'],
offset= 0,
format = '%.1f',
shoulder = 0.02,
tolerance=1e-03*power_flows['Excitation'],
unit = 'W')
n_diagrams = 0
init_diag = None
sankey.add(flows=[power_flows['Excitation'],
-1*(power_flows['Absorbed']
+ power_flows['Radiated'])],
labels = ['Excitation',
'Excitation'],
prior = init_diag,
connect=(2,0),
orientations=[0, -0],#arrow directions,
pathlengths = [.15,0.15],
trunklength = len_trunk,
edgecolor = 'None',
facecolor = clrs['exc'] #viridis(0.9)
)
sankey.add(flows=[
(power_flows['Absorbed'] + power_flows['Radiated']),
-1*power_flows['Radiated'],
-1*power_flows['Absorbed']],
labels = ['Excitation',
'Radiated',
'Absorbed'],
# prior= (0),
prior= (n_diagrams),
connect=(1,0),
orientations=[0, -1, -0],#arrow directions,
pathlengths = [0.15,0.2,0.15],
trunklength = len_trunk-0.2,
edgecolor = 'None',
facecolor = clrs['rad'] #viridis(0.5)
)
sankey.add(flows=[power_flows['Absorbed'],
-1*power_flows['PTO Loss Mechanical'],
-1*power_flows['Useful']],
labels = ['Absorbed',
'PTO-Loss Mechanical' ,
'Useful'],
prior= (n_diagrams+1),
connect=(2,0),
orientations=[0, -1, -0],#arrow directions,
pathlengths = [.15,0.2,0.15],
trunklength = len_trunk,
edgecolor = 'None',
facecolor = clrs['abs'] #viridis(0.9)
)
sankey.add(flows=[(power_flows['Useful']),
-1*power_flows['PTO Loss Electrical'],
-1*power_flows['Electrical']],
labels = ['Useful',
'PTO-Loss Electrical' ,
'Electrical'],
prior= (n_diagrams+2),
connect=(2,0),
orientations=[0, -1, -0],#arrow directions,
pathlengths = [.15,0.2,0.15],
trunklength = len_trunk,
edgecolor = 'None',
facecolor = clrs['use'] #viridis(0.9)
)
sankey.add(flows=[(power_flows['Electrical']),
-1*power_flows['Electrical']],
labels = ['',
'Electrical'],
prior= (n_diagrams+3),
connect=(2,0),
orientations=[0, -0],#arrow directions,
pathlengths = [.15,0.15],
trunklength = len_trunk,
edgecolor = 'None',
facecolor = clrs['elec'] #viridis(0.9)
)
sankey.ax.axis([sankey.extent[0] - sankey.margin,
sankey.extent[1] + sankey.margin,
sankey.extent[2] - sankey.margin,
sankey.extent[3] + sankey.margin])
sankey.ax.set_aspect('equal', adjustable='box')
diagrams = sankey.diagrams
for diagram in diagrams:
for text in diagram.texts:
text.set_fontsize(8)
#Remvove labels that are double
len_diagrams = len(diagrams)
diagrams[len_diagrams-4].texts[0].set_text('') #remove exciation from hydro
diagrams[len_diagrams-5].texts[-1].set_text('') #remove excitation from excitation
diagrams[len_diagrams-3].texts[0].set_text('') #remove absorbed from absorbed
diagrams[len_diagrams-2].texts[0].set_text('') #remove use from use-elec
diagrams[len_diagrams-2].texts[-1].set_text('') #remove electrical from use-elec
diagrams[len_diagrams-1].texts[0].set_text('') #remove electrical in from elec
if len_diagrams > 5:
axes.legend() #add legend for the reference arrows
if len_diagrams >6:
diagrams[1].texts[0].set_text('')
axes.set_aspect('equal')
axes.set_title(axes_title)
axes.axis("off")
if return_fig_and_axes:
return fig, axes
power_flows = {
'Optimal Excitation': 100,
'Deficit Excitation': 30,
'Excitation': 70,
'Deficit Radiated': 20,
'Deficit Absorbed': 10,
'Radiated': 30,
'Absorbed': 40,
'Electrical': 30,
'Useful': 35,
'PTO Loss Mechanical': 5,
'PTO Loss Electrical': 5
}
plot_power_flow(power_flows, axes_title='Power Flow Diagram')Reactions are currently unavailable
Activity
akeeste commentedon Jan 6, 2026
Note for @akeeste and @megande4 per @simmsa request: look into ways we can pass the data flexibly so that this same sankey diagram function can be used for other scenarios beyond wave energy
Maybe we can custom the number of arrows, labels, and directions with a dictionary of values