Ejemplo n.º 1
0
def test_calculate_ac_inv_costs_not_summed(mock_grid):
    inflation_2010 = calculate_inflation(2010)
    inflation_2020 = calculate_inflation(2020)
    expected_ac_cost = {
        # ((reg_mult1 + reg_mult2) / 2) * sum(basecost * rateA * miles)
        "line_cost": {
            10:
            0,  # This branch would normally be dropped by calculate_ac_inv_costs
            11:
            ((1 + 2.25) / 2) * 3666.67 * 10 * 679.179925842 * inflation_2010,
            12:
            ((1 + 2.25) / 2) * 1500 * 1100 * 680.986501516 * inflation_2010,
            15: ((1 + 1) / 2) * 2333.33 * 50 * 20.003889808 * inflation_2010,
        },
        # for each: rateA * basecost * regional multiplier
        "transformer_cost": {
            13: (30 * 7670 * 1) * inflation_2020,
            14: (40 * 8880 * 2.25) * inflation_2020,
        },
    }
    ac_cost = _calculate_ac_inv_costs(mock_grid, sum_results=False)
    for branch_type, upgrade_costs in expected_ac_cost.items():
        assert set(upgrade_costs.keys()) == set(ac_cost[branch_type].index)
        for branch, cost in upgrade_costs.items():
            assert cost == pytest.approx(ac_cost[branch_type].loc[branch])
def test_calculate_dc_inv_costs(mock_grid):
    expected_dc_cost = (
        # lines
        10 * 679.1799258421203 * 457.1428571 * calculate_inflation(2015)
        # terminals
        + 135e3 * 10 * 2 * calculate_inflation(2020))
    dc_cost = _calculate_dc_inv_costs(mock_grid)
    assert dc_cost == pytest.approx(expected_dc_cost)
Ejemplo n.º 3
0
def test_calculate_dc_inv_costs(mock_grid):
    expected_dc_cost = (
        # lines
        calculate_inflation(2015) * 457.1428571 *
        (10 * 679.1799258421203 + 200 * 20.003889808)
        # terminals
        + 135e3 * (10 + 200) * 2 * calculate_inflation(2020))
    dc_cost = _calculate_dc_inv_costs(mock_grid)
    assert dc_cost == pytest.approx(expected_dc_cost)
Ejemplo n.º 4
0
def test_calculate_dc_inv_costs_not_summed(mock_grid):
    expected_dc_cost = {
        5: (457.1428571 * 10 * 679.1799258421203 * calculate_inflation(2015) +
            135e3 * 10 * 2 * calculate_inflation(2020)),
        14: (457.1428571 * 200 * 20.003889808 * calculate_inflation(2015) +
             135e3 * 200 * 2 * calculate_inflation(2020)),
    }
    dc_cost = _calculate_dc_inv_costs(mock_grid, sum_results=False)
    for dcline_id, expected_cost in expected_dc_cost.items():
        assert expected_cost == pytest.approx(dc_cost.loc[dcline_id])
def test_calculate_ac_inv_costs(mock_grid):
    expected_ac_cost = {
        # ((reg_mult1 + reg_mult2) / 2) * sum(basecost * rateA * miles)
        "line_cost":
        (((1 + 2.25) / 2) *
         (3666.67 * 10 * 679.179925842 + 1500 * 1100 * 680.986501516) *
         calculate_inflation(2010)),
        # for each: rateA * basecost * regional multiplier
        "transformer_cost":
        ((30 * 7670 * 1) + (40 * 8880 * 2.25)) * calculate_inflation(2020),
    }
    ac_cost = _calculate_ac_inv_costs(mock_grid)
    assert ac_cost.keys() == expected_ac_cost.keys()
    for k in ac_cost.keys():
        assert ac_cost[k] == pytest.approx(expected_ac_cost[k])
def test_calculate_gen_inv_costs_2030(mock_grid):
    gen_inv_cost = _calculate_gen_inv_costs(mock_grid, 2030,
                                            "Moderate").to_dict()
    expected_gen_inv_cost = {
        # for each: capacity (kW) * regional multiplier * base technology cost
        "solar":
        sum([
            15e3 * 1.01701 * 836.3842785,
            12e3 * 1.01701 * 836.3842785,
            8e3 * 1.01701 * 836.3842785,
        ]),
        "coal":
        30e3 * 1.05221 * 4049.047403,
        "wind":
        10e3 * 1.16979 * 1297.964758 + 15e3 * 1.04348 * 1297.964758,
        "ng":
        20e3 * 1.050755 * 983.2351768,
        "storage":
        100e3 * 1.012360 * 817 + 200e3 * 1.043730 * 817,
        "nuclear":
        1000e3 * 1.07252 * 6727.799801,
    }
    inflation = calculate_inflation(2018)
    expected_gen_inv_cost = {
        k: v * inflation
        for k, v in expected_gen_inv_cost.items()
    }
    assert gen_inv_cost.keys() == expected_gen_inv_cost.keys()
    for k in gen_inv_cost.keys():
        assert gen_inv_cost[k] == pytest.approx(expected_gen_inv_cost[k])
Ejemplo n.º 7
0
def test_calculate_gen_inv_costs_not_summed(mock_grid):
    gen_inv_cost = _calculate_gen_inv_costs(mock_grid,
                                            2025,
                                            "Advanced",
                                            sum_results=False)
    expected_gen_inv_cost = {
        # for each: capacity (kW) * regional multiplier * base technology cost
        3: 15e3 * 1.01701 * 1013.912846,
        5: 30e3 * 1.05221 * 4099.115851,
        6: 10e3 * 1.16979 * 1301.120135,
        7: 12e3 * 1.01701 * 1013.912846,
        8: 8e3 * 1.01701 * 1013.912846,
        9: 20e3 * 1.050755 * 1008.001936,
        10: 15e3 * 1.04348 * 1301.120135,
        11: 1000e3 * 1.07252 * 6928.866991,
        12: 100e3 * 1.012360 * 779,
        13: 200e3 * 1.043730 * 779,
    }
    inflation = calculate_inflation(2018)
    expected_gen_inv_cost = {
        k: v * inflation
        for k, v in expected_gen_inv_cost.items()
    }
    assert set(gen_inv_cost.index) == set(expected_gen_inv_cost.keys())
    for k in gen_inv_cost.index:
        assert gen_inv_cost.loc[k] == pytest.approx(expected_gen_inv_cost[k])
    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
Ejemplo n.º 9
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
Ejemplo n.º 10
0
def test_calculate_ac_inv_costs_transformers_only(mock_grid):
    expected_ac_cost = {
        # ((reg_mult1 + reg_mult2) / 2) * sum(basecost * rateA * miles)
        "line_cost":
        0,
        # for each: rateA * basecost * regional multiplier
        "transformer_cost":
        ((30 * 7670 * 1) + (40 * 8880 * 2.25)) * calculate_inflation(2020),
    }
    this_grid = copy.deepcopy(mock_grid)
    this_grid.branch = this_grid.branch.query(
        "branch_device_type == 'Transformer'")
    ac_cost = _calculate_ac_inv_costs(this_grid)
    assert ac_cost.keys() == expected_ac_cost.keys()
    for k in ac_cost.keys():
        assert ac_cost[k] == pytest.approx(expected_ac_cost[k])
Ejemplo n.º 11
0
def test_calculate_ac_inv_costs_lines_only(mock_grid):
    expected_ac_cost = {
        # ((reg_mult1 + reg_mult2) / 2) * sum(basecost * rateA * miles)
        "line_cost":
        (calculate_inflation(2010) *
         ((((1 + 2.25) / 2) *
           (3666.67 * 10 * 679.179925842 + 1500 * 1100 * 680.986501516)) +
          ((1 + 1) / 2) * 2333.33 * 50 * 20.003889808)),
        # for each: rateA * basecost * regional multiplier
        "transformer_cost":
        0,
    }
    this_grid = copy.deepcopy(mock_grid)
    this_grid.branch = this_grid.branch.query("branch_device_type == 'Line'")
    ac_cost = _calculate_ac_inv_costs(this_grid)
    assert ac_cost.keys() == expected_ac_cost.keys()
    for k in ac_cost.keys():
        assert ac_cost[k] == pytest.approx(expected_ac_cost[k])
Ejemplo n.º 12
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}
Ejemplo n.º 13
0
def _calculate_gen_inv_costs(grid_new, year, cost_case, sum_results=True):
    """Given a grid, calculate the total cost of building that generation investment.
    Computes total capital cost as CAPEX_total =
        CAPEX ($/MW) * Pmax (MW) * reg_cap_cost_mult (regional cost multiplier)
    This function is separate from calculate_gen_inv_costs() for testing purposes.
    Currently only uses one (arbutrary) sub-technology. Drops the rest of the costs.
        Will want to fix for wind/solar (based on resource supply curves).
    Currently uses ReEDS regions to find regional multipliers.

    :param powersimdata.input.grid.Grid grid_new: grid instance.
    :param int/str year: year of builds (used in financials).
    :param str cost_case: the ATB cost case of data:
        'Moderate': mid cost case
        'Conservative': generally higher costs
        'Advanced': generally lower costs
    :raises ValueError: if year not 2020 - 2050, or cost case not an allowed option.
    :raises TypeError: if year gets the wrong type, or if cost_case is not str.
    :return: (*pandas.Series*) -- Total generation investment cost,
        summed by technology.
    """
    def load_cost(year, cost_case):
        """
        Load in base costs from NREL's 2020 ATB for generation technologies (CAPEX).
            Can be adapted in the future for FOM, VOM, & CAPEX.
        This data is pulled from the ATB xlsx file Summary pages (saved as csv's).
        Therefore, currently uses default financials, but will want to create custom
            financial functions in the future.

        :param int/str year: year of cost projections.
        :param str cost_case: the ATB cost case of data
            (see :py:func:`write_poly_shapefile` for details).
        :return: (*pandas.DataFrame*) -- Cost by technology/subtype (in $2018).
        """
        cost = pd.read_csv(const.gen_inv_cost_path)
        cost = cost.dropna(axis=0, how="all")

        # drop non-useful columns
        cols_drop = cost.columns[~cost.columns.
                                 isin([str(x) for x in cost.columns[0:6]] +
                                      ["Metric", str(year)])]
        cost.drop(cols_drop, axis=1, inplace=True)

        # rename year of interest column
        cost.rename(columns={str(year): "value"}, inplace=True)

        # get rid of #refs
        cost.drop(cost[cost["value"] == "#REF!"].index, inplace=True)

        # get rid of $s, commas
        cost["value"] = cost["value"].str.replace("$", "", regex=True)
        cost["value"] = cost["value"].str.replace(",", "",
                                                  regex=True).astype("float64")
        # scale from $/kW to $/MW
        cost["value"] *= 1000

        cost.rename(columns={"value": "CAPEX"}, inplace=True)

        # select scenario of interest
        cost = cost[cost["CostCase"] == cost_case]
        cost.drop(["CostCase"], axis=1, inplace=True)

        return cost

    if isinstance(year, (int, str)):
        year = int(year)
        if year not in range(2020, 2051):
            raise ValueError("year not in range.")
    else:
        raise TypeError("year must be int or str.")

    if isinstance(cost_case, str):
        if cost_case not in ["Moderate", "Conservative", "Advanced"]:
            raise ValueError(
                "cost_case not Moderate, Conservative, or Advanced")
    else:
        raise TypeError("cost_case must be str.")

    plants = grid_new.plant.append(grid_new.storage["gen"])
    plants = plants[~plants.type.isin(
        ["dfo", "other"])]  # drop these technologies, no cost data

    # BASE TECHNOLOGY COST

    # load in investment costs $/MW
    gen_costs = load_cost(year, cost_case)
    # keep only certain (arbitrary) subclasses for now
    gen_costs = gen_costs[gen_costs["TechDetail"].isin(
        const.gen_inv_cost_techdetails_to_keep)]
    # rename techs to match grid object
    gen_costs.replace(const.gen_inv_cost_translation, inplace=True)
    gen_costs.drop(["Key", "FinancialCase", "CRPYears"], axis=1, inplace=True)
    # ATB technology costs merge
    plants = plants.merge(gen_costs,
                          right_on="Technology",
                          left_on="type",
                          how="left")

    # REGIONAL COST MULTIPLIER

    # Find ReEDS regions of plants (for regional cost multipliers)
    plant_buses = plants.bus_id.unique()
    try:
        bus_reg = pd.read_csv(const.bus_reeds_regions_path, index_col="bus_id")
        if not set(plant_buses) <= set(bus_reg.index):
            missing_buses = set(plant_buses) - set(bus_reg.index)
            bus_reg = bus_reg.append(
                bus_to_reeds_reg(grid_new.bus.loc[missing_buses]))
            bus_reg.sort_index().to_csv(const.bus_reeds_regions_path)
    except FileNotFoundError:
        bus_reg = bus_to_reeds_reg(grid_new.bus.loc[plant_buses])
        bus_reg.sort_index().to_csv(const.bus_reeds_regions_path)
    plants = plants.merge(bus_reg,
                          left_on="bus_id",
                          right_index=True,
                          how="left")

    # Determine one region 'r' for each plant, based on one of two mappings
    plants.loc[:, "r"] = ""
    # Some types get regional multipliers via 'wind regions' ('rs')
    wind_region_mask = plants["type"].isin(
        const.regional_multiplier_wind_region_types)
    plants.loc[wind_region_mask, "r"] = plants.loc[wind_region_mask, "rs"]
    # Other types get regional multipliers via 'BA regions' ('rb')
    ba_region_mask = plants["type"].isin(
        const.regional_multiplier_ba_region_types)
    plants.loc[ba_region_mask, "r"] = plants.loc[ba_region_mask, "rb"]
    plants.drop(["rs", "rb"], axis=1, inplace=True)

    # merge regional multipliers with plants
    region_multiplier = pd.read_csv(const.regional_multiplier_path)
    region_multiplier.replace(const.regional_multiplier_gen_translation,
                              inplace=True)
    plants = plants.merge(region_multiplier,
                          left_on=["r", "Technology"],
                          right_on=["r", "i"],
                          how="left")

    # multiply all together to get summed CAPEX ($)
    plants.loc[:, "CAPEX_total"] = (plants["CAPEX"] * plants["Pmax"] *
                                    plants["reg_cap_cost_mult"])

    # sum cost by technology
    plants.loc[:, "CAPEX_total"] *= calculate_inflation(2018)
    if sum_results:
        return plants.groupby(["Technology"])["CAPEX_total"].sum()
    else:
        return plants
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}
def _calculate_gen_inv_costs(grid_new, year, cost_case, sum_results=True):
    """Calculate cost of upgrading generators. ReEDS regions are used to find
    regional multipliers.

    :param powersimdata.input.grid.Grid grid_new: grid instance.
    :param int/str year: year of builds.
    :param str cost_case: ATB cost case of data. *'Moderate'*: mid cost case
        *'Conservative'*: generally higher costs, *'Advanced'*: generally lower costs.
    :raises ValueError: if year not 2020 - 2050, or cost case not an allowed option.
    :raises TypeError: if year not int/str or cost_case not str.
    :param bool sum_results: whether to sum data frame for plant costs. Defaults to
        True.
    :return: (*pandas.Series*) -- Overnight generation investment cost.
        If ``sum_results``, indices are technologies and values are total cost.
        Otherwise, indices are IDs of plants (including storage, which is given
        pseudo-plant-IDs), and values are individual generator costs.
        Whether summed or not, values are $USD, inflation-adjusted to today.

    .. note:: the function computes the total capital cost as:
        CAPEX_total = overnight CAPEX ($/MW) * Power capacity (MW) * regional multiplier
    """

    def load_cost(year, cost_case):
        """Load in base costs from NREL's 2020 ATB for generation technologies (CAPEX).

        :param int/str year: year of cost projections.
        :param str cost_case: ATB cost case of data (see
        :return: (*pandas.DataFrame*) -- cost by technology/subtype in $2018.

        .. todo:: it can be adapted in the future for FOM, VOM, & CAPEX. This data is
            pulled from the ATB xlsx file summary pages. Therefore, it currently uses
            default financials, but will want to create custom financial functions in
            the future.
        """
        cost = pd.read_csv(const.gen_inv_cost_path)
        cost = cost.dropna(axis=0, how="all")

        # drop non-useful columns
        cols_drop = cost.columns[
            ~cost.columns.isin(
                [str(x) for x in cost.columns[0:6]] + ["Metric", str(year)]
            )
        ]
        cost.drop(cols_drop, axis=1, inplace=True)

        # rename year of interest column
        cost.rename(columns={str(year): "value"}, inplace=True)

        # get rid of #refs
        cost.drop(cost[cost["value"] == "#REF!"].index, inplace=True)

        # get rid of $s, commas
        cost["value"] = cost["value"].str.replace("$", "", regex=True)
        cost["value"] = cost["value"].str.replace(",", "", regex=True).astype("float64")
        # scale from $/kW to $/MW
        cost["value"] *= 1000

        cost.rename(columns={"value": "CAPEX"}, inplace=True)

        # select scenario of interest
        if cost_case != "Moderate":
            # The 2020 ATB only has "Moderate" for nuclear, so we need to make due.
            warnings.warn(
                f"No cost data available for Nuclear for {cost_case} cost case, "
                "using Moderate cost case data instead"
            )
            new_nuclear = cost.query(
                "Technology == 'Nuclear' and CostCase == 'Moderate'"
            ).copy()
            new_nuclear.CostCase = cost_case
            cost = pd.concat([cost, new_nuclear], ignore_index=True)
        cost = cost[cost["CostCase"] == cost_case]
        cost.drop(["CostCase"], axis=1, inplace=True)

        return cost

    if isinstance(year, (int, str)):
        year = int(year)
        if year not in range(2020, 2051):
            raise ValueError("year not in range.")
    else:
        raise TypeError("year must be int or str.")

    if isinstance(cost_case, str):
        if cost_case not in ["Moderate", "Conservative", "Advanced"]:
            raise ValueError("cost_case not Moderate, Conservative, or Advanced")
    else:
        raise TypeError("cost_case must be str.")

    storage_plants = grid_new.storage["gen"].set_index(
        grid_new.storage["StorageData"].UnitIdx.astype(int)
    )
    plants = append_keep_index_name(grid_new.plant, storage_plants)
    plants = plants[
        ~plants.type.isin(["dfo", "other"])
    ]  # drop these technologies, no cost data

    # BASE TECHNOLOGY COST

    # load in investment costs $/MW
    gen_costs = load_cost(year, cost_case)
    # keep only certain (arbitrary) subclasses for now
    gen_costs = gen_costs[
        gen_costs["TechDetail"].isin(const.gen_inv_cost_techdetails_to_keep)
    ]
    # rename techs to match grid object
    gen_costs.replace(const.gen_inv_cost_translation, inplace=True)
    gen_costs.drop(["Key", "FinancialCase", "CRPYears"], axis=1, inplace=True)
    # ATB technology costs merge
    plants = merge_keep_index(
        plants, gen_costs, right_on="Technology", left_on="type", how="left"
    )

    # REGIONAL COST MULTIPLIER

    # Find ReEDS regions of plants (for regional cost multipliers)
    plant_buses = plants.bus_id.unique()
    try:
        bus_reg = pd.read_csv(const.bus_reeds_regions_path, index_col="bus_id")
        if not set(plant_buses) <= set(bus_reg.index):
            missing_buses = set(plant_buses) - set(bus_reg.index)
            bus_reg = bus_reg.append(bus_to_reeds_reg(grid_new.bus.loc[missing_buses]))
            bus_reg.sort_index().to_csv(const.bus_reeds_regions_path)
    except FileNotFoundError:
        bus_reg = bus_to_reeds_reg(grid_new.bus.loc[plant_buses])
        bus_reg.sort_index().to_csv(const.bus_reeds_regions_path)
    plants = merge_keep_index(
        plants, bus_reg, left_on="bus_id", right_index=True, how="left"
    )

    # Determine one region 'r' for each plant, based on one of two mappings
    plants.loc[:, "r"] = ""
    # Some types get regional multipliers via 'wind regions' ('rs')
    wind_region_mask = plants["type"].isin(const.regional_multiplier_wind_region_types)
    plants.loc[wind_region_mask, "r"] = plants.loc[wind_region_mask, "rs"]
    # Other types get regional multipliers via 'BA regions' ('rb')
    ba_region_mask = plants["type"].isin(const.regional_multiplier_ba_region_types)
    plants.loc[ba_region_mask, "r"] = plants.loc[ba_region_mask, "rb"]
    plants.drop(["rs", "rb"], axis=1, inplace=True)

    # merge regional multipliers with plants
    region_multiplier = pd.read_csv(const.regional_multiplier_path)
    region_multiplier.replace(const.regional_multiplier_gen_translation, inplace=True)
    plants = merge_keep_index(
        plants,
        region_multiplier,
        left_on=["r", "Technology"],
        right_on=["r", "i"],
        how="left",
    )

    # multiply all together to get summed CAPEX ($)
    plants.loc[:, "cost"] = (
        plants["CAPEX"] * plants["Pmax"] * plants["reg_cap_cost_mult"]
    )

    # sum cost by technology
    plants.loc[:, "cost"] *= calculate_inflation(2018)
    if sum_results:
        return plants.groupby(["Technology"])["cost"].sum()
    else:
        return plants["cost"]