Skip to content

Feature request: WEC average power flows as Sankey diagrams #416

@dtgaebe

Description

@dtgaebe

I think MHKiT would be a good platform to host a function that creates Sankey average power flow diagrams for WECs like this:

Image

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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions