Esempio n. 1
0
def _calculate_mw_miles(original_grid, ct, exclude_branches=None):
    """Given a base grid and a change table, calculate the number of upgraded
    lines and transformers, and the total upgrade quantity (in MW & MW-miles).
    This function is separate from calculate_mw_miles() for testing purposes.
    Currently only supports change_tables that specify branches, not zone_name.
    Currently lumps Transformer and TransformerWinding upgrades together.

    :param powersimdata.input.grid.Grid original_grid: grid instance.
    :param dict ct: change table instance.
    :param list/tuple/set/None exclude_branches: branches to exclude.
    :raises ValueError: if not all values in exclude_branches are in the grid.
    :raises TypeError: if exclude_branches gets the wrong type.
    :return: (*dict*) -- Upgrades to the branches.
    """

    upgrade_categories = ("mw_miles", "transformer_mw", "num_lines", "num_transformers")
    upgrades = {u: 0 for u in upgrade_categories}

    if "branch" not in ct or "branch_id" not in ct["branch"]:
        return upgrades

    if exclude_branches is None:
        exclude_branches = {}
    elif isinstance(exclude_branches, (list, set, tuple)):
        good_branch_indices = original_grid.branch.index
        if not all([e in good_branch_indices for e in exclude_branches]):
            raise ValueError("not all branches are present in grid!")
        exclude_branches = set(exclude_branches)
    else:
        raise TypeError("exclude_branches must be None, list, tuple, or set")

    base_branch = original_grid.branch
    upgraded_branches = ct["branch"]["branch_id"]
    for b, v in upgraded_branches.items():
        if b in exclude_branches:
            continue
        # 'upgraded' capacity is v-1 because a scale of 1 = an upgrade of 0
        upgraded_capacity = base_branch.loc[b, "rateA"] * (v - 1)
        device_type = base_branch.loc[b, "branch_device_type"]
        if device_type == "Line":
            from_coords = (
                base_branch.loc[b, "from_lat"],
                base_branch.loc[b, "from_lon"],
            )
            to_coords = (base_branch.loc[b, "to_lat"], base_branch.loc[b, "to_lon"])
            addtl_mw_miles = upgraded_capacity * haversine(from_coords, to_coords)
            upgrades["mw_miles"] += addtl_mw_miles
            upgrades["num_lines"] += 1
        elif device_type == "Transformer":
            upgrades["transformer_mw"] += upgraded_capacity
            upgrades["num_transformers"] += 1
        elif device_type == "TransformerWinding":
            upgrades["transformer_mw"] += upgraded_capacity
            upgrades["num_transformers"] += 1
        else:
            raise Exception("Unknown branch: " + str(b))

    return upgrades
    def _calculate_single_line_cost(line, bus):
        """Calculate cost of upgrading a single HVDC line.

        :param pandas.Series line: HVDC line series featuring *'from_bus_id'*',
            *'to_bus_id'* and *'Pmax'*.
        :param pandas.Dataframe bus: bus data frame featuring *'lat'*, *'lon'*.
        :return: (*float*) -- HVDC line upgrade cost in $2015.
        """
        # Calculate distance
        from_lat = bus.loc[line.from_bus_id, "lat"]
        from_lon = bus.loc[line.from_bus_id, "lon"]
        to_lat = bus.loc[line.to_bus_id, "lat"]
        to_lon = bus.loc[line.to_bus_id, "lon"]
        miles = haversine((from_lat, from_lon), (to_lat, to_lon))
        # Calculate cost
        total_cost = line.Pmax * (
            miles * const.hvdc_line_cost["costMWmi"] * calculate_inflation(2015)
            + 2 * const.hvdc_terminal_cost_per_MW * calculate_inflation(2020)
        )
        return total_cost
Esempio n. 3
0
    def _add_branch(self):
        """Adds branch(es) to the grid."""
        v2x = voltage_to_x_per_distance(self.grid)
        for entry in self.ct["new_branch"]:
            new_branch = {c: 0 for c in self.grid.branch.columns}
            from_bus_id = entry["from_bus_id"]
            to_bus_id = entry["to_bus_id"]
            interconnect = self.grid.bus.loc[from_bus_id].interconnect
            from_zone_id = self.grid.bus.loc[from_bus_id].zone_id
            to_zone_id = self.grid.bus.loc[to_bus_id].zone_id
            from_zone_name = self.grid.id2zone[from_zone_id]
            to_zone_name = self.grid.id2zone[to_zone_id]
            from_lon = self.grid.bus.loc[from_bus_id].lon
            from_lat = self.grid.bus.loc[from_bus_id].lat
            to_lon = self.grid.bus.loc[to_bus_id].lon
            to_lat = self.grid.bus.loc[to_bus_id].lat
            from_basekv = v2x[self.grid.bus.loc[from_bus_id].baseKV]
            to_basekv = v2x[self.grid.bus.loc[to_bus_id].baseKV]
            distance = haversine((from_lat, from_lon), (to_lat, to_lon))
            x = distance * np.mean([from_basekv, to_basekv])

            new_branch["from_bus_id"] = entry["from_bus_id"]
            new_branch["to_bus_id"] = entry["to_bus_id"]
            new_branch["status"] = 1
            new_branch["ratio"] = 0
            new_branch["branch_device_type"] = "Line"
            new_branch["rateA"] = entry["Pmax"]
            new_branch["interconnect"] = interconnect
            new_branch["from_zone_id"] = from_zone_id
            new_branch["to_zone_id"] = to_zone_id
            new_branch["from_zone_name"] = from_zone_name
            new_branch["to_zone_name"] = to_zone_name
            new_branch["from_lon"] = from_lon
            new_branch["from_lat"] = from_lat
            new_branch["to_lon"] = to_lon
            new_branch["to_lat"] = to_lat
            new_branch["x"] = x
            new_index = [self.grid.branch.index[-1] + 1]
            self.grid.branch = pd.concat(
                [self.grid.branch,
                 pd.DataFrame(new_branch, index=new_index)])
Esempio n. 4
0
    def _calculate_single_line_cost(line, bus):
        """Given a series representing a DC line upgrade/addition, and a dataframe of
        bus locations, calculate this line's upgrade cost.

        :param pandas.Series line: DC line series featuring:
            {"from_bus_id", "to_bus_id", "Pmax"}.
        :param pandas.Dataframe bus: Bus data frame featuring {"lat", "lon"}.
        :return: (*float*) -- DC line upgrade cost (in $2015).
        """
        # Calculate distance
        from_lat = bus.loc[line.from_bus_id, "lat"]
        from_lon = bus.loc[line.from_bus_id, "lon"]
        to_lat = bus.loc[line.to_bus_id, "lat"]
        to_lon = bus.loc[line.to_bus_id, "lon"]
        miles = haversine((from_lat, from_lon), (to_lat, to_lon))
        # Calculate cost
        total_cost = line.Pmax * (
            miles * const.hvdc_line_cost["costMWmi"] *
            calculate_inflation(2015) +
            2 * const.hvdc_terminal_cost_per_MW * calculate_inflation(2020))
        return total_cost
Esempio n. 5
0
def voltage_to_x_per_distance(grid):
    """Calculates reactance per distance for voltage level.

    :param powersimdata.input.grid.Grid grid: a Grid object instance.
    :return: (*dict*) -- bus voltage to average reactance per mile.
    """
    branch = grid.branch[grid.branch.branch_device_type == "Line"]
    distance = (branch[["from_lat", "from_lon", "to_lat",
                        "to_lon"]].apply(lambda x: haversine((x[0], x[1]),
                                                             (x[2], x[3])),
                                         axis=1).values)

    no_zero = np.nonzero(distance)[0]
    x_per_distance = (branch.iloc[no_zero].x / distance[no_zero]).values

    basekv = np.array(
        [grid.bus.baseKV[i] for i in branch.iloc[no_zero].from_bus_id])

    v2x = {
        v: np.mean(x_per_distance[np.where(basekv == v)[0]])
        for v in set(basekv)
    }

    return v2x
def plot_powerflow_snapshot(
    scenario,
    hour,
    b2b_dclines=None,
    demand_centers=None,
    ac_branch_color="#8B36FF",
    dc_branch_color="#01D4ED",
    solar_color="#FFBB45",
    wind_color="#78D911",
    demand_color="gray",
    figsize=(1400, 800),
    circle_scale_factor=0.25,
    bg_width_scale_factor=0.001,
    pf_width_scale_factor=0.00125,
    arrow_pf_threshold=3000,
    arrow_dist_threshold=20,
    num_ac_arrows=1,
    num_dc_arrows=1,
    min_arrow_size=5,
    branch_alpha=0.5,
    state_borders_kwargs=None,
    x_range=None,
    y_range=None,
    legend_font_size=None,
):
    """Plot a snapshot of powerflow.

    :param powersimdata.scenario.scenario.Scenario scenario: scenario to plot.
    :param pandas.Timestamp/numpy.datetime64/datetime.datetime hour: snapshot interval.
    :param dict b2b_dclines: which DC lines are actually B2B facilities. Keys are:
        {"from", "to"}, values are iterables of DC line indices to plot (indices in
        "from" get plotted at the "from" end, and vice versa).
    :param pandas.DataFrame demand_centers: lat/lon centers at which to plot the demand
        from each load zone.
    :param str ac_branch_color: color to plot AC branches.
    :param str dc_branch_color: color to plot DC branches.
    :param str solar_color: color to plot solar generation.
    :param str wind_color: color to plot wind generation.
    :param str demand_color: color to plot demand.
    :param tuple figsize: size of the bokeh figure (in pixels).
    :param int/float circle_scale_factor: scale factor for demand/solar/wind circles.
    :param int/float bg_width_scale_factor: scale factor for grid capacities.
    :param int/float pf_width_scale_factor: scale factor for power flows.
    :param int/float arrow_pf_threshold: minimum power flow (MW) for adding arrows.
    :param int/float arrow_dist_threshold: minimum distance (miles) for adding arrows.
    :param int num_ac_arrows: number of arrows for each AC branch.
    :param int num_dc_arrows: number of arrows for each DC branch.
    :param int/float min_arrow_size: minimum arrow size.
    :param int/float branch_alpha: opaqueness of branches.
    :param dict state_borders_kwargs: keyword arguments to be passed to
        :func:`postreise.plot.plot_states.add_state_borders`.
    :param tuple(float, float) x_range: x range to zoom plot to (EPSG:3857).
    :param tuple(float, float) y_range: y range to zoom plot to (EPSG:3857).
    :param int/str legend_font_size: size to display legend specified as e.g. 12/'12pt'.
    :return: (*bokeh.plotting.figure*) -- power flow snapshot map.
    """
    _check_scenario_is_in_analyze_state(scenario)
    _check_date_range_in_scenario(scenario, hour, hour)
    # Get scenario data
    grid = scenario.state.get_grid()
    bus = grid.bus
    plant = grid.plant
    # Augment the branch dataframe with extra info needed for plotting
    branch = grid.branch
    branch["pf"] = scenario.state.get_pf().loc[hour]
    branch = branch.query("pf != 0").copy()
    branch["dist"] = branch.apply(lambda x: haversine((x.from_lat, x.from_lon),
                                                      (x.to_lat, x.to_lon)),
                                  axis=1)
    branch["arrow_size"] = branch["pf"].abs(
    ) * pf_width_scale_factor + min_arrow_size
    branch = project_branch(branch)
    # Augment the dcline dataframe with extra info needed for plotting
    dcline = grid.dcline
    dcline["pf"] = scenario.state.get_dcline_pf().loc[hour]
    dcline["from_lat"] = dcline.apply(lambda x: bus.loc[x.from_bus_id, "lat"],
                                      axis=1)
    dcline["from_lon"] = dcline.apply(lambda x: bus.loc[x.from_bus_id, "lon"],
                                      axis=1)
    dcline["to_lat"] = dcline.apply(lambda x: bus.loc[x.to_bus_id, "lat"],
                                    axis=1)
    dcline["to_lon"] = dcline.apply(lambda x: bus.loc[x.to_bus_id, "lon"],
                                    axis=1)
    dcline["dist"] = dcline.apply(lambda x: haversine((x.from_lat, x.from_lon),
                                                      (x.to_lat, x.to_lon)),
                                  axis=1)
    dcline["arrow_size"] = dcline["pf"].abs(
    ) * pf_width_scale_factor + min_arrow_size
    dcline = project_branch(dcline)
    # Create a dataframe for demand plotting, if necessary
    if demand_centers is not None:
        demand = scenario.state.get_demand()
        demand_centers["demand"] = demand.loc[hour]
        demand_centers = project_bus(demand_centers)

    # create canvas
    canvas = create_map_canvas(figsize=figsize,
                               x_range=x_range,
                               y_range=y_range)

    # Add state borders
    default_state_borders_kwargs = {"fill_alpha": 0.0, "background_map": False}
    all_state_borders_kwargs = ({
        **default_state_borders_kwargs,
        **state_borders_kwargs
    } if state_borders_kwargs is not None else default_state_borders_kwargs)
    _check_func_kwargs(add_state_borders, set(all_state_borders_kwargs),
                       "state_borders_kwargs")
    canvas = add_state_borders(canvas, **all_state_borders_kwargs)

    if b2b_dclines is not None:
        # Append the pseudo AC lines to the branch dataframe, remove from dcline
        all_b2b_dclines = list(b2b_dclines["to"]) + list(b2b_dclines["from"])
        pseudo_ac_lines = dcline.loc[all_b2b_dclines]
        pseudo_ac_lines["rateA"] = pseudo_ac_lines[["Pmin",
                                                    "Pmax"]].abs().max(axis=1)
        branch = branch.append(pseudo_ac_lines)
        # Construct b2b dataframe so that all get plotted at their 'from' x/y
        b2b_from = dcline.loc[b2b_dclines["from"]]
        b2b_to = dcline.loc[b2b_dclines["to"]].rename(
            {
                "from_x": "to_x",
                "from_y": "to_y",
                "to_x": "from_x",
                "to_y": "from_y"
            },
            axis=1,
        )
        b2b = pd.concat([b2b_from, b2b_to])
        dcline = dcline.loc[~dcline.index.isin(all_b2b_dclines)]

    # Plot grid background in grey
    canvas.multi_line(
        branch[["from_x", "to_x"]].to_numpy().tolist(),
        branch[["from_y", "to_y"]].to_numpy().tolist(),
        color="gray",
        alpha=branch_alpha,
        line_width=(branch["rateA"].abs() * bg_width_scale_factor),
    )
    canvas.multi_line(
        dcline[["from_x", "to_x"]].to_numpy().tolist(),
        dcline[["from_y", "to_y"]].to_numpy().tolist(),
        color="gray",
        alpha=branch_alpha,
        line_width=(dcline[["Pmin", "Pmax"]].abs().max(axis=1) *
                    bg_width_scale_factor),
    )
    if b2b_dclines is not None:
        canvas.scatter(
            x=b2b.from_x,
            y=b2b.from_y,
            color="gray",
            alpha=0.5,
            marker="triangle",
            size=(b2b[["Pmin", "Pmax"]].abs().max(axis=1) *
                  bg_width_scale_factor),
        )

    fake_location = branch.iloc[0].drop("x").rename({
        "from_x": "x",
        "from_y": "y"
    })
    # Legend entries
    canvas.multi_line(
        (fake_location.x, fake_location.x),
        (fake_location.y, fake_location.y),
        color=dc_branch_color,
        alpha=branch_alpha,
        line_width=10,
        legend_label="HVDC powerflow",
        visible=False,
    )
    canvas.multi_line(
        (fake_location.x, fake_location.x),
        (fake_location.y, fake_location.y),
        color=ac_branch_color,
        alpha=branch_alpha,
        line_width=10,
        legend_label="AC powerflow",
        visible=False,
    )
    canvas.circle(
        fake_location.x,
        fake_location.y,
        color=solar_color,
        alpha=0.6,
        size=5,
        legend_label="Solar Gen.",
        visible=False,
    )
    canvas.circle(
        fake_location.x,
        fake_location.y,
        color=wind_color,
        alpha=0.6,
        size=5,
        legend_label="Wind Gen.",
        visible=False,
    )

    # Plot demand
    if demand_centers is not None:
        canvas.circle(
            fake_location.x,
            fake_location.y,
            color=demand_color,
            alpha=0.3,
            size=5,
            legend_label="Demand",
            visible=False,
        )
        canvas.circle(
            demand_centers.x,
            demand_centers.y,
            color=demand_color,
            alpha=0.3,
            size=(demand_centers.demand * circle_scale_factor)**0.5,
        )

    # Aggregate solar and wind for plotting
    plant_with_pg = plant.copy()
    plant_with_pg["pg"] = scenario.state.get_pg().loc[hour]
    grouped_solar = aggregate_plant_generation(
        plant_with_pg.query("type == 'solar'"))
    grouped_wind = aggregate_plant_generation(
        plant_with_pg.query("type == 'wind'"))
    # Plot solar, wind
    canvas.circle(
        grouped_solar.x,
        grouped_solar.y,
        color=solar_color,
        alpha=0.6,
        size=(grouped_solar.pg * circle_scale_factor)**0.5,
    )
    canvas.circle(
        grouped_wind.x,
        grouped_wind.y,
        color=wind_color,
        alpha=0.6,
        size=(grouped_wind.pg * circle_scale_factor)**0.5,
    )

    # Plot powerflow on AC branches
    canvas.multi_line(
        branch[["from_x", "to_x"]].to_numpy().tolist(),
        branch[["from_y", "to_y"]].to_numpy().tolist(),
        color=ac_branch_color,
        alpha=branch_alpha,
        line_width=(branch["pf"].abs() * pf_width_scale_factor),
    )
    add_arrows(
        canvas,
        branch,
        color=ac_branch_color,
        pf_threshold=arrow_pf_threshold,
        dist_threshold=arrow_dist_threshold,
        n=num_ac_arrows,
    )

    # Plot powerflow on DC lines
    canvas.multi_line(
        dcline[["from_x", "to_x"]].to_numpy().tolist(),
        dcline[["from_y", "to_y"]].to_numpy().tolist(),
        color=dc_branch_color,
        alpha=branch_alpha,
        line_width=(dcline["pf"].abs() * pf_width_scale_factor),
    )
    add_arrows(
        canvas,
        dcline,
        color=dc_branch_color,
        pf_threshold=0,
        dist_threshold=0,
        n=num_dc_arrows,
    )
    # B2Bs
    if b2b_dclines is not None:
        canvas.scatter(
            x=b2b.from_x,
            y=b2b.from_y,
            color=dc_branch_color,
            alpha=0.5,
            marker="triangle",
            size=(b2b["pf"].abs() * pf_width_scale_factor * 5),
        )

    canvas.legend.location = "bottom_left"
    if legend_font_size is not None:
        if isinstance(legend_font_size, (int, float)):
            legend_font_size = f"{legend_font_size}pt"
        canvas.legend.label_text_font_size = legend_font_size

    return canvas
Esempio n. 7
0
def add_transmission_upgrades(
    canvas,
    branch_merge,
    dc_merge,
    b2b_indices=None,
    diff_threshold=100,
    all_branch_scale=1,
    diff_branch_scale=1,
    all_branch_min=0.1,
    diff_branch_min=1.0,
    b2b_scale=5,
    dcline_upgrade_dist_threshold=0,
):
    """Make map of branches showing upgrades.

    :param bokeh.plotting.figure.Figure canvas: canvas to add upgrades to.
    :param pandas.DataFrame branch_merge: branch of scenarios 1 and 2
    :param pandas.DataFrame dc_merge: dclines for scenarios 1 and 2
    :param list/set/tuple b2b_indices: indices of HVDC lines which are back-to-backs.
    :param int/float diff_threshold: difference threshold (in MW), above which branches
        are highlighted.
    :param int/float all_branch_scale: scale factor for plotting all branches
        (pixels/GW).
    :param int/float diff_branch_scale: scale factor for plotting branches with
        differences above the threshold (pixels/GW).
    :param int/float all_branch_min: minimum width to plot all branches.
    :param int/float diff_branch_min: minimum width to plot branches with significant
        differences.
    :param int/float b2b_scale: scale factor for plotting b2b facilities (pixels/GW).
    :param int/float dcline_upgrade_dist_threshold: minimum distance (miles) for
        plotting DC line upgrades (if none are longer, no legend entry will be created).
    :return: (*bokeh.plotting.figure.Figure*) -- Bokeh map plot of color-coded upgrades.
    """
    # plotting constants
    legend_alpha = 0.9
    all_elements_alpha = 0.5
    differences_alpha = 0.8
    # convert scale factors from pixels/GW to pixels/MW (base units for our grid data)
    all_branch_scale_MW = all_branch_scale / 1000  # noqa: N806
    diff_branch_scale_MW = diff_branch_scale / 1000  # noqa: N806
    b2b_scale_MW = b2b_scale / 1000  # noqa: N806

    # data prep
    branch_all = project_branch(branch_merge)
    branch_dc = project_branch(dc_merge)

    # For these, we will plot a triangle for the B2B location, plus 'pseudo' AC lines
    # get_level_values allows us to index into MultiIndex as necessary
    b2b_indices = {} if b2b_indices is None else b2b_indices
    b2b_mask = branch_dc.index.get_level_values(0).isin(b2b_indices)
    # .copy() avoids a pandas SettingWithCopyError later
    b2b = branch_dc.iloc[b2b_mask].copy()
    branch_dc_lines = branch_dc.loc[~b2b_mask].copy()

    # Color branches based on upgraded capacity
    branch_all["color"] = np.nan
    branch_all.loc[branch_all["diff"] > diff_threshold,
                   "color"] = colors.be_blue
    branch_all.loc[branch_all["diff"] < -1 * diff_threshold,
                   "color"] = colors.be_purple
    # Color pseudo AC branches based on upgraded capacity
    b2b["color"] = np.nan
    b2b.loc[b2b["diff"] > diff_threshold, "color"] = colors.be_blue
    b2b.loc[b2b["diff"] < -1 * diff_threshold, "color"] = colors.be_purple
    b2b = b2b[~b2b.color.isnull()]
    # Color DC lines based on upgraded capacity
    branch_dc_lines["dist"] = branch_dc_lines.apply(lambda x: haversine(
        (x.from_lat, x.from_lon), (x.to_lat, x.to_lon)),
                                                    axis=1)
    branch_dc_lines = branch_dc_lines.loc[
        branch_dc_lines.dist >= dcline_upgrade_dist_threshold]
    branch_dc_lines.loc[:, "color"] = np.nan
    branch_dc_lines.loc[branch_dc_lines["diff"] > 0, "color"] = colors.be_green
    branch_dc_lines.loc[branch_dc_lines["diff"] < 0,
                        "color"] = colors.be_lightblue

    # Create ColumnDataSources for bokeh to plot with
    source_all_ac = ColumnDataSource({
        "xs":
        branch_all[["from_x", "to_x"]].values.tolist(),
        "ys":
        branch_all[["from_y", "to_y"]].values.tolist(),
        "cap":
        branch_all["rateA"] * all_branch_scale_MW + all_branch_min,
        "color":
        branch_all["color"],
    })
    # AC branches with significant differences
    ac_diff_branches = branch_all.loc[~branch_all.color.isnull()]
    source_ac_difference = ColumnDataSource({
        "xs":
        ac_diff_branches[["from_x", "to_x"]].values.tolist(),
        "ys":
        ac_diff_branches[["from_y", "to_y"]].values.tolist(),
        "diff": (ac_diff_branches["diff"].abs() * diff_branch_scale_MW +
                 diff_branch_min),
        "color":
        ac_diff_branches["color"],
    })
    source_all_dc = ColumnDataSource({
        "xs":
        branch_dc_lines[["from_x", "to_x"]].values.tolist(),
        "ys":
        branch_dc_lines[["from_y", "to_y"]].values.tolist(),
        "cap":
        branch_dc_lines.Pmax * all_branch_scale_MW + all_branch_min,
        "color":
        branch_dc_lines["color"],
    })
    dc_diff_lines = branch_dc_lines.loc[~branch_dc_lines.color.isnull()]
    source_dc_differences = ColumnDataSource({
        "xs":
        dc_diff_lines[["from_x", "to_x"]].values.tolist(),
        "ys":
        dc_diff_lines[["from_y", "to_y"]].values.tolist(),
        "diff":
        (dc_diff_lines["diff"].abs() * diff_branch_scale_MW + diff_branch_min),
        "color":
        dc_diff_lines["color"],
    })
    source_pseudoac = ColumnDataSource(  # pseudo ac scen 1
        {
            "xs": b2b[["from_x", "to_x"]].values.tolist(),
            "ys": b2b[["from_y", "to_y"]].values.tolist(),
            "cap": b2b.Pmax * all_branch_scale_MW + all_branch_min,
            "diff": b2b["diff"].abs() * diff_branch_scale_MW + diff_branch_min,
            "color": b2b["color"],
        }
    )

    # Build the legend
    leg_x = [-8.1e6] * 2
    leg_y = [5.2e6] * 2

    # These are 'dummy' series to populate the legend with
    if len(branch_dc_lines[branch_dc_lines["diff"] > 0]) > 0:
        canvas.multi_line(
            leg_x,
            leg_y,
            color=colors.be_green,
            alpha=legend_alpha,
            line_width=10,
            legend_label="Additional HVDC Capacity",
        )
    if len(branch_dc_lines[branch_dc_lines["diff"] < 0]) > 0:
        canvas.multi_line(
            leg_x,
            leg_y,
            color=colors.be_lightblue,
            alpha=legend_alpha,
            line_width=10,
            legend_label="Reduced HVDC Capacity",
        )
    if len(branch_all[branch_all["diff"] < 0]) > 0:
        canvas.multi_line(
            leg_x,
            leg_y,
            color=colors.be_purple,
            alpha=legend_alpha,
            line_width=10,
            legend_label="Reduced AC Transmission",
        )
    if len(branch_all[branch_all["diff"] > 0]) > 0:
        canvas.multi_line(
            leg_x,
            leg_y,
            color=colors.be_blue,
            alpha=legend_alpha,
            line_width=10,
            legend_label="Upgraded AC transmission",
        )
    if len(b2b[b2b["diff"] > 0]) > 0:
        canvas.scatter(
            x=b2b.from_x[1],
            y=b2b.from_y[1],
            color=colors.be_magenta,
            marker="triangle",
            legend_label="Upgraded B2B capacity",
            size=30,
            alpha=legend_alpha,
        )

    # Everything below gets plotted into the 'main' figure
    background_plot_dicts = [
        {
            "source": source_all_ac,
            "color": "gray",
            "line_width": "cap"
        },
        {
            "source": source_all_dc,
            "color": "gray",
            "line_width": "cap"
        },
        {
            "source": source_pseudoac,
            "color": "gray",
            "line_width": "cap"
        },
    ]
    for d in background_plot_dicts:
        canvas.multi_line(
            "xs",
            "ys",
            color=d["color"],
            line_width=d["line_width"],
            source=d["source"],
            alpha=all_elements_alpha,
        )

    # all B2Bs
    canvas.scatter(
        x=b2b.from_x,
        y=b2b.from_y,
        color="gray",
        marker="triangle",
        size=b2b["Pmax"].abs() * b2b_scale_MW,
        alpha=all_elements_alpha,
    )

    difference_plot_dicts = [
        {
            "source": source_pseudoac,
            "color": "color",
            "line_width": "diff"
        },
        {
            "source": source_ac_difference,
            "color": "color",
            "line_width": "diff"
        },
        {
            "source": source_dc_differences,
            "color": "color",
            "line_width": "diff"
        },
    ]

    for d in difference_plot_dicts:
        canvas.multi_line(
            "xs",
            "ys",
            color=d["color"],
            line_width=d["line_width"],
            source=d["source"],
            alpha=differences_alpha,
        )

    # B2Bs with differences
    canvas.scatter(
        x=b2b.from_x,
        y=b2b.from_y,
        color=colors.be_magenta,
        marker="triangle",
        size=b2b["diff"].abs() * b2b_scale_MW,
    )

    return canvas
def sjoin_nearest(left_df, right_df, search_dist=0.06):
    """Perform a spatial join between two input layers.

    :param geopandas.GeoDataFrame left_df: A dataframe of Points.
    :param geopandas.GeoDataFrame right_df: A dataframe of Polygons/Multipolygons.
    :param float/int search_dist: radius (in map units) around point to detect polygons.
    :return: (*geopandas.GeoDataFrame*) -- data frame of Points mapped to each Polygon.

    .. note:: data from nearest Polygon/Multipolygon will be used as a result if a
        Point falls outside all available Polygon/Multipolygons.
    """
    def _find_nearest(series, polygons, search_dist):
        """Find the closest polygon.

        :param pandas.Series series: point to map.
        :param geopandas.geodataframe.GeoDataFrame polygons: polygons to select from.
        :param float search_dist: radius around point to detect polygons.
        """
        geom = series[left_df.geometry.name]
        # Get geometries within search distance
        candidates = polygons.loc[polygons.intersects(
            geom.buffer(search_dist))]

        if len(candidates) == 0:
            raise ValueError(
                f"No polygons found within {search_dist} of {series.name}")

        # Select the closest Polygon
        distances = candidates.apply(
            lambda x: geom.distance(x[candidates.geometry.name].exterior),
            axis=1)
        closest_poly = polygons.loc[distances.idxmin].to_frame().T

        # Reset index
        series = series.to_frame().T.reset_index(drop=True)

        # Drop geometry from closest polygon
        closest_poly = closest_poly.drop(polygons.geometry.name, axis=1)
        closest_poly = closest_poly.reset_index(drop=True)

        # Join values
        join = series.join(closest_poly, lsuffix="_left", rsuffix="_right")

        # Add information about distance to closest geometry if requested
        join["dist"] = distances.min()

        return join.squeeze()

    gpd = _check_import("geopandas")

    if "dist" in (set(left_df.columns) | set(right_df.columns)):
        raise ValueError(
            "neither series nor polygons can contain a 'dist' column")

    # Explode possible MultiGeometries. This is a major speedup!
    right_df = right_df.explode()
    right_df = right_df.reset_index(drop=True)

    # Make spatial join between points that fall inside the Polygons
    points_in_regions = gpd.sjoin(left_df=left_df,
                                  right_df=right_df,
                                  op="intersects")
    points_in_regions["dist"] = 0

    # Since polygons may overlap, there can be duplicated buses that we want to filter
    duplicated = points_in_regions.loc[points_in_regions.index.duplicated(
        keep=False)]
    to_drop = set()
    for bus in set(duplicated["bus_id"]):
        entries = duplicated.query("bus_id == @bus")
        coords = entries["geometry"].iloc[0].coords[
            0]  # First duped entry, only point
        regions = set(entries["name_abbr"])  # noqa: F841
        candidates = points_in_regions.query(
            "index not in @duplicated.index and name_abbr in @regions")
        neighbor = candidates.apply(lambda x: haversine(
            (x.geometry.x, x.geometry.y), coords),
                                    axis=1).idxmin()
        closest_region = candidates.loc[neighbor, "name_abbr"]  # noqa: F841
        # There may be more than two overlapping geometries, capture all but the closest
        drop_regions = set(
            entries.query("name_abbr != @closest_region")["name_abbr"])
        # Since indices are duplicated, we need to drop via two-column tuples
        to_drop |= {(bus, d) for d in drop_regions}

    points_in_regions = points_in_regions.loc[~points_in_regions.set_index(
        ["bus_id", "name_abbr"]).index.isin(to_drop)]

    # Find closest Polygons, for points that don't fall within any
    missing_indices = set(left_df.index) - set(points_in_regions.index)
    points_not_in_regions = left_df.loc[missing_indices]
    closest_geometries = points_not_in_regions.apply(_find_nearest,
                                                     args=(right_df,
                                                           search_dist),
                                                     axis=1)

    # Merge everything together
    closest_geometries = gpd.GeoDataFrame(closest_geometries)
    result = points_in_regions.append(closest_geometries,
                                      ignore_index=True,
                                      sort=False)
    return result
def map_interconnections(
    grid,
    branch_distance_cutoff=5,
    figsize=(1400, 800),
    branch_width_scale_factor=0.5,
    hvdc_width_scale_factor=1,
    b2b_size_scale_factor=50,
    state_borders_kwargs=None,
):
    """Map transmission lines color coded by interconnection.

    :param powersimdata.input.grid.Grid grid: grid object.
    :param int/float branch_distance_cutoff: distance cutoff for branch display.
    :param tuple figsize: size of the bokeh figure (in pixels).
    :param int/float branch_width_scale_factor: scale factor for branch capacities.
    :param int/float hvdc_width_scale_factor: scale factor for hvdc capacities.
    :param int/float b2b_size_scale_factor: scale factor for back-to_back capacities.
    :param dict state_borders_kwargs: keyword arguments to be passed to
        :func:`postreise.plot.plot_states.add_state_borders`.
    :return: (*bokeh.plotting.figure*) -- interconnection map with lines and nodes.
    :raises TypeError:
        if ``branch_device_cutoff`` is not ``float``.
        if ``branch_width_scale_factor`` is not ``int`` or ``float``.
        if ``hvdc_width_scale_factor`` is not ``int`` or ``float``.
        if ``b2b_size_scale_factor`` is not ``int`` or ``float``.
    :raises ValueError:
        if ``branch_device_cutoff`` is negative.
        if ``branch_width_scale_factor`` is negative.
        if ``hvdc_width_scale_factor`` is negative.
        if ``b2b_size_scale_factor`` is negative.
        if grid model is not supported.
    """
    _check_grid_type(grid)
    if not isinstance(branch_distance_cutoff, (int, float)):
        raise TypeError("branch_distance_cutoff must be an int")
    if branch_distance_cutoff <= 0:
        raise ValueError("branch_distance_cutoff must be strictly positive")
    if not isinstance(branch_width_scale_factor, (int, float)):
        raise TypeError("branch_width_scale_factor must be a int/float")
    if branch_width_scale_factor < 0:
        raise ValueError("branch_width_scale_factor must be positive")
    if not isinstance(hvdc_width_scale_factor, (int, float)):
        raise TypeError("hvdc_width_scale_factor must be a int/float")
    if hvdc_width_scale_factor < 0:
        raise ValueError("hvdc_width_scale_factor must be positive")
    if not isinstance(b2b_size_scale_factor, (int, float)):
        raise TypeError("b2b_size_scale_factor must be a int/float")
    if b2b_size_scale_factor < 0:
        raise ValueError("b2b_size_scale_factor must be positive")

    # branches
    branch = grid.branch.copy()
    branch["to_coord"] = list(zip(branch["to_lat"], branch["to_lon"]))
    branch["from_coord"] = list(zip(branch["from_lat"], branch["from_lon"]))
    branch["dist"] = branch.apply(
        lambda row: distance.haversine(row["to_coord"], row["from_coord"]),
        axis=1)
    branch = branch.loc[branch["dist"] > branch_distance_cutoff]
    branch = project_branch(branch)

    branch_west = branch.loc[branch["interconnect"] == "Western"]
    branch_east = branch.loc[branch["interconnect"] == "Eastern"]
    branch_tx = branch.loc[branch["interconnect"] == "Texas"]

    # HVDC lines
    all_dcline = grid.dcline.copy()
    all_dcline["from_lon"] = grid.bus.loc[all_dcline["from_bus_id"],
                                          "lon"].values
    all_dcline["from_lat"] = grid.bus.loc[all_dcline["from_bus_id"],
                                          "lat"].values
    all_dcline["to_lon"] = grid.bus.loc[all_dcline["to_bus_id"], "lon"].values
    all_dcline["to_lat"] = grid.bus.loc[all_dcline["to_bus_id"], "lat"].values
    all_dcline = project_branch(all_dcline)

    if grid.grid_model == "usa_tamu":
        b2b_id = range(9)
    else:
        raise ValueError("grid model is not supported")
    dcline = all_dcline.iloc[~all_dcline.index.isin(b2b_id)]
    b2b = all_dcline.iloc[b2b_id]

    # create canvas
    canvas = create_map_canvas(figsize=figsize)

    # add state borders
    default_state_borders_kwargs = {
        "line_width": 2,
        "fill_alpha": 0,
        "background_map": False,
    }
    all_state_borders_kwargs = ({
        **default_state_borders_kwargs,
        **state_borders_kwargs
    } if state_borders_kwargs is not None else default_state_borders_kwargs)
    _check_func_kwargs(add_state_borders, set(all_state_borders_kwargs),
                       "state_borders_kwargs")
    canvas = add_state_borders(canvas, **all_state_borders_kwargs)

    # add state tooltips
    state_counts = count_nodes_per_state(grid)
    state2label = {
        s: c
        for s, c in zip(state_counts.index, state_counts.to_numpy())
    }
    canvas = add_state_tooltips(canvas, "nodes", state2label)

    canvas.multi_line(
        branch_west[["from_x", "to_x"]].to_numpy().tolist(),
        branch_west[["from_y", "to_y"]].to_numpy().tolist(),
        color="#006ff9",
        line_width=branch_west["rateA"].abs() * 1e-3 *
        branch_width_scale_factor,
        legend_label="Western",
    )

    canvas.multi_line(
        branch_east[["from_x", "to_x"]].to_numpy().tolist(),
        branch_east[["from_y", "to_y"]].to_numpy().tolist(),
        color="#8B36FF",
        line_width=branch_east["rateA"].abs() * 1e-3 *
        branch_width_scale_factor,
        legend_label="Eastern",
    )

    canvas.multi_line(
        branch_tx[["from_x", "to_x"]].to_numpy().tolist(),
        branch_tx[["from_y", "to_y"]].to_numpy().tolist(),
        color="#01D4ED",
        line_width=branch_tx["rateA"].abs() * 1e-3 * branch_width_scale_factor,
        legend_label="Texas",
    )

    canvas.multi_line(
        dcline[["from_x", "to_x"]].to_numpy().tolist(),
        dcline[["from_y", "to_y"]].to_numpy().tolist(),
        color="#FF2370",
        line_width=dcline["Pmax"] * 1e-3 * hvdc_width_scale_factor,
        legend_label="HVDC",
    )

    canvas.scatter(
        x=b2b["from_x"],
        y=b2b["from_y"],
        color="#FF2370",
        marker="triangle",
        size=b2b["Pmax"] * 1e-3 * b2b_size_scale_factor,
        legend_label="Back-to-Back",
    )

    canvas.legend.location = "bottom_left"
    canvas.legend.label_text_font_size = "12pt"

    return canvas
Esempio n. 10
0
def _calculate_ac_inv_costs(grid_new, sum_results=True):
    """Given a grid, calculate the total cost of building that grid's
    lines and transformers.
    This function is separate from calculate_ac_inv_costs() for testing purposes.
    Currently counts Transformer and TransformerWinding as transformers.
    Currently uses NEEM regions to find regional multipliers.

    :param powersimdata.input.grid.Grid grid_new: grid instance.
    :param boolean sum_results: if True, sum dataframe for each category.
    :return: (*dict*) -- Total costs (line costs, transformer costs).
    """
    def select_mw(x, cost_df):
        """Given a single branch, determine the closest kV/MW combination and return
        the corresponding cost $/MW-mi.

        :param pandas.core.series.Series x: data for a single branch
        :param pandas.core.frame.DataFrame cost_df: DataFrame with kV, MW, cost columns
        :return: (*pandas.core.series.Series*) -- series of ['MW', 'costMWmi'] to be
            assigned to given branch
        """

        # select corresponding cost table of selected kV
        tmp = cost_df[cost_df["kV"] == x.kV]
        # get rid of NaN values in this kV table
        tmp = tmp[~tmp["MW"].isna()]
        # find closest MW & corresponding cost
        return tmp.iloc[np.argmin(np.abs(tmp["MW"] -
                                         x.rateA))][["MW", "costMWmi"]]

    def get_transformer_mult(x,
                             bus_reg,
                             ac_reg_mult,
                             xfmr_lookup_alerted=set()):
        """Determine the regional multiplier based on kV and power (closest).

        :param pandas.core.series.Series x: data for a single transformer.
        :param pandas.core.frame.DataFrame bus_reg: data frame with bus regions
        :param pandas.core.frame.DataFrame ac_reg_mult: data frame with regional mults.
        :param set xfmr_lookup_alerted: set of (voltage, region) tuples for which
            a message has already been printed that this lookup was not found.
        :return: (*float*) -- regional multiplier.
        """
        max_kV = bus.loc[[x.from_bus_id, x.to_bus_id], "baseKV"].max()
        region = bus_reg.loc[x.from_bus_id, "name_abbr"]
        region_mults = ac_reg_mult.loc[ac_reg_mult.name_abbr == region]

        mult_lookup_kV = region_mults.loc[(region_mults.kV -
                                           max_kV).abs().idxmin()].kV
        region_kV_mults = region_mults[region_mults.kV == mult_lookup_kV]
        region_kV_mults = region_kV_mults.loc[~region_kV_mults.mult.isnull()]
        if len(region_kV_mults) == 0:
            mult = 1
            if (mult_lookup_kV, region) not in xfmr_lookup_alerted:
                print(
                    f"No multiplier for voltage {mult_lookup_kV} in {region}")
                xfmr_lookup_alerted.add((mult_lookup_kV, region))
        else:
            mult_lookup_MW = region_kV_mults.loc[(region_kV_mults.MW -
                                                  x.rateA).abs().idxmin(),
                                                 "MW"]
            mult = (region_kV_mults.loc[region_kV_mults.MW ==
                                        mult_lookup_MW].squeeze().mult)
        return mult

    # import data
    ac_cost = pd.DataFrame(const.ac_line_cost)
    ac_reg_mult = pd.read_csv(const.ac_reg_mult_path)
    try:
        bus_reg = pd.read_csv(const.bus_neem_regions_path, index_col="bus_id")
    except FileNotFoundError:
        bus_reg = bus_to_neem_reg(grid_new.bus)
        bus_reg.sort_index().to_csv(const.bus_neem_regions_path)
    xfmr_cost = pd.read_csv(const.transformer_cost_path, index_col=0).fillna(0)
    xfmr_cost.columns = [int(c) for c in xfmr_cost.columns]
    # Mirror across diagonal
    xfmr_cost += xfmr_cost.to_numpy().T - np.diag(np.diag(
        xfmr_cost.to_numpy()))

    # map line kV
    bus = grid_new.bus
    branch = grid_new.branch
    branch.loc[:,
               "kV"] = branch.apply(lambda x: bus.loc[x.from_bus_id, "baseKV"],
                                    axis=1)

    # separate transformers and lines
    t_mask = branch["branch_device_type"].isin(
        ["Transformer", "TransformerWinding"])
    transformers = branch[t_mask].copy()
    lines = branch[~t_mask].copy()
    # Find closest kV rating
    lines.loc[:, "kV"] = lines.apply(
        lambda x: ac_cost.loc[(ac_cost["kV"] - x.kV).abs().idxmin(), "kV"],
        axis=1,
    )
    lines[["MW", "costMWmi"]] = lines.apply(lambda x: select_mw(x, ac_cost),
                                            axis=1)

    # check that all buses included in this file and lat/long values match,
    #   otherwise re-run mapping script on mis-matching buses.
    # these buses are missing in region file
    bus_fix_index = bus[~bus.index.isin(bus_reg.index)].index
    bus_mask = bus[~bus.index.isin(bus_fix_index)]
    bus_mask = bus_mask.merge(bus_reg, how="left", on="bus_id")
    # these buses have incorrect lat/lon values in the region mapping file.
    #   re-running the region mapping script on those buses only.
    bus_fix_index2 = bus_mask[
        ~np.isclose(bus_mask.lat_x, bus_mask.lat_y)
        | ~np.isclose(bus_mask.lon_x, bus_mask.lon_y)].index
    bus_fix_index_all = bus_fix_index.tolist() + bus_fix_index2.tolist()
    # fix the identified buses, if necessary
    if len(bus_fix_index_all) > 0:
        bus_fix = bus_to_neem_reg(bus[bus.index.isin(bus_fix_index_all)])
        fix_cols = ["name_abbr", "lat", "lon"]
        bus_reg.loc[bus_reg.index.isin(bus_fix.index),
                    fix_cols] = bus_fix[fix_cols]

    bus_reg.drop(["lat", "lon"], axis=1, inplace=True)

    # map region multipliers onto lines
    ac_reg_mult = ac_reg_mult.melt(id_vars=["kV", "MW"],
                                   var_name="name_abbr",
                                   value_name="mult")

    lines = lines.merge(bus_reg,
                        left_on="to_bus_id",
                        right_on="bus_id",
                        how="inner")
    lines = lines.merge(ac_reg_mult, on=["name_abbr", "kV", "MW"], how="left")
    lines.rename(columns={
        "name_abbr": "reg_to",
        "mult": "mult_to"
    },
                 inplace=True)

    lines = lines.merge(bus_reg,
                        left_on="from_bus_id",
                        right_on="bus_id",
                        how="inner")
    lines = lines.merge(ac_reg_mult, on=["name_abbr", "kV", "MW"], how="left")
    lines.rename(columns={
        "name_abbr": "reg_from",
        "mult": "mult_from"
    },
                 inplace=True)

    # take average between 2 buses' region multipliers
    lines.loc[:, "mult"] = (lines["mult_to"] + lines["mult_from"]) / 2.0

    # calculate MWmi
    lines.loc[:, "lengthMi"] = lines.apply(lambda x: haversine(
        (x.from_lat, x.from_lon), (x.to_lat, x.to_lon)),
                                           axis=1)
    lines.loc[:, "MWmi"] = lines["lengthMi"] * lines["rateA"]

    # calculate cost of each line
    lines.loc[:, "Cost"] = lines["MWmi"] * lines["costMWmi"] * lines["mult"]

    # calculate transformer costs
    transformers["per_MW_cost"] = transformers.apply(
        lambda x: xfmr_cost.iloc[
            xfmr_cost.index.get_loc(bus.loc[x.from_bus_id, "baseKV"],
                                    method="nearest"),
            xfmr_cost.columns.get_loc(bus.loc[x.to_bus_id, "baseKV"],
                                      method="nearest"), ],
        axis=1,
    )
    transformers["mult"] = transformers.apply(
        lambda x: get_transformer_mult(x, bus_reg, ac_reg_mult), axis=1)

    transformers["Cost"] = (transformers["rateA"] *
                            transformers["per_MW_cost"] * transformers["mult"])

    lines.Cost *= calculate_inflation(2010)
    transformers.Cost *= calculate_inflation(2020)
    if sum_results:
        return {
            "line_cost": lines.Cost.sum(),
            "transformer_cost": transformers.Cost.sum(),
        }
    else:
        return {"line_cost": lines, "transformer_cost": transformers}
def _calculate_ac_inv_costs(grid_new, sum_results=True):
    """Calculate cost of upgrading AC lines and/or transformers. NEEM regions are
    used to find regional multipliers. Note that a transformer winding is considered
    as a transformer.

    :param powersimdata.input.grid.Grid grid_new: grid instance.
    :param bool sum_results: whether to sum data frame for each branch type. Defaults to
        True.
    :return: (*dict*) -- keys are {'line_cost', 'transformer_cost'}, values are either
        float if ``sum_results``, or pandas Series indexed by branch ID.
        Whether summed or not, values are $USD, inflation-adjusted to today.
    """

    def select_mw(x, cost_df):
        """Determine the closest kV/MW combination for a single branch and return
        the corresponding cost (in $/MW-mi).

        :param pandas.Series x: data for a single branch
        :param pandas.DataFrame cost_df: data frame with *'kV'*, *'MW'*, *'costMWmi'*
            as columns
        :return: (*pandas.Series*) -- series of [*'MW'*, *'costMWmi'*] to be assigned
            to branch.
        """
        underground_regions = ("NEISO", "NYISO J-K")
        filtered_cost_df = cost_df.copy()
        # Unless we are entirely within an underground region, drop this cost class
        if not (x.from_region == x.to_region and x.from_region in underground_regions):
            filtered_cost_df = filtered_cost_df.query("kV != 345 or MW != 500")
        # select corresponding cost table of selected kV
        filtered_cost_df = filtered_cost_df[filtered_cost_df["kV"] == x.kV]
        # get rid of NaN values in this kV table
        filtered_cost_df = filtered_cost_df[~filtered_cost_df["MW"].isna()]
        # find closest MW & corresponding cost
        filtered_cost_df = filtered_cost_df.iloc[
            np.argmin(np.abs(filtered_cost_df["MW"] - x.rateA))
        ]
        return filtered_cost_df.loc[["MW", "costMWmi"]]

    def get_branch_mult(x, bus_reg, ac_reg_mult, branch_lookup_alerted=set()):
        """Determine the regional multiplier based on kV and power (closest).

        :param pandas.Series x: data for a single branch.
        :param pandas.DataFrame bus_reg: data frame with bus regions.
        :param pandas.DataFrame ac_reg_mult: data frame with regional multipliers.
        :param set branch_lookup_alerted: set of (voltage, region) tuples for which
            a message has already been printed that this lookup was not found.
        :return: (*float*) -- regional multiplier.
        """
        # Select the highest voltage for transformers (branch end voltages should match)
        max_kV = bus.loc[[x.from_bus_id, x.to_bus_id], "baseKV"].max()  # noqa: N806
        # Average the multipliers for branches (transformer regions should match)
        regions = (x.from_region, x.to_region)
        region_mults = ac_reg_mult.loc[ac_reg_mult.name_abbr.isin(regions)]
        region_mults = region_mults.groupby(["kV", "MW"]).mean().reset_index()

        mult_lookup_kV = region_mults.loc[  # noqa: N806
            (region_mults.kV - max_kV).abs().idxmin()
        ].kV
        region_kV_mults = region_mults[region_mults.kV == mult_lookup_kV]  # noqa: N806
        region_kV_mults = region_kV_mults.loc[  # noqa: N806
            ~region_kV_mults.mult.isnull()
        ]
        if len(region_kV_mults) == 0:
            mult = 1
            if (mult_lookup_kV, regions) not in branch_lookup_alerted:
                print(f"No multiplier for voltage {mult_lookup_kV} in {regions}")
                branch_lookup_alerted.add((mult_lookup_kV, regions))
        else:
            mult_lookup_MW = region_kV_mults.loc[  # noqa: N806
                (region_kV_mults.MW - x.rateA).abs().idxmin(), "MW"
            ]
            mult = (
                region_kV_mults.loc[region_kV_mults.MW == mult_lookup_MW].squeeze().mult
            )
        return mult

    # import data
    ac_cost = pd.DataFrame(const.ac_line_cost)
    ac_reg_mult = pd.read_csv(const.ac_reg_mult_path)
    ac_reg_mult = ac_reg_mult.melt(
        id_vars=["kV", "MW"], var_name="name_abbr", value_name="mult"
    )
    try:
        bus_reg = pd.read_csv(const.bus_neem_regions_path, index_col="bus_id")
    except FileNotFoundError:
        bus_reg = bus_to_neem_reg(grid_new.bus)
        bus_reg.sort_index().to_csv(const.bus_neem_regions_path)
    xfmr_cost = pd.read_csv(const.transformer_cost_path, index_col=0).fillna(0)
    xfmr_cost.columns = [int(c) for c in xfmr_cost.columns]
    # Mirror across diagonal
    xfmr_cost += xfmr_cost.to_numpy().T - np.diag(np.diag(xfmr_cost.to_numpy()))

    # check that all buses included in this file and lat/long values match,
    # otherwise re-run mapping script on mis-matching buses. These buses are missing
    # in region file
    bus = grid_new.bus
    mapped_buses = bus.query("index in @bus_reg.index")
    missing_bus_indices = set(bus.index) - set(bus_reg.index)
    mapped_buses = merge_keep_index(mapped_buses, bus_reg, how="left", on="bus_id")
    # these buses have incorrect lat/lon values in the region mapping file.
    #   re-running the region mapping script on those buses only.
    misaligned_bus_indices = mapped_buses[
        ~np.isclose(mapped_buses.lat_x, mapped_buses.lat_y)
        | ~np.isclose(mapped_buses.lon_x, mapped_buses.lon_y)
    ].index
    all_buses_to_fix = set(missing_bus_indices) | set(misaligned_bus_indices)
    # fix the identified buses, if necessary
    if len(all_buses_to_fix) > 0:
        bus_fix = bus_to_neem_reg(bus.query("index in @all_buses_to_fix"))
        fix_cols = ["name_abbr", "lat", "lon"]
        corrected_bus_mappings = bus_fix.loc[misaligned_bus_indices, fix_cols]
        new_bus_mappings = bus_fix.loc[missing_bus_indices, fix_cols]
        bus_reg.loc[misaligned_bus_indices, fix_cols] = corrected_bus_mappings
        bus_reg = append_keep_index_name(bus_reg, new_bus_mappings)

    bus_reg.drop(["lat", "lon"], axis=1, inplace=True)

    # Add extra information to branch data frame
    branch = grid_new.branch
    branch.loc[:, "kV"] = bus.loc[branch.from_bus_id, "baseKV"].tolist()
    branch.loc[:, "from_region"] = bus_reg.loc[branch.from_bus_id, "name_abbr"].tolist()
    branch.loc[:, "to_region"] = bus_reg.loc[branch.to_bus_id, "name_abbr"].tolist()
    # separate transformers and lines
    t_mask = branch["branch_device_type"].isin(["Transformer", "TransformerWinding"])
    transformers = branch[t_mask].copy()
    lines = branch[~t_mask].copy()
    if len(lines) > 0:
        # Find closest kV rating
        lines.loc[:, "kV"] = lines.apply(
            lambda x: ac_cost.loc[(ac_cost["kV"] - x.kV).abs().idxmin(), "kV"],
            axis=1,
        )
        lines[["MW", "costMWmi"]] = lines.apply(lambda x: select_mw(x, ac_cost), axis=1)

        lines["mult"] = lines.apply(
            lambda x: get_branch_mult(x, bus_reg, ac_reg_mult), axis=1
        )

        # calculate MWmi
        lines.loc[:, "lengthMi"] = lines.apply(
            lambda x: haversine((x.from_lat, x.from_lon), (x.to_lat, x.to_lon)), axis=1
        )
    else:
        new_columns = ["kV", "MW", "costMWmi", "mult", "lengthMi"]
        lines = lines.reindex(columns=[*lines.columns.tolist(), *new_columns])
    lines.loc[:, "MWmi"] = lines["lengthMi"] * lines["rateA"]

    # calculate cost of each line
    lines.loc[:, "cost"] = lines["MWmi"] * lines["costMWmi"] * lines["mult"]

    # calculate transformer costs
    if len(transformers) > 0:
        from_to_kv = [
            xfmr_cost.index.get_indexer(
                bus.loc[transformers[e], "baseKV"], method="nearest"
            )
            for e in ["from_bus_id", "to_bus_id"]
        ]
        transformers["per_MW_cost"] = [
            xfmr_cost.iloc[f, t] for f, t in zip(from_to_kv[0], from_to_kv[1])
        ]
        transformers["mult"] = transformers.apply(
            lambda x: get_branch_mult(x, bus_reg, ac_reg_mult), axis=1
        )
    else:
        # Properly handle case with no transformers, where apply returns wrong dims
        transformers["per_MW_cost"] = []
        transformers["mult"] = []

    transformers["cost"] = (
        transformers["rateA"] * transformers["per_MW_cost"] * transformers["mult"]
    )

    lines.cost *= calculate_inflation(2010)
    transformers.cost *= calculate_inflation(2020)
    if sum_results:
        return {
            "line_cost": lines.cost.sum(),
            "transformer_cost": transformers.cost.sum(),
        }
    else:
        return {"line_cost": lines.cost, "transformer_cost": transformers.cost}
Esempio n. 12
0
def _identify_mesh_branch_upgrades(
    ref_scenario,
    upgrade_n=100,
    allow_list=None,
    deny_list=None,
    congestion_metric="quantile",
    cost_metric="branches",
    quantile=None,
):
    """Identify the N most congested branches in a previous scenario, based on
    the quantile value of congestion duals, where N is specified by ``upgrade_n``. A
    quantile value of 0.95 obtains the branches with highest dual in top 5% of hours.

    :param powersimdata.scenario.scenario.Scenario ref_scenario: the reference
        scenario to be used to determine the most congested branches.
    :param int upgrade_n: the number of branches to upgrade.
    :param list/set/tuple/None allow_list: only select from these branch IDs.
    :param list/set/tuple/None deny_list: never select any of these branch IDs.
    :param str congestion_metric: numerator method: 'quantile' or 'mean'.
    :param str cost_metric: denominator method: 'branches', 'cost', 'MW', or
        'MWmiles'.
    :param float quantile: if ``congestion_metric`` == 'quantile', this is the quantile
        to use to judge branch congestion (otherwise it is unused). If None, a default
        value of 0.95 is used, i.e. we evaluate the shadow price for the worst 5% of
        hours.
    :raises ValueError: if ``congestion_metric`` or ``cost_metric`` is not recognized,
        ``congestion_metric`` == 'mean' but a ``quantile`` is specified, or
        ``congestion_metric`` == 'quantile' but there are not enough branches which are
        congested at the desired frequency based on the ``quantile`` specified.
    :return: (*set*) -- A set of ints representing branch indices.
    """

    # How big does a dual value have to be to be 'real' and not barrier cruft?
    cong_significance_cutoff = 1e-6  # $/MWh
    # If we rank by MW-miles, what 'length' do we give to zero-length branches?
    zero_length_value = 1  # miles
    # If the quantile is not provided, what should we default to?
    default_quantile = 0.95

    # Validate congestion_metric input
    allowed_congestion_metrics = ("mean", "quantile")
    if congestion_metric not in allowed_congestion_metrics:
        allowed_list = ", ".join(allowed_congestion_metrics)
        raise ValueError(f"congestion_metric must be one of: {allowed_list}")
    if congestion_metric == "mean" and quantile is not None:
        raise ValueError("quantile cannot be specified if congestion_metric is 'mean'")
    if congestion_metric == "quantile" and quantile is None:
        quantile = default_quantile

    # Validate cost_metric input
    allowed_cost_metrics = ("branches", "MW", "MWmiles", "cost")
    if cost_metric not in allowed_cost_metrics:
        allowed_list = ", ".join(allowed_cost_metrics)
        raise ValueError(f"cost_metric must be one of: {allowed_list}")

    # Get raw congestion dual values, add them
    ref_cong_abs = ref_scenario.state.get_congu() + ref_scenario.state.get_congl()
    all_branches = set(ref_cong_abs.columns.tolist())
    # Create validated composite allow list, and filter shadow price data frame
    composite_allow_list = _construct_composite_allow_list(
        all_branches, allow_list, deny_list
    )
    ref_cong_abs = ref_cong_abs.filter(items=composite_allow_list)

    if congestion_metric == "mean":
        congestion_metric_values = ref_cong_abs.mean()
    if congestion_metric == "quantile":
        congestion_metric_values = ref_cong_abs.quantile(quantile)

    # Filter out 'insignificant' values
    congestion_metric_values = congestion_metric_values.where(
        congestion_metric_values > cong_significance_cutoff
    ).dropna()
    # Filter based on composite allow list
    congested_indices = list(congestion_metric_values.index)

    # Ensure that we have enough congested branches to upgrade
    num_congested = len(congested_indices)
    if num_congested < upgrade_n:
        err_msg = "not enough congested branches: "
        err_msg += f"{upgrade_n} desired, but only {num_congested} congested."
        if congestion_metric == "quantile":
            err_msg += (
                f" The quantile used is {quantile}; increasing this value will increase"
                " the number of branches which qualify as having 'frequent-enough'"
                " congestion and can be selected for upgrades."
            )
        raise ValueError(err_msg)

    # Calculate selected cost metric for congested branches
    if cost_metric == "cost":
        # Calculate costs for an upgrade dataframe containing only composite_allow_list
        base_grid = ref_scenario.get_base_grid()
        base_grid.branch = base_grid.branch.filter(items=congested_indices, axis=0)
        upgrade_costs = _calculate_ac_inv_costs(base_grid, sum_results=False)
        # Merge the individual line/transformer data into a single Series
        merged_upgrade_costs = pd.concat([v for v in upgrade_costs.values()])
    if cost_metric in ("MW", "MWmiles"):
        ref_grid = ref_scenario.state.get_grid()
        branch_ratings = ref_grid.branch.loc[congested_indices, "rateA"]
        # Calculate 'original' branch capacities, since that's our increment
        ref_ct = ref_scenario.state.get_ct()
        try:
            branch_ct = ref_ct["branch"]["branch_id"]
        except KeyError:
            branch_ct = {}
        branch_prev_scaling = pd.Series(
            {i: (branch_ct[i] if i in branch_ct else 1) for i in congested_indices}
        )
        branch_ratings = branch_ratings / branch_prev_scaling
    # Then, apply this metric
    if cost_metric == "MW":
        branch_metric = congestion_metric_values / branch_ratings
    elif cost_metric == "MWmiles":
        branch_lengths = ref_grid.branch.loc[congested_indices].apply(
            lambda x: haversine((x.from_lat, x.from_lon), (x.to_lat, x.to_lon)), axis=1
        )
        # Replace zero-length branches by designated default, don't divide by 0
        branch_lengths = branch_lengths.replace(0, value=zero_length_value)
        branch_metric = congestion_metric_values / (branch_ratings * branch_lengths)
    elif cost_metric == "cost":
        branch_metric = congestion_metric_values / merged_upgrade_costs
    else:
        # By process of elimination, all that's left is method 'branches'
        branch_metric = congestion_metric_values

    # Sort by our metric, grab indexes for N largest values (tail), return
    ranked_branches = set(branch_metric.sort_values().tail(upgrade_n).index)
    return ranked_branches
Esempio n. 13
0
def _identify_mesh_branch_upgrades(
    ref_scenario,
    upgrade_n=100,
    quantile=0.95,
    allow_list=None,
    deny_list=None,
    method="branches",
):
    """Identify the N most congested branches in a previous scenario, based on
    the quantile value of congestion duals. A quantile value of 0.95 obtains
    the branches with highest dual in top 5% of hours.

    :param powersimdata.scenario.scenario.Scenario ref_scenario: the reference
        scenario to be used to determine the most congested branches.
    :param int upgrade_n: the number of branches to upgrade.
    :param float quantile: the quantile to use to judge branch congestion.
    :param list/set/tuple/None allow_list: only select from these branch IDs.
    :param list/set/tuple/None deny_list: never select any of these branch IDs.
    :param str method: prioritization method: 'branches', 'MW', or 'MWmiles'.
    :raises ValueError: if 'method' not recognized, or not enough branches to
        upgrade.
    :return: (*set*) -- A set of ints representing branch indices.
    """

    # How big does a dual value have to be to be 'real' and not barrier cruft?
    cong_significance_cutoff = 1e-6  # $/MWh
    # If we rank by MW-miles, what 'length' do we give to zero-length branches?
    zero_length_value = 1  # miles

    # Validate method input
    allowed_methods = ("branches", "MW", "MWmiles", "cost")
    if method not in allowed_methods:
        allowed_list = ", ".join(allowed_methods)
        raise ValueError(f"method must be one of: {allowed_list}")

    # Get raw congestion dual values, add them
    ref_cong_abs = ref_scenario.state.get_congu(
    ) + ref_scenario.state.get_congl()
    all_branches = set(ref_cong_abs.columns.tolist())
    # Create validated composite allow list
    composite_allow_list = _construct_composite_allow_list(
        all_branches, allow_list, deny_list)

    # Parse 2-D array to vector of quantile values
    ref_cong_abs = ref_cong_abs.filter(items=composite_allow_list)
    quantile_cong_abs = ref_cong_abs.quantile(quantile)
    # Filter out insignificant values
    significance_bitmask = quantile_cong_abs > cong_significance_cutoff
    quantile_cong_abs = quantile_cong_abs.where(significance_bitmask).dropna()
    # Filter based on composite allow list
    congested_indices = list(quantile_cong_abs.index)

    # Ensure that we have enough congested branches to upgrade
    num_congested = len(quantile_cong_abs)
    if num_congested < upgrade_n:
        err_msg = "not enough congested branches: "
        err_msg += f"{upgrade_n} desired, but only {num_congested} congested."
        raise ValueError(err_msg)

    # Calculate selected metric for congested branches
    if method == "cost":
        # Calculate costs for an upgrade dataframe containing only composite_allow_list
        base_grid = Grid(ref_scenario.info["interconnect"],
                         ref_scenario.info["grid_model"])
        base_grid.branch = base_grid.branch.filter(items=congested_indices,
                                                   axis=0)
        upgrade_costs = _calculate_ac_inv_costs(base_grid, sum_results=False)
        # Merge the individual line/transformer data into a single Series
        merged_upgrade_costs = pd.concat([v for v in upgrade_costs.values()])
    if method in ("MW", "MWmiles"):
        ref_grid = ref_scenario.state.get_grid()
        branch_ratings = ref_grid.branch.loc[congested_indices, "rateA"]
        # Calculate 'original' branch capacities, since that's our increment
        ref_ct = ref_scenario.state.get_ct()
        try:
            branch_ct = ref_ct["branch"]["branch_id"]
        except KeyError:
            branch_ct = {}
        branch_prev_scaling = pd.Series({
            i: (branch_ct[i] if i in branch_ct else 1)
            for i in congested_indices
        })
        branch_ratings = branch_ratings / branch_prev_scaling
    if method == "MW":
        branch_metric = quantile_cong_abs / branch_ratings
    elif method == "MWmiles":
        branch_lengths = ref_grid.branch.loc[congested_indices].apply(
            lambda x: haversine((x.from_lat, x.from_lon),
                                (x.to_lat, x.to_lon)),
            axis=1)
        # Replace zero-length branches by designated default, don't divide by 0
        branch_lengths = branch_lengths.replace(0, value=zero_length_value)
        branch_metric = quantile_cong_abs / (branch_ratings * branch_lengths)
    elif method == "cost":
        branch_metric = quantile_cong_abs / merged_upgrade_costs
    else:
        # By process of elimination, all that's left is method 'branches'
        branch_metric = quantile_cong_abs

    # Sort by our metric, grab indexes for N largest values (tail), return
    ranked_branches = set(branch_metric.sort_values().tail(upgrade_n).index)
    return ranked_branches
Esempio n. 14
0
def map_interconnections(
    grid, state_counts, hover_choice, hvdc_width=1, us_states_dat=None
):
    """Maps transmission lines color coded by interconnection.

    :param powersimdata.input.grid.Grid grid: grid object.
    :param pandas.DataFrame state_counts: state names and node counts, created by
        :func:`count_nodes_per_state`.
    :param str hover_choice: "nodes" for state_counts nodes per state, otherwise HVDC
        capacity in hover over tool tips for hvdc lines only.
    :param float hvdc_width: adjust width of HVDC lines on map.
    :param dict us_states_dat: dictionary of state border lats/lons. If None, get
        from :func:`postreise.plot.plot_states.get_state_borders`.
    :return: (*bokeh.plotting.figure*) -- map of transmission lines.
    """
    if us_states_dat is None:
        us_states_dat = get_state_borders()

    # projection steps for mapping
    branch = grid.branch
    branch_bus = grid.bus
    branch_map = project_branch(branch)
    branch_map["point1"] = list(zip(branch_map.to_lat, branch_map.to_lon))
    branch_map["point2"] = list(zip(branch_map.from_lat, branch_map.from_lon))
    branch_map["dist"] = branch_map.apply(
        lambda row: distance.haversine(row["point1"], row["point2"]), axis=1
    )

    # speed rendering on website by removing very short branches
    branch_map = branch_map.loc[branch_map.dist > 5]

    branch_west = branch_map.loc[branch_map.interconnect == "Western"]
    branch_east = branch_map.loc[branch_map.interconnect == "Eastern"]
    branch_tx = branch_map.loc[branch_map.interconnect == "Texas"]
    branch_mdc = grid.dcline

    branch_mdc["from_lon"] = branch_bus.loc[branch_mdc.from_bus_id, "lon"].values
    branch_mdc["from_lat"] = branch_bus.loc[branch_mdc.from_bus_id, "lat"].values
    branch_mdc["to_lon"] = branch_bus.loc[branch_mdc.to_bus_id, "lon"].values
    branch_mdc["to_lat"] = branch_bus.loc[branch_mdc.to_bus_id, "lat"].values
    branch_mdc = project_branch(branch_mdc)

    # back to backs are index 0-8, treat separately
    branch_mdc1 = branch_mdc.iloc[
        9:,
    ]
    b2b = branch_mdc.iloc[
        0:9,
    ]

    branch_mdc_leg = branch_mdc
    branch_mdc_leg.loc[0:8, ["to_x"]] = np.nan
    branch_mdc_leg["to_x"] = branch_mdc_leg["to_x"].fillna(branch_mdc_leg["from_x"])
    branch_mdc_leg.loc[0:8, ["to_y"]] = np.nan
    branch_mdc_leg["to_y"] = branch_mdc_leg["to_y"].fillna(branch_mdc_leg["from_y"])

    # pseudolines for legend to show hvdc and back to back, plot UNDER map
    multi_line_source6 = ColumnDataSource(
        {
            "xs": branch_mdc_leg[["from_x", "to_x"]].values.tolist(),
            "ys": branch_mdc_leg[["from_y", "to_y"]].values.tolist(),
            "capacity": branch_mdc_leg.Pmax.astype(float) * 0.00023 + 0.2,
            "cap": branch_mdc_leg.Pmax.astype(float),
        }
    )

    # state borders
    a, b = project_borders(us_states_dat, state_list=list(state_counts["state"]))

    # transmission data sources
    line_width_const = 0.000225

    multi_line_source = ColumnDataSource(
        {
            "xs": branch_west[["from_x", "to_x"]].values.tolist(),
            "ys": branch_west[["from_y", "to_y"]].values.tolist(),
            "capacity": branch_west.rateA * line_width_const + 0.1,
        }
    )

    multi_line_source2 = ColumnDataSource(
        {
            "xs": branch_east[["from_x", "to_x"]].values.tolist(),
            "ys": branch_east[["from_y", "to_y"]].values.tolist(),
            "capacity": branch_east.rateA * line_width_const + 0.1,
        }
    )

    multi_line_source3 = ColumnDataSource(
        {
            "xs": branch_tx[["from_x", "to_x"]].values.tolist(),
            "ys": branch_tx[["from_y", "to_y"]].values.tolist(),
            "capacity": branch_tx.rateA * line_width_const + 0.1,
        }
    )
    # hvdc
    multi_line_source4 = ColumnDataSource(
        {
            "xs": branch_mdc1[["from_x", "to_x"]].values.tolist(),
            "ys": branch_mdc1[["from_y", "to_y"]].values.tolist(),
            "capacity": branch_mdc1.Pmax.astype(float) * line_width_const * hvdc_width
            + 0.1,
            "cap": branch_mdc1.Pmax.astype(float),
        }
    )

    # pseudolines for ac
    multi_line_source5 = ColumnDataSource(
        {
            "xs": b2b[["from_x", "to_x"]].values.tolist(),
            "ys": b2b[["from_y", "to_y"]].values.tolist(),
            "capacity": b2b.Pmax.astype(float) * 0.00023 + 0.2,
            "cap": b2b.Pmax.astype(float),
            "col": (
                "#006ff9",
                "#006ff9",
                "#006ff9",
                "#006ff9",
                "#006ff9",
                "#006ff9",
                "#006ff9",
                "#8B36FF",
                "#8B36FF",
            ),
        }
    )

    # lower 48 states, patches
    source = ColumnDataSource(
        dict(
            xs=a,
            ys=b,
            col=["gray" for i in range(48)],
            col2=["gray" for i in range(48)],
            label=list(state_counts["count"]),
            state_name=list(state_counts["state"]),
        )
    )

    # Set up figure
    tools: str = "pan, wheel_zoom, reset, save"

    p = figure(
        tools=tools,
        x_axis_location=None,
        y_axis_location=None,
        plot_width=800,
        plot_height=800,
        output_backend="webgl",
        sizing_mode="stretch_both",
        match_aspect=True,
    )

    # for legend, hidden lines
    leg_clr = ["#006ff9", "#8B36FF", "#01D4ED", "#FF2370"]
    leg_lab = ["Western", "Eastern", "Texas", "HVDC"]
    leg_xs = [-1.084288e07] * 4
    leg_ys = [4.639031e06] * 4

    for (colr, leg, x, y) in zip(leg_clr, leg_lab, leg_xs, leg_ys):
        p.line(x, y, color=colr, width=5, legend=leg)

    # pseudo lines for hover tips
    lines = p.multi_line(
        "xs", "ys", color="black", line_width="capacity", source=multi_line_source6
    )

    # background tiles
    p.add_tile(get_provider(Vendors.CARTODBPOSITRON))

    # state borders
    patch = p.patches("xs", "ys", fill_alpha=0.0, line_color="col", source=source)

    # branches
    source_list = [multi_line_source, multi_line_source2, multi_line_source3]

    for (colr, source) in zip(leg_clr[0:3], source_list):
        p.multi_line("xs", "ys", color=colr, line_width="capacity", source=source)

    p.multi_line(
        "xs", "ys", color="#FF2370", line_width="capacity", source=multi_line_source4
    )
    # pseudo ac
    p.multi_line(
        "xs", "ys", color="col", line_width="capacity", source=multi_line_source5
    )

    # triangles for b2b
    p.scatter(
        x=b2b.from_x,
        y=b2b.from_y,
        color="#FF2370",
        marker="triangle",
        size=b2b.Pmax / 50 + 5,
        legend="Back-to-Back",
    )

    # legend formatting
    p.legend.location = "bottom_right"
    p.legend.label_text_font_size = "12pt"

    if hover_choice == "nodes":
        hover = HoverTool(
            tooltips=[
                ("State", "@state_name"),
                ("Nodes", "@label"),
            ],
            renderers=[patch],
        )

    else:
        hover = HoverTool(
            tooltips=[
                ("HVDC capacity MW", "@cap"),
            ],
            renderers=[lines],
        )

    p.add_tools(hover)

    return p