Skip to content

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

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

Activity

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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions