Source code for marmot.plottingmodules.thermal_cap_reserve

# -*- coding: utf-8 -*-
"""Thermal capacity plots.

This module plots figures which show the amount of thermal capacity 
available but not committed (i.e in reserve)

@author: Daniel Levie and Marty Schwarz
"""

import logging
from pathlib import Path
from typing import List

import matplotlib.pyplot as plt
import pandas as pd

import marmot.utils.mconfig as mconfig
from marmot.plottingmodules.plotutils.plot_data_helper import (
    GenCategories,
    PlotDataStoreAndProcessor,
    set_facet_col_row_dimensions,
)
from marmot.plottingmodules.plotutils.plot_exceptions import (
    MissingInputData,
    MissingZoneData,
)
from marmot.plottingmodules.plotutils.plot_library import PlotLibrary
from marmot.plottingmodules.plotutils.styles import GeneratorColorDict
from marmot.plottingmodules.plotutils.timeseries_modifiers import (
    adjust_for_leapday,
    set_timestamp_date_range,
)

logger = logging.getLogger("plotter." + __name__)
plot_data_settings: dict = mconfig.parser("plot_data")
shift_leapday: bool = mconfig.parser("shift_leapday")


[docs]class ThermalReserve(PlotDataStoreAndProcessor): """Thermal capacity in reserve plots. The thermal_cap_reserve module contains methods that display the amount of generation in reserve, i.e non committed capacity. ThermalReserve inherits from the PlotDataStoreAndProcessor class to assist in creating figures. """ def __init__( self, Zones: List[str], Scenarios: List[str], AGG_BY: str, ordered_gen: List[str], marmot_solutions_folder: Path, gen_categories: GenCategories = GenCategories(), marmot_color_dict: dict = None, ylabels: List[str] = None, xlabels: List[str] = None, **kwargs, ): """ Args: Zones (List[str]): List of regions/zones to plot. Scenarios (List[str]): List of scenarios to plot. AGG_BY (str): Informs region type to aggregate by when creating plots. ordered_gen (List[str]): Ordered list of generator technologies to plot, order defines the generator technology position in stacked bar and area plots. marmot_solutions_folder (Path): Directory containing Marmot solution outputs. gen_categories (GenCategories): Instance of GenCategories class, groups generator technologies into defined categories. Deafults to GenCategories. marmot_color_dict (dict, optional): Dictionary of colors to use for generation technologies. Defaults to None. ylabels (List[str], optional): y-axis labels for facet plots. Defaults to None. xlabels (List[str], optional): x-axis labels for facet plots. Defaults to None. """ # Instantiation of PlotDataStoreAndProcessor super().__init__(AGG_BY, ordered_gen, marmot_solutions_folder, **kwargs) self.Zones = Zones self.Scenarios = Scenarios self.gen_categories = gen_categories if marmot_color_dict is None: self.marmot_color_dict = GeneratorColorDict.set_random_colors( self.ordered_gen ).color_dict else: self.marmot_color_dict = marmot_color_dict self.ylabels = ylabels self.xlabels = xlabels
[docs] def thermal_cap_reserves( self, start_date_range: str = None, end_date_range: str = None, data_resolution: str = "", **_, ): """Plots the total thermal generation capacity that is not committed, i.e in reserve. If multiple scenarios are included, each one will be plotted on a separate facet subplot. Args: start_date_range (str, optional): Defines a start date at which to represent data from. Defaults to None. end_date_range (str, optional): Defines a end date at which to represent data to. Defaults to None. data_resolution (str, optional): Specifies the data resolution to pull from the formatted data and plot. Defaults to "", which will pull interval data. .. versionadded:: 0.10.0 Returns: dict: Dictionary containing the created plot and its data table. """ outputs: dict = {} # List of properties needed by the plot, properties are a set of tuples and # contain 3 parts: required True/False, property name and scenarios required, # scenarios must be a list. properties = [ (True, f"generator_Generation{data_resolution}", self.Scenarios), (True, f"generator_Available_Capacity{data_resolution}", self.Scenarios), ] # Runs get_formatted_data within PlotDataStoreAndProcessor to populate PlotDataStoreAndProcessor dictionary # with all required properties, returns a 1 if required data is missing check_input_data = self.get_formatted_data(properties) if 1 in check_input_data: return MissingInputData() for zone_input in self.Zones: logger.info(f"Zone = {zone_input}") # sets up x, y dimensions of plot ncols, nrows = set_facet_col_row_dimensions( self.xlabels, self.ylabels, multi_scenario=self.Scenarios ) grid_size = ncols * nrows # Used to calculate any excess axis to delete plot_number = len(self.Scenarios) excess_axs = grid_size - plot_number mplt = PlotLibrary(nrows, ncols, sharey=True, squeeze=False, ravel_axs=True) fig, axs = mplt.get_figure() plt.subplots_adjust(wspace=0.05, hspace=0.2) data_table_chunks = [] for i, scenario in enumerate(self.Scenarios): logger.info(f"Scenario = {scenario}") generation: pd.DataFrame = self[ f"generator_Generation{data_resolution}" ].get(scenario) if shift_leapday: generation = adjust_for_leapday(generation) avail_cap: pd.DataFrame = self[ f"generator_Available_Capacity{data_resolution}" ].get(scenario) if shift_leapday: avail_cap = adjust_for_leapday(avail_cap) # Check if zone is in avail_cap try: avail_cap = avail_cap.xs(zone_input, level=self.AGG_BY) except KeyError: logger.warning(f"No installed capacity in: {zone_input}") break generation = generation.xs(zone_input, level=self.AGG_BY) avail_cap = self.df_process_gen_inputs(avail_cap) generation = self.df_process_gen_inputs(generation) generation = generation.loc[:, (generation != 0).any(axis=0)] thermal_reserve = avail_cap - generation non_thermal_gen = set(thermal_reserve.columns) - set( self.gen_categories.thermal ) # filter for only thermal generation thermal_reserve = thermal_reserve.drop(labels=non_thermal_gen, axis=1) # Convert units if i == 0: unitconversion = self.capacity_energy_unitconversion( thermal_reserve, self.Scenarios, sum_values=True ) thermal_reserve = thermal_reserve / unitconversion["divisor"] # Check if thermal_reserve contains data, if not skips if thermal_reserve.empty == True: out = MissingZoneData() outputs[zone_input] = out continue if pd.notna(start_date_range): thermal_reserve = set_timestamp_date_range( thermal_reserve, start_date_range, end_date_range ) if thermal_reserve.empty is True: logger.warning("No Generation in selected Date Range") continue # Create data table for each scenario scenario_names = pd.Series( [scenario] * len(thermal_reserve), name="Scenario" ) data_table = thermal_reserve.add_suffix(f" ({unitconversion['units']})") data_table = data_table.set_index([scenario_names], append=True) data_table_chunks.append(data_table) mplt.stackplot( thermal_reserve, color_dict=self.marmot_color_dict, labels=thermal_reserve.columns, sub_pos=i, ) axs[i].margins(x=0.01) mplt.set_subplot_timeseries_format(sub_pos=i) # add facet labels mplt.add_facet_labels(xlabels=self.xlabels, ylabels=self.ylabels) # Add legend mplt.add_legend(reverse_legend=True, sort_by=self.ordered_gen) # Remove extra axes mplt.remove_excess_axs(excess_axs, grid_size) plt.ylabel( f"Thermal capacity reserve ({unitconversion['units']})", color="black", rotation="vertical", labelpad=40, ) if plot_data_settings["plot_title_as_region"]: mplt.add_main_title(zone_input) # If data_table_chunks is empty, does not return data or figure if not data_table_chunks: outputs[zone_input] = MissingZoneData() continue # Concat all data tables together Data_Table_Out = pd.concat(data_table_chunks, copy=False, axis=0) outputs[zone_input] = {"fig": fig, "data_table": Data_Table_Out} return outputs