Skip to content

Plot time series

add_reserves(n)

plot the reserves of the network

Source code in workflow/scripts/plot_time_series.py
def add_reserves(n: pypsa.Network):
    """plot the reserves of the network"""

    curtailed = n.statistics.curtailment(aggregate_time=False, bus_carrier="AC")

plot_energy_balance(n, plot_config, bus_carrier='AC', start_date='2060-03-31 21:00', end_date='2060-04-06 12:00:00', aggregate_fossil=False, add_load_line=True, add_reserves=False, ax=None)

Plot the electricity balance of the network for the given time range

Parameters:

Name Type Description Default
n Network

the network

required
plot_config dict

the plotting config (snakemake.config["plotting"])

required
bus_carrier str

the carrier for the energy_balance op. Defaults to "AC".

'AC'
start_date str

the range to plot. Defaults to "2060-03-31 21:00".

'2060-03-31 21:00'
end_date str

the range to plot. Defaults to "2060-04-06 12:00:00".

'2060-04-06 12:00:00'
aggregate_fossil bool

whether to aggregate fossil fuels. Defaults to False.

False
add_load_line bool

add a dashed line for the load. Defaults to True.

True
Source code in workflow/scripts/plot_time_series.py
def plot_energy_balance(
    n: pypsa.Network,
    plot_config: dict,
    bus_carrier="AC",
    start_date="2060-03-31 21:00",
    end_date="2060-04-06 12:00:00",
    aggregate_fossil=False,
    add_load_line=True,
    add_reserves=False,
    ax: plt.Axes = None,
):
    """Plot the electricity balance of the network for the given time range

    Args:
        n (pypsa.Network): the network
        plot_config (dict): the plotting config (snakemake.config["plotting"])
        bus_carrier (str, optional): the carrier for the energy_balance op. Defaults to "AC".
        start_date (str, optional): the range to plot. Defaults to "2060-03-31 21:00".
        end_date (str, optional): the range to plot. Defaults to "2060-04-06 12:00:00".
        aggregate_fossil (bool, optional): whether to aggregate fossil fuels. Defaults to False.
        add_load_line (bool, optional): add a dashed line for the load. Defaults to True.
    """
    if not ax:
        fig, ax = plt.subplots(figsize=(16, 8))
    else:
        fig = ax.get_figure()

    p = (
        n.statistics.energy_balance(aggregate_time=False, bus_carrier=bus_carrier)
        .dropna(how="all")
        .groupby("carrier")
        .sum()
        .div(PLOT_SUPPLY_UNITS)
        # .drop("-")
        .T
    )

    p = p.loc[start_date:end_date]
    p.rename(columns={"-": "Load", "AC": "transmission losses"}, inplace=True)

    # aggreg fossil
    if aggregate_fossil:
        coal = p.filter(regex="[C|c]oal")
        p.drop(columns=coal.columns, inplace=True)
        p["Coal"] = coal.sum(axis=1)
        gas = p.filter(regex="[G|g]as")
        p.drop(columns=gas.columns, inplace=True)
        p["Gas"] = gas.sum(axis=1)

    extra_c = {
        "Load": plot_config["tech_colors"]["electric load"],
        "transmission losses": plot_config["tech_colors"]["transmission losses"],
    }
    nice_tech_colors = make_nice_tech_colors(plot_config["tech_colors"], plot_config["nice_names"])
    color_series = get_stat_colors(n, nice_tech_colors, extra_colors=extra_c)
    # colors & names part 1
    p.rename(plot_config["nice_names"], inplace=True)
    p.rename(columns={k: k.title() for k in p.columns}, inplace=True)
    color_series.index = color_series.index.str.strip()
    # split into supply and wothdrawal
    supply = p.where(p > 0).dropna(axis=1, how="all")
    charge = p.where(p < 0).dropna(how="all", axis=1)

    # fix names and order

    charge.rename(columns={"Battery Storage": "Battery"}, inplace=True)
    supply.rename(columns={"Battery Discharger": "Battery"}, inplace=True)
    color_series = color_series[charge.columns.union(supply.columns)]
    color_series.rename(
        {"Battery Discharger": "Battery", "Battery Storage": "Battery"},
        inplace=True,
    )
    # Deduplicate color_series
    color_series = color_series[~color_series.index.duplicated(keep="first")]

    preferred_order = plot_config["preferred_order"]
    plot_order = (
        supply.columns.intersection(preferred_order).to_list()
        + supply.columns.difference(preferred_order).to_list()
    )

    plot_order_charge = [name for name in preferred_order if name in charge.columns] + [
        name for name in charge.columns if name not in preferred_order
    ]

    supply = supply.reindex(columns=plot_order)
    charge = charge.reindex(columns=plot_order_charge)
    if not charge.empty:
        charge.plot.area(ax=ax, linewidth=0, color=color_series.loc[charge.columns])

    supply.plot.area(
        ax=ax,
        linewidth=0,
        color=color_series.loc[supply.columns].values,
    )
    if add_load_line:
        charge["load_pos"] = charge["Load"] * -1
        charge["load_pos"].plot(linewidth=2, color="black", label="Load", ax=ax, linestyle="--")
        charge.drop(columns="load_pos", inplace=True)

    ax.legend(ncol=1, loc="center left", bbox_to_anchor=(1, 0.5), frameon=False, fontsize=16)
    ax.set_ylabel(PLOT_SUPPLY_LABEL)
    ax.set_ylim(charge.sum(axis=1).min() * 1.07, supply.sum(axis=1).max() * 1.07)
    ax.grid(axis="y")
    ax.set_xlim(supply.index.min(), supply.index.max())

    fig.tight_layout()

    return ax

plot_load_duration_curve(network, carrier='AC', ax=None)

Plot the load duration curve for the given carrier

Parameters:

Name Type Description Default
network Network

the pypasa network object

required
carrier str

the load carrier, defaults to AC

'AC'
ax Axes

figure axes, if none fig will be created. Defaults to None.

None

Returns:

Type Description
Axes

plt.Axes: the plotting axes

Source code in workflow/scripts/plot_time_series.py
def plot_load_duration_curve(
    network: pypsa.Network, carrier: str = "AC", ax: plt.Axes = None
) -> plt.Axes:
    """Plot the load duration curve for the given carrier

    Args:
        network (pypsa.Network): the pypasa network object
        carrier (str, optional): the load carrier, defaults to AC
        ax (plt.Axes, optional): figure axes, if none fig will be created. Defaults to None.

    Returns:
        plt.Axes: the plotting axes
    """

    if not ax:
        fig, ax = plt.subplots(figsize=(16, 8))
    else:
        fig = ax.get_figure()

    load = network.statistics.withdrawal(
        groupby=get_location_and_carrier,
        aggregate_time=False,
        bus_carrier=carrier,
        comps="Load",
    ).sum()
    load_curve = load.sort_values(ascending=False) / PLOT_CAP_UNITS
    load_curve.reset_index(drop=True).plot(ax=ax, lw=3)
    ax.set_ylabel(f"Load [{PLOT_CAP_LABEL}]")
    ax.set_xlabel("Hours")

    fig.tight_layout()
    return ax

plot_price_duration_by_node(network, carrier='AC', logy=True, y_lower=0.001, fig_shape=(8, 4))

Plot the price duration curve for the given carrier by node Args: network (pypsa.Network): the pypsa network object carrier (str, optional): the load carrier, defaults to AC (bus suffix) logy (bool, optional): use log scale for y axis, defaults to True y_lower (float, optional): lower limit for y axis, defaults to 1e-3 fig_shape (tuple, optional): shape of the figure, defaults to (8, 4)

Returns:

Type Description
Axes

plt.Axes: the plotting axes

Raises: ValueError: if the figure shape is too small for the number of regions

Source code in workflow/scripts/plot_time_series.py
def plot_price_duration_by_node(
    network: pypsa.Network,
    carrier: str = "AC",
    logy=True,
    y_lower=1e-3,
    fig_shape=(8, 4),
) -> plt.Axes:
    """Plot the price duration curve for the given carrier by node
    Args:
        network (pypsa.Network): the pypsa network object
        carrier (str, optional): the load carrier, defaults to AC (bus suffix)
        logy (bool, optional): use log scale for y axis, defaults to True
        y_lower (float, optional): lower limit for y axis, defaults to 1e-3
        fig_shape (tuple, optional): shape of the figure, defaults to (8, 4)

    Returns:
        plt.Axes: the plotting axes
    Raises:
        ValueError: if the figure shape is too small for the number of regions
    """

    if carrier == "AC":
        suffix = ""
    else:
        suffix = f" {carrier}"

    nodal_prices = network.buses_t.marginal_price[pd.Index(PROV_NAMES) + suffix]

    if fig_shape[0] * fig_shape[1] < len(nodal_prices.columns):
        raise ValueError(
            f"Figure shape {fig_shape} is too small for {len(nodal_prices.columns)} regions. "
            + "Please increase the number of subplots."
        )
    fig, axes = plt.subplots(fig_shape[0], fig_shape[1], sharex=True, sharey=True, figsize=(12, 12))

    # region by region sorting of prices
    for i, region in enumerate(nodal_prices.columns):
        reg_pr = nodal_prices[region]
        reg_pr.sort_values(ascending=False).reset_index(drop=True).plot(
            ax=axes[i // 4, i % fig_shape[1]], label=region
        )
        axes[i // 4, i % fig_shape[1]].set_title(region, fontsize=10)
        if logy:
            axes[i // 4, i % fig_shape[1]].semilogy()
        if y_lower:
            axes[i // 4, i % fig_shape[1]].set_ylim(y_lower, reg_pr.max() * 1.2)
        elif reg_pr.min() > 1e-5 and not logy:
            axes[i // 4, i % fig_shape[1]].set_ylim(0, reg_pr.max() * 1.2)
    fig.tight_layout(h_pad=0.2, w_pad=0.2)
    for ax in axes.flat:
        # Remove all x-tick labels except the largest value
        xticks = ax.get_xticks()
        if len(xticks) > 0:
            ax.set_xticks([xticks[0], xticks[-1]])
            ax.set_xticklabels([f"{xticks[0]:.0f}", f"{xticks[-1]:.0f}"])

    return ax

plot_price_duration_curve(network, carrier='AC', ax=None, figsize=(8, 8))

Plot the price duration curve for the given carrier

Parameters:

Name Type Description Default
network Network

the pypasa network object

required
carrier str

the load carrier, defaults to AC

'AC'
ax Axes

Axes to plot on, if none fig will be created. Defaults to None.

None
figsize tuple

size of the figure (if no ax given), defaults to (8, 8)

(8, 8)

Returns:

Type Description
Axes

plt.Axes: the plotting axes

Source code in workflow/scripts/plot_time_series.py
def plot_price_duration_curve(
    network: pypsa.Network, carrier="AC", ax: plt.Axes = None, figsize=(8, 8)
) -> plt.Axes:
    """Plot the price duration curve for the given carrier

    Args:
        network (pypsa.Network): the pypasa network object
        carrier (str, optional): the load carrier, defaults to AC
        ax (plt.Axes, optional): Axes to plot on, if none fig will be created. Defaults to None.
        figsize (tuple, optional): size of the figure (if no ax given), defaults to (8, 8)

    Returns:
        plt.Axes: the plotting axes
    """
    if not ax:
        fig, ax = plt.subplots(figsize=figsize)
    else:
        fig = ax.get_figure()

    ntwk_el_price = (
        -1
        * network.statistics.revenue(bus_carrier=carrier, aggregate_time=False, comps="Load")
        / network.statistics.withdrawal(bus_carrier=carrier, aggregate_time=False, comps="Load")
    ).T
    ntwk_el_price.rename(columns={"-": "Load"}, inplace=True)
    ntwk_el_price.Load.sort_values(ascending=False).reset_index(drop=True).plot(
        title="Price Duration Curve", ax=ax, lw=2
    )
    fig.tight_layout()

    return ax

plot_price_heatmap(network, carrier='AC', log_values=False, color_map='viridis', time_range=None, ax=None)

Plot the price heat map (region vs time) for the given carrier

Parameters:

Name Type Description Default
network Network

the pypsa network object

required
carrier str

the carrier for which to get the price. Defaults to "AC".

'AC'
log_values bool

whether to use log scale for the prices. Defaults to False.

False
color_map str

the color map to use. Defaults to "viridis".

'viridis'
time_range Index

the time range to plot. Defaults to None (all times).

None
ax Axes

the plotting axis. Defaults to None (new fig).

None

Returns:

Type Description
Axes

plt.Axes: the axes for plotting

Source code in workflow/scripts/plot_time_series.py
def plot_price_heatmap(
    network: pypsa.Network,
    carrier="AC",
    log_values=False,
    color_map="viridis",
    time_range: pd.Index = None,
    ax: plt.Axes = None,
) -> plt.Axes:
    """Plot the price heat map (region vs time) for the given carrier

    Args:
        network (pypsa.Network): the pypsa network object
        carrier (str, optional): the carrier for which to get the price. Defaults to "AC".
        log_values (bool, optional): whether to use log scale for the prices. Defaults to False.
        color_map (str, optional): the color map to use. Defaults to "viridis".
        time_range (pd.Index, optional): the time range to plot. Defaults to None (all times).
        ax (plt.Axes, optional): the plotting axis. Defaults to None (new fig).

    Returns:
        plt.Axes: the axes for plotting
    """

    if not ax:
        fig, ax = plt.subplots(figsize=(20, 8))
    else:
        fig = ax.get_figure()

    carrier_buses = network.buses.carrier[network.buses.carrier == carrier].index.values
    nodal_prices = network.buses_t.marginal_price[carrier_buses]

    if time_range is not None:
        # Filter nodal_prices by the given time range
        nodal_prices = nodal_prices.loc[time_range]
    # Normalize nodal_prices with log transformation
    if log_values:
        # Avoid log(0) by clipping values to a minimum of 0.1
        normalized_prices = np.log(nodal_prices.clip(lower=0.1))
        label = "Log-Transformed Price [€/MWh]"
    else:
        normalized_prices = nodal_prices
        label = "Price [€/MWh]"
    # Create a heatmap of normalized nodal_prices
    plot_index = normalized_prices.index.strftime("%m-%d %H:%M").to_list()
    normalized_prices.index = plot_index
    sns.heatmap(
        normalized_prices.T,
        cmap=color_map,
        cbar_kws={"label": label},
        ax=ax,
    )

    # Customize the plot
    if log_values:
        ax.set_title("Heatmap of Log-Transformed Nodal Prices")
    else:
        ax.set_title("Heatmap of Nodal Prices")

    ax.set_xlabel("Time")
    ax.set_ylabel("Nodes")
    fig.tight_layout()

    return ax

plot_regional_load_durations(network, carrier='AC', ax=None, cmap='plasma')

Plot the load duration curve for the given carrier stacked by region

Parameters:

Name Type Description Default
network Network

the pypasa network object

required
carrier str

the load carrier, defaults to AC

'AC'
ax Axes

axes to plot on, if none fig will be created. Defaults to None.

None

Returns:

Type Description
Axes

plt.Axes: the plotting axes

Source code in workflow/scripts/plot_time_series.py
def plot_regional_load_durations(
    network: pypsa.Network, carrier="AC", ax=None, cmap="plasma"
) -> plt.Axes:
    """Plot the load duration curve for the given carrier stacked by region

    Args:
        network (pypsa.Network): the pypasa network object
        carrier (str, optional): the load carrier, defaults to AC
        ax (plt.Axes, optional): axes to plot on, if none fig will be created. Defaults to None.

    Returns:
        plt.Axes: the plotting axes
    """
    if not ax:
        fig, ax = plt.subplots(figsize=(10, 8))
    else:
        fig = ax.get_figure()

    loads_all = network.statistics.withdrawal(
        groupby=get_location_and_carrier,
        aggregate_time=False,
        bus_carrier=carrier,
        comps="Load",
    ).sum()
    load_curve_all = loads_all.sort_values(ascending=False) / PLOT_CAP_UNITS
    regio = network.statistics.withdrawal(
        groupby=get_location_and_carrier,
        aggregate_time=False,
        bus_carrier=carrier,
        comps="Load",
    )
    regio = regio.droplevel(1).T
    load_curve_regio = regio.loc[load_curve_all.index] / PLOT_CAP_UNITS
    load_curve_regio.reset_index(drop=True).plot.area(
        ax=ax, stacked=True, cmap=cmap, legend=True, lw=3
    )
    ax.set_ylabel(f"Load [{PLOT_CAP_LABEL}]")
    ax.set_xlabel("Hours")
    ax.legend(
        ncol=3,
        loc="upper center",
        bbox_to_anchor=(0.5, -0.15),
        fontsize="small",
        title_fontsize="small",
        fancybox=True,
        shadow=True,
    )

    fig.tight_layout()

    return ax

plot_residual_load_duration_curve(network, ax=None, vre_techs=['Onshore Wind', 'Offshore Wind', 'Solar'])

Plot the residual load duration curve for the given carrier

Parameters:

Name Type Description Default
network Network

the pypasa network object

required
ax Axes

Axes to plot on, if none fig will be created. Defaults to None.

None

Returns:

Type Description
Axes

plt.Axes: the plotting axes

Source code in workflow/scripts/plot_time_series.py
def plot_residual_load_duration_curve(
    network, ax: plt.Axes = None, vre_techs=["Onshore Wind", "Offshore Wind", "Solar"]
) -> plt.Axes:
    """Plot the residual load duration curve for the given carrier

    Args:
        network (pypsa.Network): the pypasa network object
        ax (plt.Axes, optional): Axes to plot on, if none fig will be created. Defaults to None.

    Returns:
        plt.Axes: the plotting axes
    """
    CARRIER = "AC"
    if not ax:
        fig, ax = plt.subplots(figsize=(16, 8))
    load = network.statistics.withdrawal(
        groupby=get_location_and_carrier,
        aggregate_time=False,
        bus_carrier=CARRIER,
        comps="Load",
    ).sum()

    vre_supply = (
        network.statistics.supply(
            groupby=get_location_and_carrier,
            aggregate_time=False,
            bus_carrier=CARRIER,
            comps="Generator",
        )
        .groupby(level=1)
        .sum()
        .loc[vre_techs]
        .sum()
    )

    residual = (load - vre_supply).sort_values(ascending=False) / PLOT_CAP_UNITS
    residual.reset_index(drop=True).plot(ax=ax, lw=3)
    ax.set_ylabel(f"Residual Load [{PLOT_CAP_LABEL}]")
    ax.set_xlabel("Hours")

    return ax

plot_vre_heatmap(n, config, color_map='magma', log_values=True, time_range=None)

Plot the VRE generation per hour and day as a heatmap

Parameters:

Name Type Description Default
n Network

the pypsa network object

required
time_range Index

the time range to plot. Defaults to None (all times).

None
log_values bool

whether to use log scale for the values. Defaults to True.

True
config dict

the run config (snakemake.config).

required
Source code in workflow/scripts/plot_time_series.py
def plot_vre_heatmap(
    n: pypsa.Network, config: dict, color_map="magma", log_values=True, time_range: pd.Index = None,
):
    """Plot the VRE generation per hour and day as a heatmap

    Args:
        n (pypsa.Network): the pypsa network object
        time_range (pd.Index, optional): the time range to plot. Defaults to None (all times).
        log_values (bool, optional): whether to use log scale for the values. Defaults to True.
        config (dict, optional): the run config (snakemake.config).

    """

    vres = config["Techs"].get(
        "non_dispatchable", ["Offshore Wind", "Onshore Wind", "Solar", "Solar Residential"]
    )
    vre_avail = (
        n.statistics.supply(
            comps="Generator",
            aggregate_time=False,
            bus_carrier="AC",
            nice_names=False,
            groupby=["location", "carrier"],
        )
        .query("carrier in @vres")
        .T.fillna(0)
    )

    if time_range is not None:
        vre_avail = vre_avail.loc[time_range]

    for tech in vres[::-1]:
        tech_avail = vre_avail.T.query("carrier == @tech")
        tech_avail.index = tech_avail.index.droplevel(1)
        tech_avail = tech_avail.T
        tech_avail.index = tech_avail.index.strftime("%m-%d %H:%M")
        if log_values:
            # Avoid log(0) by clipping values to a minimum of 10
            tech_avail = np.log(tech_avail.clip(lower=10))
        fig, ax = plt.subplots()
        sns.heatmap(tech_avail.T, ax=ax, cmap=color_map)
        ax.set_title(f"{tech} generation by province")

plot_vre_timemap(network, color_map='viridis', time_range=None)

Plot the VRE generation per hour and day as a heatmap

Parameters:

Name Type Description Default
network Network

the pypsa network object

required
color_map str

the color map to use. Defaults to "viridis".

'viridis'
time_range Index

the time range to plot. Defaults to None (all times).

None
Source code in workflow/scripts/plot_time_series.py
def plot_vre_timemap(
    network: pypsa.Network,
    color_map="viridis",
    time_range: pd.Index = None,
):
    """Plot the VRE generation per hour and day as a heatmap

    Args:
        network (pypsa.Network): the pypsa network object
        color_map (str, optional): the color map to use. Defaults to "viridis".
        time_range (pd.Index, optional): the time range to plot. Defaults to None (all times).
    """

    vres = ["offwind", "onwind", "solar"]
    vre_avail = (
        network.statistics.supply(
            comps="Generator", aggregate_time=False, bus_carrier="AC", nice_names=False
        )
        .query("carrier in @vres")
        .T.fillna(0)
    )
    if time_range is not None:
        vre_avail = vre_avail.loc[time_range]

    vre_avail["day"] = vre_avail.index.strftime("%d-%m")
    vre_avail["hour"] = vre_avail.index.hour

    for tech in vres:
        pivot_ = vre_avail.pivot_table(index="hour", columns="day", values=tech)
        fig, ax = plt.subplots(figsize=(12, 6))
        sns.heatmap(pivot_.sort_index(ascending=False), cmap=color_map, ax=ax)
        ax.set_title(f"{tech} generation by hour and day")

        fig.tight_layout()