def test_area_to_loadzone_argument_type():
    with pytest.raises(TypeError, match="area must be a str"):
        area_to_loadzone("europe_tub", 3)

    with pytest.raises(TypeError,
                       match="area_type must be either None or str"):
        area_to_loadzone("europe_tub", "all", area_type=["interconnect"])
Example #2
0
def _make_zonename2target(grid, targets):
    """Creates a dictionary of {zone_name: target_name} pairs.

    :param powersimdata.input.grid.Grid grid: Grid instance defining the set of zones.
    :param pandas.DataFrame targets: a dataframe used to look up constituent zones.
    :return: (*dict*) -- a dictionary of {zone_name: target_name} pairs.
    :raises ValueError: if a zone is not present in any target areas, or
        if a zone is present in more than one target area.
    """
    grid_model = grid.get_grid_model()
    target_zones = {
        target_name: area_to_loadzone(grid_model, target_name) if pd.isnull(
            targets.loc[target_name, "area_type"]) else area_to_loadzone(
                grid_model, target_name, targets.loc[target_name, "area_type"])
        for target_name in targets.index.tolist()
    }
    # Check for any collisions
    zone_sets = target_zones.values()
    if len(set.union(*zone_sets)) != sum([len(t) for t in zone_sets]):
        zone_sets_list = [zone for _set in zone_sets for zone in _set]
        duplicates = {
            zone
            for zone in zone_sets_list if zone_sets_list.count(zone) > 1
        }
        error_areas = {
            zone: {
                area
                for area, zone_set in target_zones.items() if zone in zone_set
            }
            for zone in duplicates
        }
        error_msgs = [
            f"{k} within: {', '.join(v)}" for k, v in error_areas.items()
        ]
        raise ValueError(
            f"Zone(s) within multiple area! {'; '.join(error_msgs)}")
    zonename2target = {}
    for target_name, zone_set in target_zones.items():
        # Filter out parts of states not in the interconnect(s) in this Grid
        filtered_zone_set = zone_set & set(grid.zone2id.keys())
        zonename2target.update(
            {zone: target_name
             for zone in filtered_zone_set})
    untargetted_zones = set(grid.zone2id.keys()) - set(zonename2target.keys())
    if len(untargetted_zones) > 0:
        err_msg = f"Targets do not cover all load zones. Missing: {untargetted_zones}"
        raise ValueError(err_msg)
    return zonename2target
Example #3
0
def get_plant_id_for_resources_in_area(scenario,
                                       area,
                                       resources,
                                       area_type=None):
    """Get the list of plant ids of certain resources in the specific 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'*
    :return: (*list*) -- list of plant id
    """
    resource_set = set([resources]) if isinstance(resources,
                                                  str) else set(resources)
    grid = scenario.state.get_grid()
    loadzone_set = area_to_loadzone(scenario.info["grid_model"],
                                    area,
                                    area_type=area_type)
    plant_id = grid.plant[
        (grid.plant["zone_name"].isin(loadzone_set))
        & (grid.plant["type"].isin(resource_set))].index.tolist()

    return plant_id
Example #4
0
    def area_to_loadzone(self, area, area_type=None):
        """Map the query area to a list of loadzones. For more info, see
            :func:`powersimdata.network.model.area_to_loadzone`.

        :param str area: one of: *loadzone*, *state*, *state abbreviation*,
            *interconnect*, *'all'*
        :param str area_type: one of: *'loadzone'*, *'state'*,
            *'state_abbr'*, *'interconnect'*
        :return: (*set*) -- set of loadzones associated to the query area
        """
        return area_to_loadzone(self.grid_model, area, area_type)
def test_area_to_loadzone():
    assert area_to_loadzone("usa_tamu", "El Paso") == {"El Paso"}
    assert area_to_loadzone("usa_tamu", "Texas",
                            area_type="state") == area_to_loadzone(
                                "usa_tamu", "Texas")
    assert area_to_loadzone("usa_tamu", "Texas", area_type="state") == {
        "East Texas",
        "South Central",
        "Far West",
        "North Central",
        "West",
        "North",
        "Texas Panhandle",
        "South",
        "East",
        "Coast",
        "El Paso",
    }

    assert area_to_loadzone("usa_tamu", "Texas", area_type="interconnect") == {
        "South Central",
        "Far West",
        "North Central",
        "West",
        "North",
        "South",
        "East",
        "Coast",
    }
    assert area_to_loadzone("usa_tamu",
                            "MT") == {"Montana Eastern", "Montana Western"}
def test_area_to_loadzone_argument_value():
    with pytest.raises(ValueError):
        area_to_loadzone("usa_tamu", "all", area_type="province")

    with pytest.raises(ValueError,
                       match="Invalid area / area_type combination"):
        area_to_loadzone("usa_tamu", "California", area_type="loadzone")

    with pytest.raises(ValueError,
                       match="Invalid area / area_type combination"):
        area_to_loadzone("usa_tamu", "WA", area_type="interconnect")

    with pytest.raises(ValueError,
                       match="Invalid area / area_type combination"):
        area_to_loadzone("europe_tub", "France", area_type="country_abbr")
Example #7
0
def get_branches_by_area(grid, area_names, method="either"):
    """Given a set of area names, select branches which are in one or more of
    these areas.

    :param powersimdata.input.grid.Grid grid: Grid to query for topology.
    :param list/set/tuple area_names: an iterable of area names, used to look
        up zone names via :func:`powersimdata.network.model.area_to_loadzone`.
    :param str method: whether to include branches which span zones. Options:
        - 'internal': only select branches which are to/from the same area.
        - 'bridging': only select branches which connect area to another.
        - 'either': select branches if either end is in area. Equivalent to
        'internal' + 'bridging'.
    :raise TypeError: if area_names not a list/set/tuple, or method not a str.
    :raise ValueError: if not all elements of area_names are strings, if method
        is not one of the recognized methods.
    :return: (*set*) -- a set of branch IDs.
    """
    allowed_methods = {"internal", "bridging", "either"}
    if not isinstance(grid, Grid):
        raise TypeError("grid must be a Grid object")
    if not isinstance(area_names, (list, set, tuple)):
        raise TypeError("area_names must be list, set, or tuple")
    if not all([isinstance(a, str) for a in area_names]):
        raise ValueError("each value in area_names must be a str")
    if not isinstance(method, str):
        raise TypeError("method must be a str")
    if method not in allowed_methods:
        raise ValueError("valid methods are: " + " | ".join(allowed_methods))

    branch = grid.branch
    selected_branches = set()
    grid_model = grid.get_grid_model()
    for a in area_names:
        load_zone_names = area_to_loadzone(grid_model, a)
        to_bus_in_area = branch.to_zone_name.isin(load_zone_names)
        from_bus_in_area = branch.from_zone_name.isin(load_zone_names)
        if method in ("internal", "either"):
            internal_branches = branch[to_bus_in_area & from_bus_in_area].index
            selected_branches |= set(internal_branches)
        if method in ("bridging", "either"):
            bridging_branches = branch[to_bus_in_area ^ from_bus_in_area].index
            selected_branches |= set(bridging_branches)

    return selected_branches
Example #8
0
def get_demand_time_series(scenario, area, area_type=None):
    """Get time series demand in 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 area_type: one of *'loadzone'*, *'state'*, *'state_abbr'*,
        *'interconnect'*
    :return: (*pandas.Series*) -- time series of total demand, index: time stamps,
        column: demand values
    """
    grid = scenario.state.get_grid()
    loadzone_set = area_to_loadzone(scenario.info["grid_model"],
                                    area,
                                    area_type=area_type)
    loadzone_id_set = {
        grid.zone2id[lz]
        for lz in loadzone_set if lz in grid.zone2id
    }

    return scenario.state.get_demand()[loadzone_id_set].sum(axis=1)
Example #9
0
def get_storage_id_in_area(scenario, area, area_type=None):
    """Get the list of storage ids in the specific 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 area_type: one of *'loadzone'*, *'state'*, *'state_abbr'*,
        *'interconnect'*
    :return: (*list*) -- list of storage id
    """
    grid = scenario.state.get_grid()
    loadzone_set = area_to_loadzone(
        scenario.info["grid_model"], area, area_type=area_type
    )
    loadzone_id_set = {grid.zone2id[lz] for lz in loadzone_set if lz in grid.zone2id}

    gen = grid.storage["gen"]
    storage_id = gen.loc[
        gen["bus_id"].apply(lambda x: grid.bus.loc[x, "zone_id"]).isin(loadzone_id_set)
    ].index.tolist()

    return storage_id
Example #10
0
def create_change_table(input_targets, ref_scenario):
    """Using a reference scenario, create a change table which scales all
    plants in a base grid to capacities matching the reference grid, with
    the exception of wind and solar plants which are scaled up according to
    the clean capacity scaling logic.

    :param pandas.DataFrame input_targets: table of targets, with previous and
        next capacities.
    :param powersimdata.scenario.scenario.Scenario ref_scenario: reference scenario
        to mimic.
    :return: (*dict*) -- dictionary to be passed to a change table.
    """
    epsilon = 1e-3
    interconnect = ref_scenario.info["interconnect"]
    base_grid = Grid([interconnect])
    grid_zones = base_grid.plant.zone_name.unique()
    ref_grid = ref_scenario.state.get_grid()
    ct = mimic_generation_capacity(base_grid, ref_grid)
    for region in input_targets.index:
        prev_solar = input_targets.loc[region, "solar.prev_capacity"]
        prev_wind = input_targets.loc[region, "wind.prev_capacity"]
        next_solar = input_targets.loc[region, "solar.next_capacity"]
        next_wind = input_targets.loc[region, "wind.next_capacity"]
        zone_names = area_to_loadzone(ref_scenario.info["grid_model"], region)
        zone_ids = [
            base_grid.zone2id[n] for n in zone_names if n in grid_zones
        ]
        if prev_solar > 0:
            scale = next_solar / prev_solar
            if abs(scale - 1) > epsilon:
                for id in zone_ids:
                    _apply_zone_scale_factor_to_ct(ct, "solar", id, scale)
        if prev_wind > 0:
            scale = next_wind / prev_wind
            if abs(scale - 1) > epsilon:
                for id in zone_ids:
                    _apply_zone_scale_factor_to_ct(ct, "wind", id, scale)
    return ct
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)
Example #12
0
def plot_capacity_vs_price(
    grid, num_segments, area, gen_type, area_type=None, plot=True
):
    """Plots the generator capacity vs. the generator price for a specified area
        and generation type.

    :param powersimdata.input.grid.Grid grid: Grid object.
    :param int num_segments: The number of segments into which the piecewise linear
        cost curve is split.
    :param str area: Either the load zone, state name, state abbreviation, or
        interconnect.
    :param str gen_type: Generation type.
    :param str area_type: one of: *'loadzone'*, *'state'*, *'state_abbr'*,
        *'interconnect'*. Defaults to None, which allows
        :func:`powersimdata.network.model.area_to_loadzone` to infer the type.
    :param bool plot: If True, the supply curve plot is shown. If False, the plot is
        not shown.
    :return: (*None*) -- The capacity vs. price plot is displayed according to the user.
    :raises TypeError: if a powersimdata.input.grid.Grid object is not input.
    :raises ValueError: if the specified area or generator type is not applicable.
    """

    plt = _check_import("matplotlib.pyplot")

    # Check that a Grid object is input
    if not isinstance(grid, Grid):
        raise TypeError("A Grid object must be input.")

    # Check that the desired number of linearized cost curve segments is an int
    if not isinstance(num_segments, int):
        raise TypeError(
            "The number of linearized cost curve segments must be input as an int."
        )

    # Obtain the desired generator cost and plant information data
    data = get_supply_data(grid, num_segments)

    # Check the input supply data
    check_supply_data(data, num_segments)

    # Check to make sure the generator type is valid
    if gen_type not in data["type"].unique():
        raise ValueError(f"{gen_type} is not a valid generation type.")

    # Identify the load zones that correspond to the specified area and area_type
    returned_zones = area_to_loadzone(grid.get_grid_model(), area, area_type)

    # Trim the DataFrame to only be of the desired area and generation type
    data = data.loc[data.zone_name.isin(returned_zones)]
    data = data.loc[data["type"] == gen_type]

    # Remove generators that have no capacity (e.g., Maine coal generators)
    if data["slope1"].isnull().values.any():
        data.dropna(subset=["slope1"], inplace=True)

    # Check if the area contains generators of the specified type
    if data.empty:
        return

    # Combine the p_diff and slope information for each cost segment
    df_cols = []
    for i in range(num_segments):
        df_cols.append(data.loc[:, ("p_diff" + str(i + 1), "slope" + str(i + 1))])
        df_cols[i].rename(
            columns={"p_diff" + str(i + 1): "p_diff", "slope" + str(i + 1): "slope"},
            inplace=True,
        )
    df = pd.concat(df_cols, axis=0)
    df = df.reset_index(drop=True)

    # Determine the average
    total_cap = df["p_diff"].sum()
    if total_cap == 0:
        data_avg = 0
    else:
        data_avg = (df["slope"] * df["p_diff"]).sum() / total_cap

    # Plot the comparison
    if plot:
        ax = df.plot.scatter(
            x="p_diff", y="slope", s=50, figsize=[20, 10], grid=True, fontsize=20
        )
        plt.title(
            f"Capacity vs. Price for {gen_type} generators in {area}", fontsize=20
        )
        plt.xlabel("Segment Capacity (MW)", fontsize=20)
        plt.ylabel("Segment Price ($/MW)", fontsize=20)
        ax.plot(df["p_diff"], [data_avg] * len(df.index), c="red")
        plt.show()
Example #13
0
def plot_c1_vs_c2(
    grid,
    area,
    gen_type,
    area_type=None,
    plot=True,
    zoom=False,
    num_sd=3,
    alpha=0.1,
):
    """Compares the c1 and c2 parameters from the quadratic generator cost curves.

    :param powersimdata.input.grid.Grid grid: Grid object.
    :param str area: Either the load zone, state name, state abbreviation, or
        interconnect.
    :param str gen_type: Generation type.
    :param str area_type: one of: *'loadzone'*, *'state'*, *'state_abbr'*,
        *'interconnect'*. Defaults to None, which allows
        :func:`powersimdata.network.model.area_to_loadzone` to infer the type.
    :param bool plot: If True, the c1 vs. c2 plot is shown. If False, the plot is not
        shown.
    :param bool zoom: If True, filters out c2 outliers to enable better visualization.
        If False, there is no filtering.
    :param float/int num_sd: The number of standard deviations used to filter out c2
        outliers.
    :param float alpha: The alpha blending value for the scatter plot; takes values
        between 0 (transparent) and 1 (opaque).
    :return: (*None*) -- The c1 vs. c2 plot is displayed according to the user.
    :raises TypeError: if a powersimdata.input.grid.Grid object is not input.
    :raises ValueError: if the specified area or generator type is not applicable.
    """

    plt = _check_import("matplotlib.pyplot")

    # Check that a Grid object is input
    if not isinstance(grid, Grid):
        raise TypeError("A Grid object must be input.")

    # Obtain a copy of the Grid object
    grid = copy.deepcopy(grid)

    # Access the generator cost and plant information data
    gencost_df = grid.gencost["before"]
    plant_df = grid.plant

    # Create a new DataFrame with the desired columns
    data = pd.concat(
        [
            plant_df[["type", "interconnect", "zone_name", "Pmin", "Pmax"]],
            gencost_df[
                gencost_df.columns.difference(
                    ["type", "startup", "shutdown", "n", "interconnect"], sort=False
                )
            ],
        ],
        axis=1,
    )

    # Check to make sure the generator type is valid
    if gen_type not in data["type"].unique():
        raise ValueError(f"{gen_type} is not a valid generation type.")

    # Identify the load zones that correspond to the specified area and area_type
    returned_zones = area_to_loadzone(grid.get_grid_model(), area, area_type)

    # Trim the DataFrame to only be of the desired area and generation type
    data = data.loc[data.zone_name.isin(returned_zones)]
    data = data.loc[data["type"] == gen_type]

    # Remove generators that have no capacity (e.g., Maine coal generators)
    data = data[data["Pmin"] != data["Pmax"]]

    # Check if the area contains generators of the specified type
    if data.empty:
        return

    # Filters out large c2 outlier values so the overall trend can be better visualized
    zoom_name = ""
    if zoom:
        # Drop values outside a specified number of standard deviations of c2
        sd_c2 = np.std(data["c2"])
        mean_c2 = np.mean(data["c2"])
        cutoff = mean_c2 + num_sd * sd_c2
        if len(data[data["c2"] > cutoff]) > 0:
            zoom = True
            data = data[data["c2"] <= cutoff]
            max_ylim = np.max(data["c2"] + 0.01)
            min_ylim = np.min(data["c2"] - 0.01)
            max_xlim = np.max(data["c1"] + 1)
            min_xlim = np.min(data["c1"] - 1)
            zoom_name = "(zoomed)"
        else:
            zoom = False

    # Plot the c1 vs. c2 comparison
    if plot:
        fig, ax = plt.subplots()
        fig.set_size_inches(20, 10)
        plt.scatter(
            data["c1"],
            data["c2"],
            s=np.sqrt(data["Pmax"]) * 10,
            alpha=alpha,
            c=data["Pmax"],
            cmap="plasma",
        )
        plt.grid()
        plt.title(
            f"c1 vs. c2 for {gen_type} generators in {area} {zoom_name}", fontsize=20
        )
        if zoom:
            plt.ylim([min_ylim, max_ylim])
            plt.xlim([min_xlim, max_xlim])
        plt.xlabel("c1", fontsize=20)
        plt.ylabel("c2", fontsize=20)
        plt.xticks(fontsize=20)
        plt.yticks(fontsize=20)
        cbar = plt.colorbar()
        cbar.set_label("Capacity (MW)", fontsize=20)
        cbar.ax.tick_params(labelsize=20)
        plt.show()
Example #14
0
def build_supply_curve(grid, num_segments, area, gen_type, area_type=None, plot=True):
    """Builds a supply curve for a specified area and generation type.

    :param powersimdata.input.grid.Grid grid: Grid object.
    :param int num_segments: The number of segments into which the piecewise linear
        cost curve is split.
    :param str area: Either the load zone, state name, state abbreviation, or
        interconnect.
    :param str gen_type: Generation type.
    :param str area_type: one of: *'loadzone'*, *'state'*, *'state_abbr'*,
        *'interconnect'*. Defaults to None, which allows
        :func:`powersimdata.network.model.area_to_loadzone` to infer the type.
    :param bool plot: If True, the supply curve plot is shown. If False, the plot is
        not shown.
    :return: (*tuple*) -- First element is a list of capacity (MW) amounts needed
        to create supply curve. Second element is a list of bids ($/MW) in the supply
        curve.
    :raises TypeError: if a powersimdata.input.grid.Grid object is not input.
    :raises ValueError: if the specified area or generator type is not applicable.
    """

    # Check that a Grid object is input
    if not isinstance(grid, Grid):
        raise TypeError("A Grid object must be input.")

    # Check that the desired number of linearized cost curve segments is an int
    if not isinstance(num_segments, int):
        raise TypeError(
            "The number of linearized cost curve segments must be input as an int."
        )

    # Obtain the desired generator cost and plant information data
    data = get_supply_data(grid, num_segments)

    # Check the input supply data
    check_supply_data(data, num_segments)

    # Check to make sure the generator type is valid
    if gen_type not in data["type"].unique():
        raise ValueError(f"{gen_type} is not a valid generation type.")

    # Identify the load zones that correspond to the specified area and area_type
    returned_zones = area_to_loadzone(grid.get_grid_model(), area, area_type)

    # Trim the DataFrame to only be of the desired area and generation type
    data = data.loc[data.zone_name.isin(returned_zones)]
    data = data.loc[data["type"] == gen_type]

    # Remove generators that have no capacity (e.g., Maine coal generators)
    if data["slope1"].isnull().values.any():
        data.dropna(subset=["slope1"], inplace=True)

    # Check if the area contains generators of the specified type
    if data.empty:
        return [], []

    # Combine the p_diff and slope information for each cost segment
    df_cols = []
    for i in range(num_segments):
        df_cols.append(data.loc[:, ("p_diff" + str(i + 1), "slope" + str(i + 1))])
        df_cols[i].rename(
            columns={"p_diff" + str(i + 1): "p_diff", "slope" + str(i + 1): "slope"},
            inplace=True,
        )
    df = pd.concat(df_cols, axis=0)

    # Sort the trimmed DataFrame by slope
    df = df.sort_values(by="slope")
    df = df.reset_index(drop=True)

    # Determine the points that comprise the supply curve
    P = []
    F = []
    p_diff_sum = 0
    for i in df.index:
        P.append(p_diff_sum)
        F.append(df["slope"][i])
        P.append(df["p_diff"][i] + p_diff_sum)
        F.append(df["slope"][i])
        p_diff_sum += df["p_diff"][i]

    # Plot the curve
    if plot:
        plt = _check_import("matplotlib.pyplot")
        plt.figure(figsize=[20, 10])
        plt.plot(P, F)
        plt.title(f"Supply curve for {gen_type} generators in {area}", fontsize=20)
        plt.xlabel("Capacity (MW)", fontsize=20)
        plt.ylabel("Price ($/MW)", fontsize=20)
        plt.xticks(fontsize=20)
        plt.yticks(fontsize=20)
        plt.show()

    # Return the capacity and bid amounts
    return P, F
Example #15
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)
Example #16
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