Beispiel #1
0
    def __init__(self, grid_model, interconnect, table):
        """Constructor."""
        mi = ModelImmutables(grid_model)

        self.grid_model = mi.model
        self.interconnect = mi.interconnect_to_name(interconnect)

        self.base_grid = Grid(interconnect, source=grid_model)
        self.change_table = ChangeTable(self.base_grid)

        self.existing = table[table.interconnect == self.interconnect]
Beispiel #2
0
def test_check_resources_are_renewable_and_format():
    _check_resources_are_renewable_and_format(["wind_offshore", "wind"])
    _check_resources_are_renewable_and_format("solar")
    _check_resources_are_renewable_and_format({"wind"})
    _check_resources_are_renewable_and_format(
        {"solar"}, mi=ModelImmutables("europe_tub")
    )
Beispiel #3
0
def _check_areas_and_format(areas, mi=ModelImmutables("usa_tamu")):
    """Ensure that areas are valid. Duplicates are removed and state abbreviations are
    converted to their actual name.

    :param str/list/tuple/set areas: areas(s) to check. Could be load zone name(s),
        state name(s)/abbreviation(s) or interconnect(s).
    :param powersimdata.network.model.ModelImmutables mi: immutables of a grid model.
    :raises TypeError: if ``areas`` is not a list/tuple/set of str.
    :raises ValueError: if ``areas`` is empty or not valid.
    :return: (*set*) -- areas as a set. State/Country abbreviations are converted to
        state/country names.
    """
    if isinstance(areas, str):
        areas = {areas}
    elif isinstance(areas, (list, set, tuple)):
        if not all([isinstance(z, str) for z in areas]):
            raise TypeError("all areas must be str")
        areas = set(areas)
    else:
        raise TypeError("areas must be a str or a list/tuple/set of str")
    if len(areas) == 0:
        raise ValueError("areas must be non-empty")
    all_areas = set().union(*(mi.zones[z] for z in mi.zones["mappings"]))
    if not areas <= all_areas:
        diff = areas - all_areas
        raise ValueError("invalid area(s): %s" % " | ".join(diff))

    abv_in_areas = [z for z in areas if z in mi.zones["abv"]]
    for a in abv_in_areas:
        areas.remove(a)
        areas.add(mi.zones[f"abv2{mi.zones['division']}"][a])

    return areas
Beispiel #4
0
def generate_emissions_stats(scenario, pollutant="carbon", method="simple"):
    """Generate emissions statistics from the input generation data. Method
    descriptions: 'simple' uses a fixed ratio of CO2 to MWh, 'always-on' uses
    generator heat-rate curves including non-zero intercepts, 'decommit' uses
    generator heat-rate curves but de-commits generators if they are off
    (detected by pg < 1 MW).

    :param powersimdata.scenario.scenario.Scenario scenario: scenario instance.
    :param str pollutant: pollutant to analyze.
    :param str method: selected method to handle no-load fuel consumption.
    :return: (*pandas.DataFrame*) -- emissions data frame.
    """
    _check_scenario_is_in_analyze_state(scenario)
    mi = ModelImmutables(scenario.info["grid_model"])
    allowed_methods = {
        "carbon": {"simple", "always-on", "decommit"},
        "nox": {"simple"},
        "so2": {"simple"},
    }
    emissions_per_mwh = {
        "carbon": mi.plants["carbon_per_mwh"],
        "nox": mi.plants["nox_per_mwh"],
        "so2": mi.plants["so2_per_mwh"],
    }

    if pollutant not in allowed_methods.keys():
        raise ValueError("Unknown pollutant for generate_emissions_stats()")
    if not isinstance(method, str):
        raise TypeError("method must be a str")
    if method not in allowed_methods[pollutant]:
        err_msg = f"method for {pollutant} must be one of: {allowed_methods[pollutant]}"
        raise ValueError(err_msg)

    pg = scenario.state.get_pg()
    grid = scenario.state.get_grid()
    emissions = pd.DataFrame(np.zeros_like(pg),
                             index=pg.index,
                             columns=pg.columns)

    if method == "simple":
        for fuel, val in emissions_per_mwh[pollutant].items():
            indices = (grid.plant["type"] == fuel).to_numpy()
            emissions.loc[:, indices] = pg.loc[:, indices] * val / 1000
    elif method in ("decommit", "always-on"):
        decommit = True if method == "decommit" else False

        costs = calc_costs(pg, grid.gencost["before"], decommit=decommit)
        heat = np.zeros_like(costs)

        for fuel, val in mi.plants["carbon_per_mmbtu"].items():
            indices = (grid.plant["type"] == fuel).to_numpy()
            heat[:, indices] = (costs[:, indices] /
                                grid.plant["GenFuelCost"].values[indices])
            emissions.loc[:, indices] = heat[:, indices] * val * 44 / 12 / 1000
    else:
        raise Exception("I should not be able to get here")

    return emissions
Beispiel #5
0
    def __init__(self, interconnect):
        """Constructor."""
        super().__init__()

        self.grid_model = "usa_tamu"
        self.interconnect = check_and_format_interconnect(
            interconnect, model=self.grid_model)
        self.model_immutables = ModelImmutables(self.grid_model,
                                                interconnect=interconnect)
        self._set_data_loc(os.path.dirname(__file__))
def count_nodes_per_state(grid):
    """Count nodes per state to add as hover-over info in :func`map_interconnections`

    :param powersimdata.input.grid.Grid grid: grid object.
    :return: (*pandas.Series*) -- index: state names, values: number of nodes.
    """
    id2state = ModelImmutables(grid.grid_model).zones["id2abv"]
    grid.bus["state"] = grid.bus["zone_id"].map(id2state)
    counts = pd.Series(grid.bus["state"].value_counts())

    return counts
Beispiel #7
0
    def __init__(self, interconnect, source="usa_tamu", engine="REISE"):
        """Constructor."""
        if not isinstance(source, str):
            raise TypeError("source must be a str")
        supported_engines = {"REISE", "REISE.jl"}
        if engine not in supported_engines:
            raise ValueError(
                f"Engine must be one of {','.join(supported_engines)}")

        try:
            self.model_immutables = ModelImmutables(source)
        except ValueError:
            self.model_immutables = ModelImmutables(
                _get_grid_model_from_scenario_list(source))

        key = cache_key(interconnect, source)
        cached = _cache.get(key)
        if cached is not None:
            data = cached
        elif source == "usa_tamu":
            data = TAMU(interconnect)
        elif os.path.splitext(source)[1] == ".mat":
            if engine == "REISE":
                data = FromREISE(source)
            elif engine == "REISE.jl":
                data = FromREISEjl(source)

        self.data_loc = data.data_loc
        self.interconnect = data.interconnect
        self.zone2id = data.zone2id
        self.id2zone = data.id2zone
        self.sub = data.sub
        self.plant = data.plant
        self.gencost = data.gencost
        self.dcline = data.dcline
        self.bus2sub = data.bus2sub
        self.bus = data.bus
        self.branch = data.branch
        self.storage = data.storage

        _cache.put(key, self)
Beispiel #8
0
def count_nodes_per_state(grid):
    """Count nodes per state to add as hover-over info in :func`map_interconnections`

    :param powersimdata.input.grid.Grid grid: grid object.
    :return: (*pandas.DataFrame*) -- dataframe containing state names and count of nodes per state.
    """
    id2state = ModelImmutables(grid.get_grid_model()).zones["id2abv"]
    grid.bus["state"] = grid.bus["zone_id"].map(id2state)
    liststates = grid.bus["state"].value_counts()
    state_counts = pd.DataFrame(liststates)
    state_counts.reset_index(inplace=True)
    state_counts.rename(columns={"index": "state", "state": "count"}, inplace=True)

    return state_counts
Beispiel #9
0
def _check_resources_are_renewable_and_format(
    resources, mi=ModelImmutables("usa_tamu")
):
    """Ensure that resources are valid renewable resources and convert variable to
    a set.
    :param powersimdata.network.model.ModelImmutables mi: immutables of a grid model.
    :param str/list/tuple/set resources: resource(s) to analyze.
    :raises ValueError: if resources are not renewables.
    return: (*set*) -- resources as a set
    """
    resources = _check_resources_and_format(resources, mi=mi)
    if not resources <= mi.plants["renewable_resources"]:
        diff = resources - mi.plants["all_resources"]
        raise ValueError("invalid renewable resource(s): %s" % " | ".join(diff))
    return resources
Beispiel #10
0
    def __init__(self, interconnect, source="usa_tamu", engine="REISE"):
        """Constructor."""
        if not isinstance(source, str):
            raise TypeError("source must be a str")
        if source not in self.SUPPORTED_MODELS and not source.endswith(".mat"):
            raise ValueError(
                f"Source must be one of {','.join(self.SUPPORTED_MODELS)} "
                "or the path to a .mat file that represents a grid "
            )
        if engine not in self.SUPPORTED_ENGINES:
            raise ValueError(
                f"Engine must be one of {','.join(self.SUPPORTED_ENGINES)}"
            )

        key = cache_key(interconnect, source)
        cached = _cache.get(key)
        if cached is not None:
            data = cached
        elif source == "usa_tamu":
            data = TAMU(interconnect)
        elif os.path.splitext(source)[1] == ".mat":
            if engine == "REISE":
                data = FromREISE(source)
            elif engine == "REISE.jl":
                data = FromREISEjl(source)

        self.data_loc = data.data_loc
        self.interconnect = data.interconnect
        self.zone2id = data.zone2id
        self.id2zone = data.id2zone
        self.sub = data.sub
        self.plant = data.plant
        self.gencost = data.gencost
        self.dcline = data.dcline
        self.bus2sub = data.bus2sub
        self.bus = data.bus
        self.branch = data.branch
        self.storage = data.storage

        _cache.put(key, self)

        self.grid_model = self._get_grid_model()
        self.model_immutables = ModelImmutables(self.grid_model)
Beispiel #11
0
def _check_resources_and_format(resources, mi=ModelImmutables("usa_tamu")):
    """Ensure that resources are valid and convert variable to a set.

    :param str/list/tuple/set resources: resource(s) to check.
    :param powersimdata.network.model.ModelImmutables mi: immutables of a grid model.
    :raises TypeError: if resources is not a list/tuple/set of str.
    :raises ValueError: if resources is empty or not valid.
    :return: (*set*) -- resources as a set.
    """
    if isinstance(resources, str):
        resources = {resources}
    elif isinstance(resources, (list, set, tuple)):
        if not all([isinstance(r, str) for r in resources]):
            raise TypeError("all resources must be str")
        resources = set(resources)
    else:
        raise TypeError("resources must be a str or a list/tuple/set of str")
    if len(resources) == 0:
        raise ValueError("resources must be non-empty")
    if not resources <= mi.plants["all_resources"]:
        diff = resources - mi.plants["all_resources"]
        raise ValueError("invalid resource(s): %s" % " | ".join(diff))
    return resources
Beispiel #12
0
def _check_areas_and_format(areas, grid_model="usa_tamu"):
    """Ensure that areas are valid. Duplicates are removed and state abbreviations are
    converted to their actual name.

    :param str/list/tuple/set areas: areas(s) to check. Could be load zone name(s),
        state name(s)/abbreviation(s) or interconnect(s).
    :param str grid_model: grid model.
    :raises TypeError: if areas is not a list/tuple/set of str.
    :raises ValueError: if areas is empty or not valid.
    :return: (*set*) -- areas as a set. State abbreviations are converted to state
        names.
    """
    mi = ModelImmutables(grid_model)
    if isinstance(areas, str):
        areas = {areas}
    elif isinstance(areas, (list, set, tuple)):
        if not all([isinstance(z, str) for z in areas]):
            raise TypeError("all areas must be str")
        areas = set(areas)
    else:
        raise TypeError("areas must be a str or a list/tuple/set of str")
    if len(areas) == 0:
        raise ValueError("areas must be non-empty")
    all_areas = (mi.zones["loadzone"]
                 | mi.zones["abv"]
                 | mi.zones["state"]
                 | mi.zones["interconnect"])
    if not areas <= all_areas:
        diff = areas - all_areas
        raise ValueError("invalid area(s): %s" % " | ".join(diff))

    abv_in_areas = [z for z in areas if z in mi.zones["abv"]]
    for a in abv_in_areas:
        areas.remove(a)
        areas.add(mi.zones["abv2state"][a])

    return areas
Beispiel #13
0
    def __init__(self, grid_attrs=None, model="usa_tamu"):
        """Constructor.

        :param dict grid_attrs: dictionary of {*field_name*, *data_dict*} pairs
            where *field_name* is the name of the data frame (sub, bus2sub,
            branch, bus, dcline, gencost or plant) and *data_dict* a dictionary
            in which the keys are the column name of the data frame associated
            to *field_name* and the values are a list of values.
        :param str model: grid model. Use to access geographical information such
            as loadzones, interconnections, etc.
        """
        if grid_attrs is None:
            grid_attrs = {}

        if not isinstance(grid_attrs, dict):
            raise TypeError("grid_attrs must be a dict")

        for key in grid_attrs.keys():
            if not isinstance(key, str):
                raise TypeError("grid_attrs keys must all be str")

        extra_keys = set(grid_attrs.keys()) - acceptable_keys
        if len(extra_keys) > 0:
            raise ValueError("Got unknown key(s):" + str(extra_keys))

        self.model_immutables = ModelImmutables(model)

        cols = {
            "sub": sub_columns,
            "bus2sub": bus2sub_columns,
            "branch": branch_columns,
            "bus": bus_columns,
            "dcline": dcline_columns,
            "plant": plant_columns,
        }

        self.data_loc = None
        self.interconnect = None
        self.zone2id = {}
        self.id2zone = {}

        # Loop through names for grid data frames, add (maybe empty) data
        # frames.
        for df_name in indices:
            if df_name in grid_attrs:
                df = pd.DataFrame(grid_attrs[df_name])
            else:
                df = pd.DataFrame(columns=([indices[df_name]] + cols[df_name]))
            df.set_index(indices[df_name], inplace=True)
            setattr(self, df_name, df)

        # Gencost is special because there are two dataframes in a dict
        gencost = {}
        for gridattr_name, gc_name in gencost_names.items():
            if gridattr_name in grid_attrs:
                df = pd.DataFrame(grid_attrs[gridattr_name])
            else:
                df = pd.DataFrame(columns=(["plant_id"] + gencost_columns))
            df.set_index("plant_id", inplace=True)
            gencost[gc_name] = df
        self.gencost = gencost

        # Storage is special because there are multiple dataframes in a dict
        storage = {}
        for storage_attr_name, storage_name in storage_names.items():
            if storage_attr_name in grid_attrs:
                df = pd.DataFrame(grid_attrs[storage_attr_name])
            else:
                df = pd.DataFrame(
                    columns=(["plant_id"] + storage_columns[storage_name])
                )
            storage[storage_name] = df
        self.storage = storage
def plot_pie_generation_vs_capacity(
    areas,
    area_types=None,
    scenario_ids=None,
    scenario_names=None,
    time_range=None,
    time_zone=None,
    custom_data=None,
    resource_labels=None,
    resource_colors=None,
    min_percentage=0,
):
    """Plot any number of scenarios as pie charts with two columns per scenario -
    generation and capacity.

    :param list/str areas: list of area(s), each area is one of *loadzone*, *state*,
        *state abbreviation*, *interconnect*, *'all'*
    :param list/str area_types: list of area_type(s), each area_type is one of
        *'loadzone'*, *'state'*, *'state_abbr'*, *'interconnect'*, defaults to None.
    :param int/list/str scenario_ids: list of scenario id(s), defaults to None.
    :param list/str scenario_names: list of scenario name(s) of same len as scenario
        ids, defaults to None
    :param tuple time_range: [start_timestamp, end_timestamp] where each time stamp
        is pandas.Timestamp/numpy.datetime64/datetime.datetime. If None, the entire
        time range is used for the given scenario.
    :param str time_zone: new time zone, defaults to None, which uses UTC.
    :param list custom_data: list of dictionaries with each element being
        hand-generated data as returned by
        :func:`postreise.plot_bar_generation_vs_capacity.make_gen_cap_custom_data`,
        defaults to None.
    :param dict resource_labels: a dictionary with keys being resource types and values
        being labels, which is used to customize resource labels for selected resource
        types to show in the plots. Defaults to None, in which case a default set of
        labels is used.
    :param dict resource_colors: a dictionary with keys being resource types and values
        being colors, which is used to customize resource colors for selected resource
        types to show in the plots. Defaults to None, in which case a default set of
        colors is used.
    :param float min_percentage: roll up small pie pieces into a single category,
        resources with percentage less than the set value will be pooled together,
        defaults to 0.
    :raises ValueError:
        if length of area_types and areas is different and/or
        if length of scenario_names and scenario_ids is different and/or
        if less than two scenario_ids and/or custom_data in total is provided.
    :raises TypeError:
        if resource_labels are provided but not in a dictionary format and/or
        if resource_colors are provided but not in a dictionary format.

    .. note::
        if one wants to plot scenario data and custom data together, custom data MUST be
        in TWh for generation and GW for capacity in order to conduct appropriate
        comparison.
    """
    if isinstance(areas, str):
        areas = [areas]
    if isinstance(area_types, str):
        area_types = [area_types]
    if not area_types:
        area_types = [None] * len(areas)
    if len(areas) != len(area_types):
        raise ValueError(
            "ERROR: if area_types are provided, number of area_types must match number of areas"
        )

    if not scenario_ids:
        scenario_ids = []
    if isinstance(scenario_ids, (int, str)):
        scenario_ids = [scenario_ids]
    if isinstance(scenario_names, str):
        scenario_names = [scenario_names]
    if scenario_names and len(scenario_names) != len(scenario_ids):
        raise ValueError(
            "ERROR: if scenario names are provided, number of scenario names must match number of scenario ids"
        )
    if not custom_data:
        custom_data = {}
    if len(scenario_ids) + len(custom_data) <= 1:
        raise ValueError(
            "ERROR: must include at least two scenario ids and/or custom data")
    if not resource_labels:
        resource_labels = dict()
    if not isinstance(resource_labels, dict):
        raise TypeError("ERROR: resource_labels should be a dictionary")
    if not resource_colors:
        resource_colors = dict()
    if not isinstance(resource_colors, dict):
        raise TypeError("ERROR: resource_colors should be a dictionary")

    all_loadzone_data = {}
    scenario_data = {}
    for i, sid in enumerate(scenario_ids):
        scenario = Scenario(sid)
        mi = ModelImmutables(scenario.info["grid_model"])
        all_loadzone_data[sid] = {
            "gen":
            sum_generation_by_type_zone(
                scenario, time_range,
                time_zone).rename(columns=mi.zones["id2loadzone"]),
            "cap":
            sum_capacity_by_type_zone(scenario).rename(
                columns=mi.zones["id2loadzone"]),
        }
        scenario_data[sid] = {
            "name":
            scenario_names[i] if scenario_names else scenario.info["name"],
            "grid_model": mi.model,
            "type2color": {
                **mi.plants["type2color"],
                **resource_colors
            },
            "type2label": {
                **mi.plants["type2label"],
                **resource_labels
            },
            "gen": {
                "label": "Generation",
                "unit": "TWh",
                "data": {}
            },
            "cap": {
                "label": "Capacity",
                "unit": "GW",
                "data": {}
            },
        }
    for area, area_type in zip(areas, area_types):
        for sid in scenario_ids:
            loadzone_set = area_to_loadzone(scenario_data[sid]["grid_model"],
                                            area, area_type)
            scenario_data[sid]["gen"]["data"][area] = (
                all_loadzone_data[sid]["gen"][loadzone_set].sum(
                    axis=1).divide(1e6).astype("float").round(2).to_dict())
            scenario_data[sid]["cap"]["data"][area] = (
                all_loadzone_data[sid]["cap"][loadzone_set].sum(
                    axis=1).divide(1e3).astype("float").round(2).to_dict())
    for c_data in custom_data:
        scenario_data[c_data["name"]] = c_data

    for area in areas:
        ax_data_list = []
        for sd in scenario_data.values():
            for side in ["gen", "cap"]:
                ax_data, labels = _roll_up_small_pie_wedges(
                    sd[side]["data"][area], sd["type2label"], min_percentage)

                ax_data_list.append({
                    "title":
                    "{0}\n{1}".format(sd["name"], sd[side]["label"]),
                    "labels":
                    labels,
                    "values":
                    list(ax_data.values()),
                    "colors": [sd["type2color"][r] for r in ax_data.keys()],
                    "unit":
                    sd[side]["unit"],
                })

        _construct_pie_visuals(area, ax_data_list)
Beispiel #15
0
def test_check_resources_and_format():
    _check_resources_and_format(["dfo", "wind", "solar", "ng"])
    _check_resources_and_format("wind_offshore", mi=ModelImmutables("europe_tub"))
    _check_resources_and_format({"nuclear"})
    _check_resources_and_format("geothermal", mi=ModelImmutables("europe_tub"))
Beispiel #16
0
def plot_generation_time_series_stack(
    scenario,
    area,
    resources,
    area_type=None,
    time_range=None,
    time_zone="utc",
    time_freq="H",
    show_demand=True,
    show_net_demand=True,
    normalize=False,
    t2c=None,
    t2l=None,
    t2hc=None,
    title=None,
    label_fontsize=20,
    title_fontsize=22,
    tick_fontsize=15,
    legend_fontsize=18,
    save=False,
    filename=None,
    filepath=None,
):
    """Generate time series generation stack plot in a certain area of a scenario.

    :param powersimdata.scenario.scenario.Scenario scenario: scenario instance
    :param str area: one of *loadzone*, *state*, *state abbreviation*,
        *interconnect*, *'all'*
    :param str/list resources: one or a list of resources. *'solar_curtailment'*,
        *'wind_curtailment'*, *'wind_offshore_curtailment'* are valid entries together
        with all available generator types in the area. The order of the resources
        determines the stack order in the figure.
    :param str area_type: one of *'loadzone'*, *'state'*, *'state_abbr'*,
        *'interconnect'*
    :param tuple time_range: [start_timestamp, end_timestamp] where each time stamp
        is pandas.Timestamp/numpy.datetime64/datetime.datetime. If None, the entire
        time range is used for the given scenario.
    :param str time_zone: new time zone.
    :param str time_freq: frequency. Either *'D'* (day), *'W'* (week), *'M'* (month).
    :param bool show_demand: show demand line in the plot or not, default is True.
    :param bool show_net_demand: show net demand line in the plot or not, default is
        True.
    :param bool normalize: normalize the generation based on capacity or not,
        default is False.
    :param dict t2c: user specified color of resource type to overwrite type2color
        default dict. key: resource type, value: color code.
    :param dict t2l: user specified label of resource type to overwrite type2label
        default dict. key: resource type, value: label.
    :param dict t2hc: user specified color of curtailable resource hatches to overwrite
        type2hatchcolor default dict. key: resource type, valid keys are
        *'wind_curtailment'*, *'solar_curtailment'*, *'wind_offshore_curtailment'*,
        value: color code.
    :param str title: user specified title of the figure, default is set to be area.
    :param float label_fontsize: user specified label fontsize, default is 20.
    :param float title_fontsize: user specified title fontsize, default is 22.
    :param float tick_fontsize: user specified ticks of axes fontsize, default is 15.
    :param float legend_fontsize: user specified legend fontsize, default is 18.
    :param bool save: save the generated figure or not, default is False.
    :param str filename: if save is True, user specified filename, use area if None.
    :param str filepath: if save is True, user specified filepath, use current
        directory if None.
    """
    _check_scenario_is_in_analyze_state(scenario)

    mi = ModelImmutables(scenario.info["grid_model"])
    type2color = mi.plants["type2color"]
    type2label = mi.plants["type2label"]
    type2hatchcolor = mi.plants["type2hatchcolor"]
    if t2c:
        type2color.update(t2c)
    if t2l:
        type2label.update(t2l)
    if t2hc:
        type2hatchcolor.update(t2hc)

    pg_stack = get_generation_time_series_by_resources(scenario,
                                                       area,
                                                       resources,
                                                       area_type=area_type)
    capacity = get_capacity_by_resources(scenario,
                                         area,
                                         resources,
                                         area_type=area_type)
    demand = get_demand_time_series(scenario, area, area_type=area_type)
    net_demand = get_net_demand_time_series(scenario,
                                            area,
                                            area_type=area_type)
    capacity_ts = pd.Series(capacity.sum(), index=pg_stack.index)

    curtailable_resources = {
        "solar_curtailment",
        "wind_curtailment",
        "wind_offshore_curtailment",
    }
    if curtailable_resources & set(resources):
        curtailment = get_curtailment_time_series(scenario,
                                                  area,
                                                  area_type=area_type)
        for r in curtailable_resources:
            if r in resources and r in curtailment.columns:
                pg_stack[r] = curtailment[r]

    if time_zone != "utc":
        pg_stack = change_time_zone(pg_stack, time_zone)
        demand = change_time_zone(demand, time_zone)
        net_demand = change_time_zone(net_demand, time_zone)
        capacity_ts = change_time_zone(capacity_ts, time_zone)
    if not time_range:
        time_range = (
            pd.Timestamp(scenario.info["start_date"]),
            pd.Timestamp(scenario.info["end_date"]),
        )
    pg_stack = slice_time_series(pg_stack, time_range[0], time_range[1])
    demand = slice_time_series(demand, time_range[0], time_range[1])
    net_demand = slice_time_series(net_demand, time_range[0], time_range[1])
    capacity_ts = slice_time_series(capacity_ts, time_range[0], time_range[1])
    if time_freq != "H":
        pg_stack = resample_time_series(pg_stack, time_freq)
        demand = resample_time_series(demand, time_freq)
        net_demand = resample_time_series(net_demand, time_freq)
        capacity_ts = resample_time_series(capacity_ts, time_freq)

    if "storage" in resources:
        pg_storage = get_storage_time_series(scenario,
                                             area,
                                             area_type=area_type)
        capacity_storage = get_storage_capacity(scenario,
                                                area,
                                                area_type=area_type)
        capacity_storage_ts = pd.Series(capacity_storage,
                                        index=pg_storage.index)

        if time_zone != "utc":
            pg_storage = change_time_zone(pg_storage, time_zone)
            capacity_storage_ts = change_time_zone(capacity_storage_ts,
                                                   time_zone)
        if time_range != ("2016-01-01 00:00:00", "2016-12-31 23:00:00"):
            pg_storage = slice_time_series(pg_storage, time_range[0],
                                           time_range[1])
            capacity_storage_ts = slice_time_series(capacity_storage_ts,
                                                    time_range[0],
                                                    time_range[1])
        if time_freq != "H":
            pg_storage = resample_time_series(pg_storage, time_freq)
            capacity_storage_ts = resample_time_series(capacity_storage_ts,
                                                       time_freq)

        pg_stack["storage"] = pg_storage.clip(lower=0)
        capacity_ts += capacity_storage_ts

        fig, (ax, ax_storage) = plt.subplots(
            2,
            1,
            figsize=(20, 15),
            sharex="row",
            gridspec_kw={
                "height_ratios": [3, 1],
                "hspace": 0.02
            },
        )
        plt.subplots_adjust(wspace=0)
        if normalize:
            pg_storage = pg_storage.divide(capacity_storage_ts, axis="index")
            ax_storage.set_ylabel("Normalized Storage",
                                  fontsize=label_fontsize)
        else:
            ax_storage.set_ylabel("Energy Storage (MW)",
                                  fontsize=label_fontsize)

        ax_storage = pg_storage.plot(color=type2color["storage"],
                                     lw=4,
                                     ax=ax_storage)
        ax_storage.fill_between(
            pg_storage.index.values,
            0,
            pg_storage.values,
            color=type2color["storage"],
            alpha=0.5,
        )

        # Erase year in xticklabels
        xt_with_year = list(ax_storage.__dict__["date_axis_info"][0])
        xt_with_year[-1] = b"%b"
        ax_storage.__dict__["date_axis_info"][0] = tuple(xt_with_year)

        ax_storage.tick_params(axis="both",
                               which="both",
                               labelsize=tick_fontsize)
        ax_storage.set_xlabel("")
        for a in fig.get_axes():
            a.label_outer()
    else:
        fig = plt.figure(figsize=(20, 10))
        ax = fig.gca()

    if normalize:
        pg_stack = pg_stack.divide(capacity_ts, axis="index")
        demand = demand.divide(capacity_ts, axis="index")
        net_demand = net_demand.divide(capacity_ts, axis="index")
        ax.set_ylabel("Normalized Generation", fontsize=label_fontsize)
    else:
        pg_stack = pg_stack.divide(1e6, axis="index")
        demand = demand.divide(1e6, axis="index")
        net_demand = net_demand.divide(1e6, axis="index")
        ax.set_ylabel("Daily Energy TWh", fontsize=label_fontsize)

    available_resources = [r for r in resources if r in pg_stack.columns]
    pg_stack[available_resources].clip(0, None).plot.area(color=type2color,
                                                          linewidth=0,
                                                          alpha=0.7,
                                                          ax=ax,
                                                          sharex="row")

    if show_demand:
        demand.plot(color="red", lw=4, ax=ax)
    if show_net_demand:
        net_demand.plot(color="red", ls="--", lw=2, ax=ax)

    if not title:
        title = area
    ax.set_title("%s" % title, fontsize=title_fontsize)
    ax.grid(color="black", axis="y")

    if "storage" not in resources:
        # Erase year in xticklabels
        xt_with_year = list(ax.__dict__["date_axis_info"][0])
        xt_with_year[-1] = b"%b"
        ax.__dict__["date_axis_info"][0] = tuple(xt_with_year)
        ax.set_xlabel("")

    ax.tick_params(which="both", labelsize=tick_fontsize)
    ax.set_ylim([
        min(0, 1.1 * net_demand.min()),
        max(ax.get_ylim()[1], 1.1 * demand.max()),
    ])

    handles, labels = ax.get_legend_handles_labels()
    if show_demand:
        labels[0] = "Demand"
    if show_net_demand:
        labels[1] = "Net Demand"
    label_offset = show_demand + show_net_demand
    labels = [type2label[l] if l in type2label else l for l in labels]

    # Add hatches
    for r in curtailable_resources:
        if r in available_resources:
            ind = available_resources.index(r)
            ax.fill_between(
                pg_stack[available_resources].index.values,
                pg_stack[available_resources].iloc[:, :ind + 1].sum(axis=1),
                pg_stack[available_resources].iloc[:, :ind].sum(axis=1),
                color="none",
                hatch="//",
                edgecolor=type2hatchcolor[r],
                linewidth=0.0,
            )
            handles[ind + label_offset] = mpatches.Patch(
                facecolor=type2color[r],
                hatch="//",
                edgecolor=type2hatchcolor[r],
                linewidth=0.0,
            )

    ax.legend(
        handles[::-1],
        labels[::-1],
        frameon=2,
        prop={"size": legend_fontsize},
        loc="upper left",
        bbox_to_anchor=(1, 1),
    )

    if save:
        if not filename:
            filename = area
        if not filepath:
            filepath = os.path.join(os.getcwd(), filename)
        plt.savefig(f"{filepath}.pdf", bbox_inches="tight", pad_inches=0)
Beispiel #17
0
def plot_curtailment_time_series(
    scenario,
    area,
    resources,
    area_type=None,
    time_range=None,
    time_zone="utc",
    time_freq="H",
    show_demand=True,
    percentage=True,
    t2c=None,
    t2l=None,
    title=None,
    label_fontsize=20,
    title_fontsize=22,
    tick_fontsize=15,
    legend_fontsize=18,
    save=False,
    filename=None,
    filepath=None,
):
    """Generate time series curtailment plot of each specified resource in a certain
    area of a scenario.

    :param powersimdata.scenario.scenario.Scenario scenario: scenario instance
    :param str area: one of *loadzone*, *state*, *state abbreviation*,
        *interconnect*, *'all'*
    :param str/list resources: one or a list of resources.
    :param str area_type: one of *'loadzone'*, *'state'*, *'state_abbr'*,
        *'interconnect'*
    :param tuple time_range: [start_timestamp, end_timestamp] where each time stamp
        is pandas.Timestamp/numpy.datetime64/datetime.datetime. If None, the entire
        time range is used for the given scenario.
    :param str time_zone: new time zone.
    :param str time_freq: frequency. Either *'D'* (day), *'W'* (week), *'M'* (month).
    :param bool show_demand: show demand line in the plot or not, default is True.
    :param bool percentage: plot the curtailment in terms of percentage or not,
        default is True.
    :param dict t2c: user specified color of resource type to overwrite type2color
        default dict. key: resource type, value: color code.
    :param dict t2l: user specified label of resource type to overwrite type2label
        default dict. key: resource type, value: label.
    :param str title: user specified title of the figure.
    :param float label_fontsize: user specified label fontsize, default is 20.
    :param float title_fontsize: user specified title fontsize, default is 22.
    :param float tick_fontsize: user specified ticks of axes fontsize, default is 15.
    :param float legend_fontsize: user specified legend fontsize, default is 18.
    :param bool save: save the generated figure or not, default is False.
    :param str filename: if save is True, user specified filename, use area if None.
    :param str filepath: if save is True, user specified filepath, use current
        directory if None.
    """
    _check_scenario_is_in_analyze_state(scenario)
    resources = _check_resources_and_format(
        resources, grid_model=scenario.info["grid_model"])

    mi = ModelImmutables(scenario.info["grid_model"])
    type2color = mi.plants["type2color"]
    type2label = mi.plants["type2label"]
    if t2c:
        type2color.update(t2c)
    if t2l:
        type2label.update(t2l)

    resource_pg = get_generation_time_series_by_resources(scenario,
                                                          area,
                                                          resources,
                                                          area_type=area_type)
    demand = get_demand_time_series(scenario, area, area_type=area_type)
    curtailment = get_curtailment_time_series(scenario,
                                              area,
                                              area_type=area_type)

    resource_pg = change_time_zone(resource_pg, time_zone)
    demand = change_time_zone(demand, time_zone)
    curtailment = change_time_zone(curtailment, time_zone)
    if not time_range:
        time_range = (
            pd.Timestamp(scenario.info["start_date"], tz="utc"),
            pd.Timestamp(scenario.info["end_date"], tz="utc"),
        )
    resource_pg = slice_time_series(resource_pg, time_range[0], time_range[1])
    demand = slice_time_series(demand, time_range[0], time_range[1])
    curtailment = slice_time_series(curtailment, time_range[0], time_range[1])
    if time_freq != "H":
        resource_pg = resample_time_series(resource_pg, time_freq)
        demand = resample_time_series(demand, time_freq)
        curtailment = resample_time_series(curtailment, time_freq)

    for r in resource_pg.columns:
        curtailment[r + "_curtailment" +
                    "_mean"] = curtailment[r + "_curtailment"].mean()
        curtailment[
            r +
            "_available"] = curtailment[r + "_curtailment"] + resource_pg[r]
        curtailment[r + "_curtailment" +
                    "_percentage"] = (curtailment[r + "_curtailment"] /
                                      curtailment[r + "_available"] * 100)
        curtailment[r + "_curtailment" + "_percentage" +
                    "_mean"] = curtailment[r + "_curtailment" +
                                           "_percentage"].mean()

    for r in resources:
        if r not in resource_pg.columns:
            raise ValueError(f"{r} is invalid in {area}!")
        fig = plt.figure(figsize=(20, 10))
        ax = fig.gca()

        title_text = f"{area} {r.capitalize()}" if not title else title
        plt.title(title_text, fontsize=title_fontsize)

        cr = r + "_curtailment"
        if percentage:
            key1, key2 = f"{cr}_percentage", f"{cr}_percentage_mean"
        else:
            key1, key2 = cr, f"{cr}_mean"
        curtailment[key1].plot(
            ax=ax,
            lw=4,
            alpha=0.7,
            color=type2color[cr],
            label=type2label[cr],
        )
        curtailment[key2].plot(
            ax=ax,
            ls="--",
            lw=4,
            alpha=0.7,
            color=type2color[cr],
            label=type2label[cr] + " Mean",
        )

        ax_twin = ax.twinx()
        curtailment[r + "_available"].plot(
            ax=ax_twin,
            lw=4,
            alpha=0.7,
            color=type2color[r],
            label=f"{type2label[r]} Energy Available",
        )

        if show_demand:
            demand.plot(ax=ax_twin,
                        lw=4,
                        alpha=0.7,
                        color="red",
                        label="Demand")

        # Erase year in xticklabels
        xt_with_year = list(ax_twin.__dict__["date_axis_info"][0])
        xt_with_year[-1] = b"%b"
        ax_twin.__dict__["date_axis_info"][0] = tuple(xt_with_year)
        ax_twin.set_xlabel("")
        ax_twin.tick_params(which="both", labelsize=tick_fontsize)
        ax_twin.yaxis.get_offset_text().set_fontsize(tick_fontsize)
        ax_twin.set_ylabel("MWh", fontsize=label_fontsize)
        ax_twin.legend(loc="upper right", prop={"size": legend_fontsize})

        ax.tick_params(which="both", labelsize=tick_fontsize)
        ax.yaxis.get_offset_text().set_fontsize(tick_fontsize)
        ax.grid(color="black", axis="y")
        ax.set_xlabel("")
        if percentage:
            ax.set_ylabel("Curtailment [%]", fontsize=label_fontsize)
        else:
            ax.set_ylabel("Curtailment", fontsize=label_fontsize)
        ax.legend(loc="upper left", prop={"size": legend_fontsize})

        if save:
            if not filename:
                filename = f"{area.lower()}_{r}_curtailment"
            if not filepath:
                filepath = os.path.join(os.getcwd(), filename)
            plt.savefig(f"{filepath}.pdf", bbox_inches="tight", pad_inches=0)
def plot_n_scenarios(*args):
    """For 1-to-n scenarios, plot stacked energy and carbon side-by-side.

    :param powersimdata.scenario.scenario.Scenario args: scenario instances.
    :raises ValueError: if arguments are not scenario instances.
    """
    if not all([isinstance(a, Scenario) for a in args]):
        raise ValueError("all inputs must be Scenario objects")
    # Build dictionaries of calculated data for each scenario
    scenario_numbers = [a.info["id"] for a in args]
    first_id = scenario_numbers[0]
    scenarios = {id: scen for (id, scen) in zip(scenario_numbers, args)}
    grid = {
        id: scenario.state.get_grid()
        for id, scenario in scenarios.items()
    }
    plant = {k: v.plant for k, v in grid.items()}
    # First scenario is chosen to set fuel colors
    type2color = ModelImmutables(
        args[0].info["grid_model"]).plants["type2color"]
    carbon_by_type, energy_by_type = {}, {}
    for id, scenario in scenarios.items():
        # Calculate raw numbers
        annual_plant_energy = scenario.state.get_pg().sum()
        raw_energy_by_type = annual_plant_energy.groupby(plant[id].type).sum()
        annual_plant_carbon = generate_emissions_stats(scenario).sum()
        raw_carbon_by_type = annual_plant_carbon.groupby(plant[id].type).sum()
        # Drop fuels with zero energy (e.g. all offshore_wind scaled to 0 MW)
        energy_by_type[id] = raw_energy_by_type[raw_energy_by_type != 0]
        carbon_by_type[id] = raw_carbon_by_type[raw_energy_by_type != 0]
    # Carbon multiplier is inverse of carbon intensity, to scale bar heights
    carbon_multiplier = energy_by_type[first_id].sum(
    ) / carbon_by_type[first_id].sum()

    # Determine the fuel types with generation in either scenario
    fuels = list(set.union(*[set(v.index) for v in energy_by_type.values()]))
    # Fill zeros in scenarios without a fuel when it's present in another
    for id in scenario_numbers:
        energy_by_type[id] = energy_by_type[id].reindex(fuels, fill_value=0)
        carbon_by_type[id] = carbon_by_type[id].reindex(fuels, fill_value=0)
    # Re-order according to plotting preferences
    fuels = [f for f in all_possible if f in fuels]
    # Re-assess subsets
    renewable_fuels = [f for f in possible_renewable if f in fuels]
    clean_fuels = [f for f in possible_clean if f in fuels]
    carbon_fuels = [f for f in possible_carbon if f in fuels]
    dropped_fuels = (set(possible_clean) | set(possible_carbon)) - (
        set(clean_fuels) | set(carbon_fuels))
    print("no energy in any grid(s), dropping: %s" % ", ".join(dropped_fuels))

    fuel_series = {
        f: sum(
            [[energy_by_type[s][f], carbon_by_type[s][f] * carbon_multiplier]
             for s in scenario_numbers],
            [],
        )
        for f in fuels
    }
    num_bars = 2 * len(scenario_numbers)
    ind = np.arange(num_bars)
    width = 0.5
    line_alpha = 0.25
    line_offsets = np.array((0.25, 0.75))

    # Strart plotting
    fig = plt.figure(figsize=(10, 8))
    ax = fig.add_subplot(111)
    # Plot bars
    patches = {}
    bottom = np.zeros(num_bars)
    for i, f in enumerate(fuels):
        patches[i] = plt.bar(ind,
                             fuel_series[f],
                             width,
                             bottom=bottom,
                             color=type2color[f])
        bottom += fuel_series[f]
    # Plot lines
    for i, fuel in enumerate(carbon_fuels):
        for j, s in enumerate(scenario_numbers):
            cumulative_energy = sum(
                [energy_by_type[s][carbon_fuels[k]] for k in range(i + 1)])
            cumulative_scaled_carbon = carbon_multiplier * sum(
                [carbon_by_type[s][carbon_fuels[k]] for k in range(i + 1)])
            ys = (cumulative_energy, cumulative_scaled_carbon)
            xs = line_offsets + 2 * j
            plt.plot(xs, ys, "k-", alpha=line_alpha)

    # Labeling
    energy_fmt = "{0:,.0f} TWh\n{1:.0f}% renewable\n{2:.0f}% carbon-free\n"
    carbon_fmt = "{0:,.0f} million\ntons CO2\n"
    energy_total = {s: energy_by_type[s].sum() for s in scenarios}
    for j, s in enumerate(scenario_numbers):
        carbon_total = carbon_by_type[s].sum()
        renewable_energy = sum([energy_by_type[s][f] for f in renewable_fuels])
        clean_energy = sum([energy_by_type[s][f] for f in clean_fuels])
        renewable_share = renewable_energy / energy_total[s] * 100
        clean_share = clean_energy / energy_total[s] * 100
        annotation_str = energy_fmt.format(energy_total[s] / 1e6,
                                           renewable_share, clean_share)
        annotation_x = 2 * (j - 0.08)
        plt.annotate(xy=(annotation_x, energy_total[s]), text=annotation_str)
        annotation_str = carbon_fmt.format(carbon_total / 1e6)
        annotation_x = 2 * (j - 0.08) + 1
        annotation_y = carbon_total * carbon_multiplier
        plt.annotate(xy=(annotation_x, annotation_y), text=annotation_str)

    # Plot formatting
    plt.ylim((0, max(energy_total.values()) * 1.2))
    labels = sum([["%s Energy" % id, "%s Carbon" % id]
                  for id in scenario_numbers], [])
    plt.xticks(ind, labels)
    plt.yticks([], [])
    ax.tick_params(axis="x", labelsize=12)
    # Invert default legend order to match stack ordering
    plt.legend(
        handles=(patches[i] for i in range(len(fuels) - 1, -1, -1)),
        labels=fuels[::-1],
        fontsize=12,
        bbox_to_anchor=(1.02, 1),
        loc="upper left",
    )
    plt.tight_layout()
    plt.show()
Beispiel #19
0
    def _read(self):
        data = loadmat(self.data_loc, squeeze_me=True, struct_as_record=False)
        mpc = data["mdi"].mpc
        try:
            # The next line will fail if no iess attribute (index energy storage system)
            # Since we use the 'squeeze_me' param, 1 storage -> an int, not an array
            n_storage = 1 if isinstance(mpc.iess, int) else mpc.iess.shape[0]
        except AttributeError:
            n_storage = 0

        try:
            dclineid = mpc.dclineid
            if isinstance(dclineid, int):
                n_dcline = 1
            else:
                n_dcline = mpc.dclineid.shape[0]
        except AttributeError:
            n_dcline = 0

        # bus
        self.bus, _ = frame("bus", mpc.bus, mpc.busid)
        self.bus.drop(columns=["bus_id"], inplace=True)

        # plant
        self.plant, plant_storage = frame("plant",
                                          mpc.gen,
                                          mpc.genid,
                                          n_storage=n_storage)
        self.plant["type"] = mpc.genfuel[:len(mpc.genid)]
        self.plant["GenFuelCost"] = mpc.genfuelcost

        # heat rate curve
        self.heat_rate_curve, _ = frame("heat_rate_curve", mpc.heatratecurve,
                                        mpc.genid)

        # gencost before
        self.gencost["before"], _ = frame("gencost_before", mpc.gencost_orig,
                                          mpc.genid)

        # gencost after
        self.gencost["after"], gencost_storage = frame("gencost_after",
                                                       mpc.gencost,
                                                       mpc.genid,
                                                       n_storage=n_storage)

        # branch
        self.branch, _ = frame("branch", mpc.branch, mpc.branchid)
        self.branch["branch_device_type"] = mpc.branchdevicetype

        # DC line
        if n_dcline > 1:
            self.dcline, _ = frame("dcline", mpc.dcline, mpc.dclineid)
        elif n_dcline == 1:
            # When n_dcline == 1, mpc.dclineid is an int instead of an array,
            #   and mpc.dcline has been squeezed to 1D. Here we fix these.
            dcline_index = np.array([mpc.dclineid])
            dcline_table = np.expand_dims(mpc.dcline, 0)
            self.dcline, _ = frame("dcline", dcline_table, dcline_index)
        else:
            self.dcline = pd.DataFrame(
                columns=column_name_provider()["dcline"])

        # substation
        self.sub, _ = frame("sub", mpc.sub, mpc.subid)

        # bus to sub mapping
        self.bus2sub, _ = frame("bus2sub", mpc.bus2sub, mpc.busid)

        # zone
        zone = pd.DataFrame(mpc.zone, columns=["zone_id", "zone_name"])
        self.zone2id = link(zone.zone_name.values, zone.zone_id.values)
        self.id2zone = link(zone.zone_id.values, zone.zone_name.values)

        # storage
        if n_storage > 0:
            self.storage["gen"] = plant_storage.astype({"bus_id": int})
            self.storage["gencost"] = gencost_storage
            col_name = self.storage["StorageData"].columns
            for c in col_name:
                if n_storage > 1:
                    self.storage["StorageData"][c] = getattr(
                        data["mdi"].Storage, c)
                else:
                    self.storage["StorageData"][c] = [
                        getattr(data["mdi"].Storage, c)
                    ]
            self.storage["genfuel"] = mpc.genfuel[len(mpc.genid):]

        # interconnect
        self.interconnect = self.sub.interconnect.unique().tolist()

        # model immutables
        if self.grid_model in ["usa_tamu", "hifld"]:
            self.model_immutables = ModelImmutables(
                self.grid_model, interconnect=self.interconnect)
def plot_bar_generation_max_min_actual(
    scenario,
    interconnect,
    gen_type,
    percentage=False,
    show_as_state=True,
    fontsize=15,
    plot_show=True,
):
    """Generate for a given scenario the bar plot of total capacity vs. actual
    generation vs. minimum generation for a specific type of generators in an
    interconnection.

    :param powersimdata.scenario.scenario.Scenario scenario: scenario instance.
    :param str interconnect: the interconnection name of interest.
    :param str gen_type: type of generator.
    :param bool percentage: show bars in percentage of total capacity or not,
        defaults to False.
    :param bool show_as_state: each bar represents a state within the given
        interconnection or not, defaults to True, if not, each bar will represent a
        loadzone instead.
    :param int/float fontsize: font size of the texts shown on the plot.
    :param plot_show: show the plot or not, defaults to True.
    :return: (*matplotlib.axes.Axes*) -- axes object of the plot.
    """
    grid = scenario.state.get_grid()
    plant = grid.plant[grid.plant.type == gen_type]
    mi = ModelImmutables(scenario.info["grid_model"])
    hour_num = (pd.Timestamp(scenario.info["end_date"]) - pd.Timestamp(
        scenario.info["start_date"])).total_seconds() / 3600 + 1
    if show_as_state:
        zone_list = [
            mi.zones["abv2state"][abv]
            for abv in mi.zones["interconnect2abv"][interconnect]
        ]
        all_max_min = (
            plant.groupby(plant.zone_name.map(
                mi.zones["loadzone2state"]))[["Pmax", "Pmin"]].sum() *
            hour_num)
        all_actual_gen = sum_generation_by_state(scenario)[gen_type] * 1000
    else:
        zone_list = mi.zones["interconnect2loadzone"][interconnect]
        all_max_min = plant.groupby("zone_name")[["Pmax", "Pmin"
                                                  ]].sum() * hour_num
        all_actual_gen = (sum_generation_by_type_zone(scenario).rename(
            columns=grid.id2zone).T[gen_type])
    df = pd.concat([all_max_min, all_actual_gen], axis=1).loc[zone_list]
    if percentage:
        df_pct = df.copy()
        df_pct["Pmax"] = df["Pmax"].apply(lambda x: 1 if x > 0 else 0)
        df_pct["Pmin"] = df["Pmin"] / df["Pmax"]
        df_pct[gen_type] = df[gen_type] / df["Pmax"]
        df = df_pct
    df.sort_index(inplace=True)

    width = 0.8
    fig, ax = plt.subplots(figsize=[30, 15])
    df["Pmax"].plot(kind="bar", width=width, color="yellowgreen", ax=ax)
    df[gen_type].plot(kind="bar", width=width * 0.7, color="steelblue", ax=ax)
    df["Pmin"].plot(kind="bar", width=width * 0.4, color="firebrick", ax=ax)
    ax.legend(
        ["Total Capacity", "Actual Generation", "Minimum Generation"],
        bbox_to_anchor=(0.5, 0.95),
        loc="lower center",
        ncol=3,
        fontsize=fontsize,
    )
    ax.grid()
    if percentage:
        ax.set_title(f"{gen_type.capitalize()} Generation %")
    else:
        ax.set_title(f"{gen_type.capitalize()} Generation MWh")
    for item in ([ax.title, ax.xaxis.label, ax.yaxis.label] +
                 ax.get_xticklabels() + ax.get_yticklabels()):
        item.set_fontsize(fontsize)
    if plot_show:
        plt.show()
    return ax
Beispiel #21
0
def plot_bar_generation_stack(
    areas,
    scenario_ids,
    resources,
    area_types=None,
    scenario_names=None,
    curtailment_split=True,
    t2c=None,
    t2l=None,
    t2hc=None,
    titles=None,
    plot_show=True,
    save=False,
    filenames=None,
    filepath=None,
):
    """Plot any number of scenarios as generation stack bar for selected resources in
    each specified areas.

    :param list/str areas: list of area(s), each area is one of *loadzone*, *state*,
        *state abbreviation*, *interconnect*, *'all'*.
    :param int/list/str scenario_ids: list of scenario id(s), defaults to None.
    :param str/list resources: one or a list of resources. *'curtailment'*,
        *'solar_curtailment'*, *'wind_curtailment'*, *'wind_offshore_curtailment'*
        are valid entries together with all available generator types in the area(s).
        The order of the resources determines the stack order in the figure.
    :param list/str area_types: list of area_type(s), each area_type is one of
        *'loadzone'*, *'state'*, *'state_abbr'*, *'interconnect'*, defaults to None.
    :param list/str scenario_names: list of scenario name(s) of same len as scenario
        ids, defaults to None.
    :param bool curtailment_split: if curtailments are split into different
        categories, defaults to True.
    :param dict t2c: user specified color of resource type to overwrite pre-defined ones
        key: resource type, value: color code.
    :param dict t2l: user specified label of resource type to overwrite pre-defined ones
        key: resource type, value: label.
    :param dict t2hc: user specified color of curtailable resource hatches to overwrite
        pre-defined ones. key: resource type, valid keys are *'curtailment'*,
        *'solar_curtailment'*, *'wind_curtailment'*, *'wind_offshore_curtailment'*,
        value: color code.
    :param dict titles: user specified figure titles, key: area, value: new figure
        title in string, use area as title if None.
    :param bool plot_show: display the generated figure or not, defaults to True.
    :param bool save: save the generated figure or not, defaults to False.
    :param dict filenames: user specified filenames, key: area, value: new filename
        in string, use area as filename if None.
    :param str filepath: if save is True, user specified filepath, use current
        directory if None.
    :return: (*list*) -- matplotlib.axes.Axes object of each plot in a list.
    :raises TypeError:
        if resources is not a list/str and/or
        if titles is provided but not in a dictionary format and/or
        if filenames is provided but not in a dictionary format.
    :raises ValueError:
        if length of area_types and areas is different and/or
        if length of scenario_names and scenario_ids is different.
    """
    if isinstance(areas, str):
        areas = [areas]
    if isinstance(scenario_ids, (int, str)):
        scenario_ids = [scenario_ids]
    if not isinstance(scenario_ids, list):
        raise TypeError("ERROR: scenario_ids should be a int/str/list")
    if isinstance(resources, str):
        resources = [resources]
    if not isinstance(resources, list):
        raise TypeError("ERROR: resources should be a list/str")
    if isinstance(area_types, str):
        area_types = [area_types]
    if not area_types:
        area_types = [None] * len(areas)
    if len(areas) != len(area_types):
        raise ValueError(
            "ERROR: if area_types are provided, number of area_types must match number of areas"
        )
    if isinstance(scenario_names, str):
        scenario_names = [scenario_names]
    if scenario_names and len(scenario_names) != len(scenario_ids):
        raise ValueError(
            "ERROR: if scenario names are provided, number of scenario names must match number of scenario ids"
        )
    if titles is not None and not isinstance(titles, dict):
        raise TypeError("ERROR: titles should be a dictionary if provided")
    if filenames is not None and not isinstance(filenames, dict):
        raise TypeError("ERROR: filenames should be a dictionary if provided")
    s_list = []
    for sid in scenario_ids:
        s_list.append(Scenario(sid))
    mi = ModelImmutables(s_list[0].info["grid_model"])
    type2color = mi.plants["type2color"]
    type2label = mi.plants["type2label"]
    type2hatchcolor = mi.plants["type2hatchcolor"]
    if t2c:
        type2color.update(t2c)
    if t2l:
        type2label.update(t2l)
    if t2hc:
        type2hatchcolor.update(t2hc)
    all_loadzone_data = dict()
    for sid, scenario in zip(scenario_ids, s_list):
        curtailment = calculate_curtailment_time_series_by_areas_and_resources(
            scenario, areas={"loadzone": mi.zones["loadzone"]})
        for area in curtailment:
            for r in curtailment[area]:
                curtailment[area][r] = curtailment[area][r].sum().sum()
        curtailment = (pd.DataFrame(curtailment).rename(
            columns=mi.zones["loadzone2id"]).T)
        curtailment.rename(
            columns={c: c + "_curtailment"
                     for c in curtailment.columns},
            inplace=True)
        curtailment["curtailment"] = curtailment.sum(axis=1)
        all_loadzone_data[sid] = pd.concat(
            [
                sum_generation_by_type_zone(scenario).T,
                scenario.state.get_demand().sum().T.rename("load"),
                curtailment,
            ],
            axis=1,
        ).rename(index=mi.zones["id2loadzone"])

    width = 0.4
    x_scale = 0.6
    ax_list = []
    for area, area_type in zip(areas, area_types):
        fig, ax = plt.subplots(figsize=(10, 8))
        for ind, s in enumerate(s_list):
            patches = []
            fuels = []
            bottom = 0
            loadzone_set = area_to_loadzone(s.info["grid_model"], area,
                                            area_type)
            data = (all_loadzone_data[scenario_ids[ind]].loc[loadzone_set].sum(
            ).divide(1e6).astype("float").round(2))
            for i, f in enumerate(resources[::-1]):
                if f == "load":
                    continue
                if curtailment_split and f == "curtailment":
                    continue
                if not curtailment_split and f in {
                        "wind_curtailment",
                        "solar_curtailment",
                        "wind_offshore_curtailment",
                }:
                    continue
                fuels.append(f)
                if "curtailment" in f:
                    patches.append(
                        ax.bar(
                            ind * x_scale,
                            data[f],
                            width,
                            bottom=bottom,
                            color=type2color.get(f, "red"),
                            hatch="//",
                            edgecolor=type2hatchcolor.get(f, "black"),
                            lw=0,
                        ))
                else:
                    patches.append(
                        ax.bar(
                            ind * x_scale,
                            data[f],
                            width,
                            bottom=bottom,
                            color=type2color[f],
                        ))
                bottom += data[f]

            # plot load line
            xs = [ind * x_scale - 0.5 * width, ind * x_scale + 0.5 * width]
            ys = [data["load"]] * 2
            line_patch = ax.plot(xs, ys, "--", color="black")

        if scenario_names:
            labels = scenario_names
        else:
            labels = [s.info["name'"] for s in s_list]
        ax.set_xticks([i * x_scale for i in range(len(s_list))])
        ax.set_xticklabels(labels, fontsize=12)
        ax.set_ylabel("TWh", fontsize=12)
        bar_legend = ax.legend(
            handles=patches[::-1] + line_patch,
            labels=[type2label.get(f, f.capitalize())
                    for f in fuels[::-1]] + ["Demand"],
            fontsize=12,
            bbox_to_anchor=(1, 1),
            loc="upper left",
        )
        ax.add_artist(bar_legend)
        ax.set_axisbelow(True)
        ax.grid(axis="y")
        if titles is not None and area in titles:
            ax.set_title(titles[area])
        else:
            ax.set_title(area)
        fig.tight_layout()
        ax_list.append(ax)
        if plot_show:
            plt.show()
        if save:
            if filenames is not None and area in filenames:
                filename = filenames[area]
            else:
                filename = area
            if not filepath:
                filepath = os.getcwd()
            fig.savefig(
                f"{os.path.join(filepath, filename)}.pdf",
                bbox_inches="tight",
                pad_inches=0,
            )
    return ax_list
Beispiel #22
0
    def _generate_model_immutables(self):
        """Generate the model immutables"""
        mapping = ModelImmutables(self.grid_model,
                                  interconnect=self.interconnect)

        # loadzone
        mapping.zones["loadzone"] = set(self.zone2id)
        mapping.zones["id2loadzone"] = self.id2zone
        mapping.zones["loadzone2id"] = self.zone2id
        mapping.zones["loadzone2abv"] = self.network.buses["country"].to_dict()
        mapping.zones["loadzone2country"] = (
            self.network.buses["country"].map(abv2country).to_dict())
        mapping.zones["loadzone2interconnect"] = {
            l: mapping.zones["abv2interconnect"][a]
            for l, a in mapping.zones["loadzone2abv"].items()
        }
        mapping.zones["id2timezone"] = {
            self.zone2id[l]: abv2timezone[a]
            for l, a in mapping.zones["loadzone2abv"].items()
        }
        mapping.zones["timezone2id"] = {
            t: i
            for i, t in mapping.zones["id2timezone"].items()
        }

        # country
        mapping.zones["country2loadzone"] = {
            abv2country[a]: set(l)
            for a, l in self.network.buses.groupby("country").groups.items()
        }
        mapping.zones["abv2loadzone"] = {
            a: set(l)
            for a, l in self.network.buses.groupby("country").groups.items()
        }
        mapping.zones["abv2id"] = {
            a: {self.zone2id[l]
                for l in l_in_country}
            for a, l_in_country in mapping.zones["abv2loadzone"].items()
        }
        mapping.zones["id2abv"] = {
            i: mapping.zones["loadzone2abv"][l]
            for i, l in self.id2zone.items()
        }

        # interconnect
        mapping.zones["interconnect2loadzone"] = {
            i: set().union(*(mapping.zones["abv2loadzone"][a]
                             for a in a_in_interconnect))
            for i, a_in_interconnect in
            mapping.zones["interconnect2abv"].items()
        }
        mapping.zones["interconnect2id"] = {
            i: set().union(*({self.zone2id[l]} for l in l_in_interconnect))
            for i, l_in_interconnect in
            mapping.zones["interconnect2loadzone"].items()
        }

        return mapping
Beispiel #23
0
def plot_bar_generation_vs_capacity(
    areas,
    area_types=None,
    scenario_ids=None,
    scenario_names=None,
    time_range=None,
    time_zone=None,
    custom_data=None,
    resource_types=None,
    resource_labels=None,
    horizontal=False,
):
    """Plot any number of scenarios as bar or horizontal bar charts with two columns per
    scenario - generation and capacity.

    :param list/str areas: list of area(s), each area is one of *loadzone*, *state*,
        *state abbreviation*, *interconnect*, *'all'*.
    :param list/str area_types: list of area_type(s), each area_type is one of
        *'loadzone'*, *'state'*, *'state_abbr'*, *'interconnect'*, defaults to None.
    :param int/list/str scenario_ids: list of scenario id(s), defaults to None.
    :param list/str scenario_names: list of scenario name(s) of same len as scenario
        ids, defaults to None.
    :param tuple time_range: [start_timestamp, end_timestamp] where each time stamp
        is pandas.Timestamp/numpy.datetime64/datetime.datetime. If None, the entire
        time range is used for the given scenario.
    :param str time_zone: new time zone, defaults to None, which uses UTC.
    :param list custom_data: list of dictionaries with each element being
        hand-generated data as returned by :func:`make_gen_cap_custom_data`, defaults
        to None.
    :param list/str resource_types: list of resource type(s) to show, defaults to None,
        which shows all available resources in the area of the corresponding scenario.
    :param dict resource_labels: a dictionary with keys being resource_types and values
        being labels to show in the plots, defaults to None, which uses
        resource_types as labels.
    :param bool horizontal: display bars horizontally, default to False.
    """
    if isinstance(areas, str):
        areas = [areas]
    if isinstance(area_types, str):
        area_types = [area_types]
    if not area_types:
        area_types = [None] * len(areas)
    if len(areas) != len(area_types):
        raise ValueError(
            "ERROR: if area_types are provided, it should have the same number of entries with areas."
        )

    if not scenario_ids:
        scenario_ids = []
    if isinstance(scenario_ids, (int, str)):
        scenario_ids = [scenario_ids]
    if isinstance(scenario_names, str):
        scenario_names = [scenario_names]
    if scenario_names and len(scenario_names) != len(scenario_ids):
        raise ValueError(
            "ERROR: if scenario names are provided, number of scenario names must match number of scenario ids"
        )
    if not custom_data:
        custom_data = {}
    if len(scenario_ids) + len(custom_data) <= 1:
        raise ValueError(
            "ERROR: must include at least two scenario ids and/or custom data")
    if isinstance(resource_types, str):
        resource_types = [resource_types]
    if not resource_labels:
        resource_labels = dict()
    if not isinstance(resource_labels, dict):
        raise TypeError("ERROR: resource_labels should be a dictionary")

    all_loadzone_data = {}
    scenario_data = {}
    for i, sid in enumerate(scenario_ids):
        scenario = Scenario(sid)
        mi = ModelImmutables(scenario.info["grid_model"])
        all_loadzone_data[sid] = {
            "gen":
            sum_generation_by_type_zone(
                scenario, time_range,
                time_zone).rename(columns=mi.zones["id2loadzone"]),
            "cap":
            sum_capacity_by_type_zone(scenario).rename(
                columns=mi.zones["id2loadzone"]),
        }
        scenario_data[sid] = {
            "name":
            scenario_names[i] if scenario_names else scenario.info["name"],
            "grid_model": mi.model,
            "gen": {
                "label": "Generation",
                "unit": "TWh",
                "data": {}
            },
            "cap": {
                "label": "Capacity",
                "unit": "GW",
                "data": {}
            },
        }
    for area, area_type in zip(areas, area_types):
        for sid in scenario_ids:
            loadzone_set = area_to_loadzone(scenario_data[sid]["grid_model"],
                                            area, area_type)
            scenario_data[sid]["gen"]["data"][area] = (
                all_loadzone_data[sid]["gen"][loadzone_set].sum(
                    axis=1).divide(1e6).astype("float").round(2).to_dict())
            scenario_data[sid]["cap"]["data"][area] = (
                all_loadzone_data[sid]["cap"][loadzone_set].sum(
                    axis=1).divide(1e3).astype("float").round(2).to_dict())
    for c_data in custom_data:
        scenario_data[c_data["name"]] = c_data

    for area in areas:
        if not resource_types:
            area_resource_types = sorted(
                set(r for sd in scenario_data.values()
                    for side in ["gen", "cap"]
                    for r, v in sd[side]["data"][area].items() if v > 0))
        else:
            area_resource_types = resource_types

        ax_data_list = []
        for side in ["gen", "cap"]:
            ax_data = {}
            for sd in scenario_data.values():
                # If we don't have data for a resource type, set it to 0
                ax_data[sd["name"]] = [
                    sd[side]["data"][area].get(r, 0)
                    for r in area_resource_types
                ]
            ax_data_list.append({
                "title":
                f"""{sd[side]["label"]} ({sd[side]["unit"]})""",
                "labels":
                [resource_labels.get(r, r) for r in area_resource_types],
                "values":
                ax_data,
                "unit":
                sd[side]["unit"],
            })

        if horizontal:
            _construct_hbar_visuals(area, ax_data_list)
        else:
            _construct_bar_visuals(area, ax_data_list)
Beispiel #24
0
def summarize_hist_gen(
    hist_gen_raw: pd.DataFrame, all_resources: list, grid_model="usa_tamu"
) -> pd.DataFrame:
    """Get the total historical generation for each generator type and state
    combination, adding totals for interconnects and for all states.

    :param pandas.DataFrame hist_gen_raw: historical generation data frame. Columns
        are resources and indices are either state or load zone.
    :param list all_resources: list of resources.
    :param str grid_model: grid_model
    :return: (*pandas.DataFrame*) -- historical generation per resource.
    """
    _check_data_frame(hist_gen_raw, "PG")
    filtered_colnames = _check_resources_and_format(
        all_resources, grid_model=grid_model
    )
    mi = ModelImmutables(grid_model)

    result = hist_gen_raw.copy()

    # Interconnection
    eastern_areas = (
        set([mi.zones["abv2state"][s] for s in mi.zones["interconnect2abv"]["Eastern"]])
        | mi.zones["interconnect2loadzone"]["Eastern"]
    )
    eastern = result.loc[result.index.isin(eastern_areas)].sum()

    ercot_areas = mi.zones["interconnect2loadzone"]["Texas"]
    ercot = result.loc[result.index.isin(ercot_areas)].sum()

    western_areas = (
        set([mi.zones["abv2state"][s] for s in mi.zones["interconnect2abv"]["Western"]])
        | mi.zones["interconnect2loadzone"]["Western"]
    )
    western = result.loc[result.index.isin(western_areas)].sum()

    # State
    def _groupby_state(index: str) -> str:
        """Use state as a dict key if index is a smaller region (e.g. Texas East),
        otherwise use the given index.

        :param str index: either a state name or region within a state.
        :return: (*str*) -- the corresponding state name.
        """
        return (
            mi.zones["loadzone2state"][index]
            if index in mi.zones["loadzone2state"]
            else index
        )

    result = result.groupby(by=_groupby_state).aggregate(np.sum)

    # Summary
    all = result.sum()

    result.loc["Eastern interconnection"] = eastern
    result.loc["Western interconnection"] = western
    result.loc["Texas interconnection"] = ercot
    result.loc["All"] = all

    result = result.loc[:, filtered_colnames]
    result.rename(columns=mi.plants["type2label"], inplace=True)

    return result