diff --git a/CHANGELOG.rst b/CHANGELOG.rst index c155b9d6..75233c7c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,12 @@ Changelog ========= +Version 1.1.4 +============= +- Add result calculations for ``DiscountedCapitalInvestment``, ``DiscountedCostByTechnology``, and ``DiscountedOperationalCost`` +- Add result calculations for ``CapitalInvestmentStorage``, ``DiscountedCapitalInvestmentStorage``, ``DiscountedCostByStorage`` and ``DiscountedSalvageValueStorage`` +- Correct ``TotalDiscountedCost`` calculation to account for storage costs + Version 1.1.3 =========================== - Lock pandas to 2.1.4 or later diff --git a/src/otoole/preprocess/config.yaml b/src/otoole/preprocess/config.yaml index a915644d..ba1b9db7 100644 --- a/src/otoole/preprocess/config.yaml +++ b/src/otoole/preprocess/config.yaml @@ -332,6 +332,11 @@ AnnualVariableOperatingCost: type: result dtype: float default: 0 +CapitalInvestmentStorage: + indices: [REGION, STORAGE, YEAR] + type: result + dtype: float + default: 0 CapitalInvestment: indices: [REGION, TECHNOLOGY, YEAR] type: result @@ -342,11 +347,42 @@ Demand: type: result dtype: float default: 0 +DiscountedCapitalInvestmentStorage: + short_name: DiscountedCapitalInvestStorage + indices: [REGION, STORAGE, YEAR] + type: result + dtype: float + default: 0 +DiscountedCapitalInvestment: + indices: [REGION, TECHNOLOGY, YEAR] + type: result + dtype: float + default: 0 +DiscountedCostByStorage: + indices: [REGION, STORAGE, YEAR] + type: result + dtype: float + default: 0 +DiscountedCostByTechnology: + indices: [REGION, TECHNOLOGY, YEAR] + type: result + dtype: float + default: 0 +DiscountedOperationalCost: + indices: [REGION, TECHNOLOGY, YEAR] + type: result + dtype: float + default: 0 DiscountedSalvageValue: indices: [REGION, TECHNOLOGY, YEAR] type: result dtype: float default: 0 +DiscountedSalvageValueStorage: + indices: [REGION, STORAGE, YEAR] + type: result + dtype: float + default: 0 DiscountedTechnologyEmissionsPenalty: short_name: DiscountedTechEmissionsPenalty indices: [REGION, TECHNOLOGY, YEAR] diff --git a/src/otoole/results/result_package.py b/src/otoole/results/result_package.py index 8a70bdd0..acf1b56d 100644 --- a/src/otoole/results/result_package.py +++ b/src/otoole/results/result_package.py @@ -43,7 +43,14 @@ def __init__( "AnnualTechnologyEmissionByMode": self.annual_technology_emission_by_mode, "AnnualVariableOperatingCost": self.annual_variable_operating_cost, "CapitalInvestment": self.capital_investment, + "CapitalInvestmentStorage": self.capital_investment_storage, "Demand": self.demand, + "DiscountedCapitalInvestment": self.discounted_capital_investment, + "DiscountedCapitalInvestmentStorage": self.discounted_capital_investment_storage, + "DiscountedCostByStorage": self.discounted_storage_cost, + "DiscountedCostByTechnology": self.discounted_technology_cost, + "DiscountedOperationalCost": self.discounted_operational_cost, + "DiscountedSalvageValueStorage": self.discounted_salvage_value_storage, "DiscountedTechnologyEmissionsPenalty": self.discounted_tech_emis_pen, "ProductionByTechnology": self.production_by_technology, "ProductionByTechnologyAnnual": self.production_by_technology_annual, @@ -344,6 +351,35 @@ def capital_investment(self) -> pd.DataFrame: return data[(data != 0).all(1)] + def capital_investment_storage(self) -> pd.DataFrame: + """CapitalInvestmentStorage + + Notes + ----- + From the formulation:: + + r~REGION, s~STORAGE, y~YEAR, + CapitalCostStorage[r,s,y] * NewStorageCapacity[r,s,y] + ~VALUE; + """ + try: + capital_cost_storage = self["CapitalCostStorage"] + new_capacity_storage = self["NewStorageCapacity"] + + except KeyError as ex: + raise KeyError(self._msg("CapitalInvestmentStorage", str(ex))) + + capital_investment_storage = capital_cost_storage.mul( + new_capacity_storage, fill_value=0 + ) + + data = capital_investment_storage + + if not data.empty: + data = data.groupby(by=["REGION", "STORAGE", "YEAR"]).sum() + + return data[(data != 0).all(1)] + def demand(self) -> pd.DataFrame: """Demand @@ -398,6 +434,304 @@ def discounted_tech_emis_pen(self) -> pd.DataFrame: return data[(data != 0).all(1)] + def discounted_capital_investment(self) -> pd.DataFrame: + """DiscountingCapitalInvestment + + Notes + ----- + From the formulation:: + + r~REGION, t~TECHNOLOGY, y~YEAR, + DiscountingCapitalInvestment[r,t,y] := + CapitalCost[r,t,y] * NewCapacity[r,t,y] * CapitalRecoveryFactor[r,t] * PvAnnuity[r,t] / DiscountFactor[r,y] + + Alternatively, can be written as:: + + r~REGION, t~TECHNOLOGY, y~YEAR, + DiscountingCapitalInvestment[r,t,y] := UndiscountedCapitalInvestment[r,t,y] / DiscountFactor[r,y] + + """ + + try: + discount_rate = self["DiscountRate"] + year_df = self["YEAR"].copy(deep=True) + region_df = self["REGION"].copy(deep=True) + + years = year_df["VALUE"].tolist() + regions = region_df["VALUE"].tolist() + capital_investment = self["CapitalInvestment"] + + except KeyError as ex: + raise KeyError(self._msg("DiscountedCapitalInvestment", str(ex))) + + df = discount_factor(regions, years, discount_rate, 0.0) + + data = capital_investment.div(df, fill_value=0.0) + + if not data.empty: + data = data.groupby(by=["REGION", "TECHNOLOGY", "YEAR"]).sum() + + return data[(data != 0).all(1)] + + def discounted_capital_investment_storage(self) -> pd.DataFrame: + """DiscountedCapitalInvestmentStorage + + Notes + ----- + From the formulation:: + + r~REGION, s~STORAGE, y~YEAR, + DiscountedCapitalInvestmentStorage[r,s,y] := + CapitalCostStorage[r,s,y] * NewCapacity[r,t,y] / DiscountFactor[r,y] + + Alternatively, can be written as:: + + r~REGION, s~STORAGE, y~YEAR, + DiscountedCapitalInvestmentStorage[r,s,y] := UndiscountedCapitalInvestmentStorage[r,s,y] / DiscountFactor[r,y] + + """ + + try: + discount_rate_storage = self["DiscountRateStorage"] + year_df = self["YEAR"].copy(deep=True) + region_df = self["REGION"].copy(deep=True) + + years = year_df["VALUE"].tolist() + regions = region_df["VALUE"].tolist() + capital_investment_storage = self["CapitalInvestmentStorage"] + + storages = self.get_unique_values_from_index( + [ + capital_investment_storage, + ], + "STORAGE", + ) + + except KeyError as ex: + raise KeyError(self._msg("DiscountedCapitalInvestmentStorage", str(ex))) + + dfs = discount_factor_storage( + regions, storages, years, discount_rate_storage, 0.0 + ) + + data = capital_investment_storage.div(dfs, fill_value=0.0) + + if not data.empty: + data = data.groupby(by=["REGION", "STORAGE", "YEAR"]).sum() + + return data[(data != 0).all(1)] + + def discounted_operational_cost(self) -> pd.DataFrame: + """DiscountedOperationalCosts + + Notes + ----- + From the formulation:: + + r~REGION, t~TECHNOLOGY, y~YEAR, + DiscountedOperatingCost[r,t,y] := + ( + ( + ( + sum{yy in YEAR: y-yy < OperationalLife[r,t] && y-yy>=0} + NewCapacity[r,t,yy] + ) + + ResidualCapacity[r,t,y] + ) + * FixedCost[r,t,y] + + sum{l in TIMESLICE, m in MODEperTECHNOLOGY[t]} + RateOfActivity[r,l,t,m,y] * YearSplit[l,y] * VariableCost[r,t,m,y] + ) + / (DiscountFactorMid[r,y]) + + Alternatively, can be written as:: + + r~REGION, t~TECHNOLOGY, y~YEAR, + DiscountedOperatingCost[r,t,y] := + ( + AnnualVariableOperatingCost[r,t,y] + AnnualFixedOperatingCost[r,t,y] + ) + / DiscountFactorMid[r, y] + + OR + + r~REGION, t~TECHNOLOGY, y~YEAR, + DiscountedOperatingCost[r,t,y] := OperatingCost[r,t,y] / DiscountFactorMid[r, y] + + """ + + try: + discount_rate = self["DiscountRate"] + year_df = self["YEAR"].copy(deep=True) + region_df = self["REGION"].copy(deep=True) + + years = year_df["VALUE"].tolist() + regions = region_df["VALUE"].tolist() + + annual_fixed_operating_cost = self["AnnualFixedOperatingCost"] + annual_variable_operating_cost = self["AnnualVariableOperatingCost"] + + except KeyError as ex: + raise KeyError(self._msg("DiscountedOperationalCost", str(ex))) + + df_mid = discount_factor(regions, years, discount_rate, 0.5) + + undiscounted_operational_costs = annual_fixed_operating_cost.add( + annual_variable_operating_cost, fill_value=0.0 + ) + + discounted_operational_costs = undiscounted_operational_costs.div( + df_mid, fill_value=0.0 + ) + + data = discounted_operational_costs + + if not data.empty: + data = data.groupby(by=["REGION", "TECHNOLOGY", "YEAR"]).sum() + + return data[(data != 0).all(1)] + + def discounted_storage_cost(self) -> pd.DataFrame: + """TotalDiscountedCostByStorage + + Notes + ----- + From the formulation:: + + r~REGION, s~STORAGE, y~YEAR, + TotalDiscountedStorageCost[r,s,y]:= + ( + CapitalCostStorage[r,s,y] * NewStorageCapacity[r,s,y] / DiscountFactorStorage[r,s,y] - + SalvageValueStorage[r,s,y] / + ( + (1+DiscountRateStorage[r,s])^(max{yy in YEAR} max(yy)-min{yy in YEAR} min(yy)+1)) + ) + ) + + Alternatively, can be written as:: + + r~REGION, s~STORAGE, y~YEAR, + TotalDiscountedStorageCost[r,s,y]:= + DiscountedCapitalInvestmentStorage[r,s,y] - DiscountedSalvageValueStorage[r,s,y] + """ + + try: + discounted_capital_investment_storage = self[ + "DiscountedCapitalInvestmentStorage" + ] + discounted_salvage_value_storage = self["DiscountedSalvageValueStorage"] + + except KeyError as ex: + raise KeyError(self._msg("TotalDiscountedCostByStorage", str(ex))) + + discounted_storage_costs = discounted_capital_investment_storage.sub( + discounted_salvage_value_storage, fill_value=0.0 + ) + + data = discounted_storage_costs + + if not data.empty: + data = data.groupby(by=["REGION", "STORAGE", "YEAR"]).sum() + return data[(data != 0).all(1)] + + def discounted_salvage_value_storage(self) -> pd.DataFrame: + """DiscountedSalvageValueStorage + + Notes + ----- + From the formulation:: + + DiscountedSalvageValueStorage[r,s,y] = SalvageValueStorage[r,s,y] / ((1+DiscountRateStorage[r,s])^(max{yy in YEAR} max(yy)-min{yy in YEAR} min(yy)+1))) + """ + + try: + salvage_value_storage = self["SalvageValueStorage"] + discount_rate_storage = self["DiscountRateStorage"] + year_df = self["YEAR"].copy(deep=True) + region_df = self["REGION"].copy(deep=True) + storage_df = self["STORAGE"].copy(deep=True) + + years = year_df["VALUE"].tolist() + regions = region_df["VALUE"].tolist() + storages = storage_df["VALUE"].tolist() + + except KeyError as ex: + raise KeyError(self._msg("DiscountedSalvageValueStorage", str(ex))) + + df_storage_salvage = discount_factor_storage_salvage( + regions, storages, years, discount_rate_storage + ) + + discounted_salvage_value_storage = salvage_value_storage.div( + df_storage_salvage, fill_value=0 + ) + + data = discounted_salvage_value_storage + + if not data.empty: + data = data.groupby(by=["REGION", "STORAGE", "YEAR"]).sum() + + return data[(data != 0).all(1)] + + def discounted_technology_cost(self) -> pd.DataFrame: + """TotalDiscountedCostByTechnology + + Notes + ----- + From the formulation:: + + r~REGION, t~TECHNOLOGY, y~YEAR, + TotalDiscountedCostByTechnology[r,t,y]:= + ( + ( + ( + sum{yy in YEAR: y-yy < OperationalLife[r,t] && y-yy>=0} + NewCapacity[r,t,yy] + ) + + ResidualCapacity[r,t,y] + ) + * FixedCost[r,t,y] + + sum{l in TIMESLICE, m in MODEperTECHNOLOGY[t]} + RateOfActivity[r,l,t,m,y] * YearSplit[l,y] * VariableCost[r,t,m,y] + ) + / (DiscountFactorMid[r,y]) + + CapitalCost[r,t,y] * NewCapacity[r,t,y] * CapitalRecoveryFactor[r,t] * PvAnnuity[r,t] / (DiscountFactor[r,y]) + + DiscountedTechnologyEmissionsPenalty[r,t,y] - DiscountedSalvageValue[r,t,y]) + + Alternatively, can be written as:: + + r~REGION, t~TECHNOLOGY, y~YEAR, + TotalDiscountedCostByTechnology[r,t,y]: = + DiscountedOperatingCost[r,t,y] + DiscountedCapitalInvestment[r,t,y] + DiscountedTechnologyEmissionsPenalty[r,t,y] - DiscountedSalvageValue[r,t,y] + """ + + try: + discounted_capital_costs = self["DiscountedCapitalInvestment"] + discounted_operational_costs = self["DiscountedOperationalCost"] + discounted_emissions_penalty = self["DiscountedTechnologyEmissionsPenalty"] + discounted_salvage_value = self["DiscountedSalvageValue"] + + except KeyError as ex: + raise KeyError(self._msg("DiscountedCostByTechnology", str(ex))) + + discounted_total_costs = discounted_operational_costs.add( + discounted_capital_costs, fill_value=0.0 + ) + + discounted_total_costs = discounted_total_costs.add( + discounted_emissions_penalty, fill_value=0.0 + ) + + discounted_total_costs = discounted_total_costs.sub( + discounted_salvage_value, fill_value=0.0 + ) + + data = discounted_total_costs + + if not data.empty: + data = data.groupby(by=["REGION", "TECHNOLOGY", "YEAR"]).sum() + return data[(data != 0).all(1)] + def production_by_technology(self) -> pd.DataFrame: """ProductionByTechnology @@ -603,59 +937,52 @@ def total_discounted_cost(self) -> pd.DataFrame: / (DiscountFactorMid[r,y]) + CapitalCost[r,t,y] * NewCapacity[r,t,y] * CapitalRecoveryFactor[r,t] * PvAnnuity[r,t] / (DiscountFactor[r,y]) + DiscountedTechnologyEmissionsPenalty[r,t,y] - DiscountedSalvageValue[r,t,y]) - + sum{s in STORAGE} + + sum{r in REGION, s in STORAGE, y in YEAR} ( - CapitalCostStorage[r,s,y] * NewStorageCapacity[r,s,y] / (DiscountFactorStorage[r,s,y]) - - CapitalCostStorage[r,s,y] * NewStorageCapacity[r,s,y] / (DiscountFactorStorage[r,s,y] + CapitalCostStorage[r,s,y] * NewStorageCapacity[r,s,y] / (DiscountFactorStorage[r,s,y] + - SalvageValueStorage[r,s,y] / ((1+DiscountRateStorage[r,s])^(max{yy in YEAR} max(yy)-min{yy in YEAR} min(yy)+1)) ) ) ~VALUE; - """ - try: - discount_rate = self["DiscountRate"] - year_df = self["YEAR"].copy(deep=True) - region_df = self["REGION"].copy(deep=True) - - years = year_df["VALUE"].tolist() - regions = region_df["VALUE"].tolist() - annual_fixed_operating_cost = self["AnnualFixedOperatingCost"] - annual_variable_operating_cost = self["AnnualVariableOperatingCost"] - capital_investment = self["CapitalInvestment"] + Alternatively, can be written as:: - discounted_emissions_penalty = self["DiscountedTechnologyEmissionsPenalty"] - discounted_salvage_value = self["DiscountedSalvageValue"] - - # capital_cost_storage = self["CapitalCostStorage"] + r~REGION, y~YEAR, + TotalDiscountedCost[r,y] := + sum{t in TECHNOLOGY} TotalDiscountedCostByTechnology[r,t,y] + sum{s in STORAGE} TotalDiscountedStorageCost[r,s,y] + """ + try: + discounted_cost_by_technology = self["DiscountedCostByTechnology"] except KeyError as ex: raise KeyError(self._msg("TotalDiscountedCost", str(ex))) - df_start = discount_factor(regions, years, discount_rate, 0.0) - - df_mid = discount_factor(regions, years, discount_rate, 0.5) - - undiscounted_operational_costs = annual_fixed_operating_cost.add( - annual_variable_operating_cost, fill_value=0.0 + discounted_tech = ( + discounted_cost_by_technology.droplevel("TECHNOLOGY") + .reset_index() + .groupby(["REGION", "YEAR"]) + .sum() ) - discounted_operational_costs = undiscounted_operational_costs.div( - df_mid, fill_value=0.0 - ) - - discounted_capital_costs = capital_investment.div(df_start, fill_value=0.0) + try: + discounted_cost_by_storage = self["DiscountedCostByStorage"] - discounted_total_costs = discounted_operational_costs.add( - discounted_capital_costs, fill_value=0.0 - ) + discounted_storage = ( + discounted_cost_by_storage.droplevel("STORAGE") + .reset_index() + .groupby(["REGION", "YEAR"]) + .sum() + ) + except KeyError as ex: # storage not always included + LOGGER.debug(ex) - discounted_total_costs = discounted_total_costs.add( - discounted_emissions_penalty, fill_value=0.0 - ) + discounted_storage = pd.DataFrame( + columns=["REGION", "YEAR", "VALUE"] + ).set_index(["REGION", "YEAR"]) - discounted_total_costs = discounted_total_costs.sub( - discounted_salvage_value, fill_value=0.0 - ) + total_discounted_cost = discounted_tech.add( + discounted_storage, fill_value=0 + ).astype(float) - data = discounted_total_costs + data = total_discounted_cost if not data.empty: data = data.groupby(by=["REGION", "YEAR"]).sum() @@ -958,3 +1285,51 @@ def discount_factor_storage( return pd.DataFrame( [], columns=["REGION", "STORAGE", "YEAR", "VALUE"] ).set_index(["REGION", "STORAGE", "YEAR"]) + + +def discount_factor_storage_salvage( + regions: List, + storages: List, + years: List, + discount_rate_storage: pd.DataFrame, +) -> pd.DataFrame: + """Discount Factor used for salvage value claculations + + Arguments + --------- + regions: list + storages: list + years: list + discount_rate_storage: pd.DataFrame + + Notes + ----- + From the formulation:: + + ((1+DiscountRateStorage[r,s])^(1+max{yy in YEAR} max(yy)-min{yy in YEAR} min(yy))); + """ + + if discount_rate_storage.empty: + raise ValueError( + "Cannot calculate discount_factor_storage_salvage due to missing discount rate" + ) + + if regions and years: + index = pd.MultiIndex.from_product( + [regions, storages, years], names=["REGION", "STORAGE", "YEAR"] + ) + discount_fac_storage_salv = discount_rate_storage.reindex(index) + + max_year = max(years) + min_year = min(years) + + discount_fac_storage_salv["VALUE"] = (1 + discount_fac_storage_salv).pow( + 1 + max_year - min_year + ) + + return discount_fac_storage_salv + + else: + return pd.DataFrame( + [], columns=["REGION", "STORAGE", "YEAR", "VALUE"] + ).set_index(["REGION", "STORAGE", "YEAR"]) diff --git a/tests/results/test_results_package.py b/tests/results/test_results_package.py index a597d4e6..8b90d5c7 100644 --- a/tests/results/test_results_package.py +++ b/tests/results/test_results_package.py @@ -7,6 +7,7 @@ capital_recovery_factor, discount_factor, discount_factor_storage, + discount_factor_storage_salvage, pv_annuity, ) @@ -126,6 +127,11 @@ def region(): return pd.DataFrame(data=["SIMPLICITY"], columns=["VALUE"]) +@fixture +def storage(): + return pd.DataFrame(data=["DAM"], columns=["VALUE"]) + + @fixture def accumulated_new_capacity(): data = pd.DataFrame( @@ -227,6 +233,229 @@ def variable_cost(): return data +@fixture +def undiscounted_capital_investment(): + data = pd.DataFrame( + data=[ + ["SIMPLICITY", "DUMMY", 2014, 10], + ["SIMPLICITY", "DUMMY", 2015, 0], + ["SIMPLICITY", "GAS_EXTRACTION", 2014, 123], + ["SIMPLICITY", "GAS_EXTRACTION", 2015, 456], + ["SIMPLICITY", "GAS_EXTRACTION", 2016, 789], + ], + columns=["REGION", "TECHNOLOGY", "YEAR", "VALUE"], + ).set_index(["REGION", "TECHNOLOGY", "YEAR"]) + return data + + +@fixture +def annual_fixed_operating_cost(): + data = pd.DataFrame( + data=[ + ["SIMPLICITY", "DUMMY", 2014, 10], + ["SIMPLICITY", "DUMMY", 2015, 0], + ["SIMPLICITY", "DUMMY", 2016, 10], + ["SIMPLICITY", "GAS_EXTRACTION", 2014, 123], + ["SIMPLICITY", "GAS_EXTRACTION", 2015, 456], + ["SIMPLICITY", "GAS_EXTRACTION", 2016, 789], + ], + columns=["REGION", "TECHNOLOGY", "YEAR", "VALUE"], + ).set_index(["REGION", "TECHNOLOGY", "YEAR"]) + return data + + +@fixture +def annual_variable_operating_cost(): + data = pd.DataFrame( + data=[ + ["SIMPLICITY", "DUMMY", 2014, 10], + ["SIMPLICITY", "DUMMY", 2015, 10], + ["SIMPLICITY", "DUMMY", 2016, 0], + ["SIMPLICITY", "GAS_EXTRACTION", 2014, 321], + ["SIMPLICITY", "GAS_EXTRACTION", 2015, 654], + ["SIMPLICITY", "GAS_EXTRACTION", 2016, 987], + ], + columns=["REGION", "TECHNOLOGY", "YEAR", "VALUE"], + ).set_index(["REGION", "TECHNOLOGY", "YEAR"]) + return data + + +@fixture +def discounted_capital_costs(): + data = pd.DataFrame( + data=[ + ["SIMPLICITY", "DUMMY", 2014, 10], + ["SIMPLICITY", "DUMMY", 2015, 0], + ["SIMPLICITY", "GAS_EXTRACTION", 2014, 111], + ["SIMPLICITY", "GAS_EXTRACTION", 2015, 222], + ["SIMPLICITY", "GAS_EXTRACTION", 2016, 333], + ], + columns=["REGION", "TECHNOLOGY", "YEAR", "VALUE"], + ).set_index(["REGION", "TECHNOLOGY", "YEAR"]) + return data + + +@fixture +def discounted_operational_costs(): + data = pd.DataFrame( + data=[ + ["SIMPLICITY", "DUMMY", 2014, 5], + ["SIMPLICITY", "DUMMY", 2015, 10], + ["SIMPLICITY", "DUMMY", 2016, 20], + ["SIMPLICITY", "GAS_EXTRACTION", 2014, 444], + ["SIMPLICITY", "GAS_EXTRACTION", 2015, 555], + ["SIMPLICITY", "GAS_EXTRACTION", 2016, 666], + ], + columns=["REGION", "TECHNOLOGY", "YEAR", "VALUE"], + ).set_index(["REGION", "TECHNOLOGY", "YEAR"]) + return data + + +@fixture +def discounted_emissions_penalty(): + data = pd.DataFrame( + data=[ + ["SIMPLICITY", "DUMMY", 2014, 10], + ["SIMPLICITY", "GAS_EXTRACTION", 2016, 777], + ], + columns=["REGION", "TECHNOLOGY", "YEAR", "VALUE"], + ).set_index(["REGION", "TECHNOLOGY", "YEAR"]) + return data + + +@fixture +def discounted_salvage_value(): + data = pd.DataFrame( + data=[ + ["SIMPLICITY", "DUMMY", 2014, 1], + ["SIMPLICITY", "DUMMY", 2015, 2], + ["SIMPLICITY", "DUMMY", 2016, 3], + ["SIMPLICITY", "GAS_EXTRACTION", 2014, 888], + ["SIMPLICITY", "GAS_EXTRACTION", 2015, 999], + ["SIMPLICITY", "GAS_EXTRACTION", 2016, 1], + ], + columns=["REGION", "TECHNOLOGY", "YEAR", "VALUE"], + ).set_index(["REGION", "TECHNOLOGY", "YEAR"]) + return data + + +@fixture +def discounted_technology_cost(): + data = pd.DataFrame( + data=[ + ["SIMPLICITY", "DUMMY", 2014, 111], + ["SIMPLICITY", "DUMMY", 2015, 222], + ["SIMPLICITY", "DUMMY", 2016, 333], + ["SIMPLICITY", "GAS_EXTRACTION", 2014, 444], + ["SIMPLICITY", "GAS_EXTRACTION", 2015, 555], + ["SIMPLICITY", "GAS_EXTRACTION", 2016, 666], + ], + columns=["REGION", "TECHNOLOGY", "YEAR", "VALUE"], + ).set_index(["REGION", "TECHNOLOGY", "YEAR"]) + return data + + +@fixture +def capital_cost_storage(): + df = pd.DataFrame( + data=[ + ["SIMPLICITY", "DAM", 2014, 1.23], + ["SIMPLICITY", "DAM", 2015, 2.34], + ["SIMPLICITY", "DAM", 2016, 3.45], + ["SIMPLICITY", "BATTERY", 2014, 4.56], + ["SIMPLICITY", "BATTERY", 2015, 5.67], + ["SIMPLICITY", "BATTERY", 2016, 6.78], + ], + columns=["REGION", "STORAGE", "YEAR", "VALUE"], + ).set_index(["REGION", "STORAGE", "YEAR"]) + return df + + +@fixture +def new_storage_capacity(): + df = pd.DataFrame( + data=[ + ["SIMPLICITY", "DAM", 2014, 1.3], + ["SIMPLICITY", "DAM", 2016, 1.6], + ["SIMPLICITY", "BATTERY", 2014, 0.9], + ], + columns=["REGION", "STORAGE", "YEAR", "VALUE"], + ).set_index(["REGION", "STORAGE", "YEAR"]) + return df + + +@fixture +def undiscounted_capital_investment_storage(): + data = pd.DataFrame( + data=[ + ["SIMPLICITY", "DAM", 2014, 1.23], + ["SIMPLICITY", "DAM", 2015, 2.34], + ], + columns=["REGION", "STORAGE", "YEAR", "VALUE"], + ).set_index(["REGION", "STORAGE", "YEAR"]) + return data + + +@fixture +def salvage_value_storage(): + data = pd.DataFrame( + data=[ + ["SIMPLICITY", "DAM", 2014, 1.23], + ["SIMPLICITY", "DAM", 2015, 2.34], + ["SIMPLICITY", "DAM", 2016, 3.45], + ], + columns=["REGION", "STORAGE", "YEAR", "VALUE"], + ).set_index(["REGION", "STORAGE", "YEAR"]) + return data + + +@fixture +def discounted_capital_costs_storage(): + data = pd.DataFrame( + data=[ + ["SIMPLICITY", "BATTERY", 2014, 11.1], + ["SIMPLICITY", "BATTERY", 2016, 22.2], + ["SIMPLICITY", "DAM", 2014, 33.3], + ["SIMPLICITY", "DAM", 2015, 44.4], + ["SIMPLICITY", "DAM", 2016, 55.5], + ], + columns=["REGION", "STORAGE", "YEAR", "VALUE"], + ).set_index(["REGION", "STORAGE", "YEAR"]) + return data + + +@fixture +def discounted_salvage_value_storage(): + data = pd.DataFrame( + data=[ + ["SIMPLICITY", "DAM", 2014, 1.23], + ["SIMPLICITY", "DAM", 2015, 2.34], + ["SIMPLICITY", "DAM", 2016, 3.45], + ["SIMPLICITY", "BATTERY", 2014, 4.56], + ["SIMPLICITY", "BATTERY", 2015, 5.67], + ["SIMPLICITY", "BATTERY", 2016, 6.78], + ], + columns=["REGION", "STORAGE", "YEAR", "VALUE"], + ).set_index(["REGION", "STORAGE", "YEAR"]) + return data + + +@fixture +def discounted_storage_cost(): + data = pd.DataFrame( + data=[ + ["SIMPLICITY", "DAM", 2014, 11.1], + ["SIMPLICITY", "DAM", 2015, 22.2], + ["SIMPLICITY", "DAM", 2016, 33.3], + ["SIMPLICITY", "BATTERY", 2014, 44.4], + ["SIMPLICITY", "BATTERY", 2015, 55.5], + ["SIMPLICITY", "BATTERY", 2016, 66.6], + ], + columns=["REGION", "STORAGE", "YEAR", "VALUE"], + ).set_index(["REGION", "STORAGE", "YEAR"]) + return data + + @fixture(scope="function") def null() -> ResultsPackage: package = ResultsPackage({}) @@ -738,6 +967,343 @@ def test_calculate_captital_investment_no_dr_idv( assert_frame_equal(actual, expected) + def test_null(self, null: ResultsPackage): + """ """ + package = null + with raises(KeyError) as ex: + package.capital_investment() + assert "Cannot calculate CapitalInvestment due to missing data" in str(ex) + + +class TestCapitalInvestmentStorage: + def test_capital_investment_storage( + self, region, year, capital_cost_storage, new_storage_capacity + ): + + results = { + "REGION": region, + "YEAR": year, + "CapitalCostStorage": capital_cost_storage, + "NewStorageCapacity": new_storage_capacity, + } + + package = ResultsPackage(results) + actual = package.capital_investment_storage() + expected = pd.DataFrame( + data=[ + ["SIMPLICITY", "BATTERY", 2014, 4.104], + ["SIMPLICITY", "DAM", 2014, 1.599], + ["SIMPLICITY", "DAM", 2016, 5.52], + ], + columns=["REGION", "STORAGE", "YEAR", "VALUE"], + ).set_index(["REGION", "STORAGE", "YEAR"]) + + assert_frame_equal(actual, expected) + + def test_null(self, null: ResultsPackage): + """ """ + package = null + with raises(KeyError) as ex: + package.capital_investment_storage() + assert "Cannot calculate CapitalInvestmentStorage due to missing data" in str( + ex + ) + + +class TestDiscountedCapitalInvestment: + def test_calculate_discounted_captital_investment( + self, + region, + year, + undiscounted_capital_investment, + discount_rate, + ): + + results = { + "REGION": region, + "YEAR": year, + "DiscountRate": discount_rate, + "CapitalInvestment": undiscounted_capital_investment, + } + + package = ResultsPackage(results) + actual = package.discounted_capital_investment() + expected = pd.DataFrame( + data=[ + ["SIMPLICITY", "DUMMY", 2014, 10], + ["SIMPLICITY", "GAS_EXTRACTION", 2014, 123], + ["SIMPLICITY", "GAS_EXTRACTION", 2015, 434.28571428], + ["SIMPLICITY", "GAS_EXTRACTION", 2016, 715.64625850], + ], + columns=["REGION", "TECHNOLOGY", "YEAR", "VALUE"], + ).set_index(["REGION", "TECHNOLOGY", "YEAR"]) + + assert_frame_equal(actual, expected) + + def test_null(self, null: ResultsPackage): + """ """ + package = null + with raises(KeyError) as ex: + package.discounted_capital_investment() + assert ( + "Cannot calculate DiscountedCapitalInvestment due to missing data" + in str(ex) + ) + + +class TestDiscountedCapitalInvestmentStorage: + def test_calculate_discounted_captital_investment_storage( + self, + region, + year, + undiscounted_capital_investment_storage, + discount_rate_storage, + ): + + results = { + "REGION": region, + "YEAR": year, + "DiscountRateStorage": discount_rate_storage, + "CapitalInvestmentStorage": undiscounted_capital_investment_storage, + } + + package = ResultsPackage(results) + actual = package.discounted_capital_investment_storage() + expected = pd.DataFrame( + data=[ + ["SIMPLICITY", "DAM", 2014, 1.23], + ["SIMPLICITY", "DAM", 2015, 2.22857143], + ], + columns=["REGION", "STORAGE", "YEAR", "VALUE"], + ).set_index(["REGION", "STORAGE", "YEAR"]) + + assert_frame_equal(actual, expected) + + def test_null(self, null: ResultsPackage): + """ """ + package = null + with raises(KeyError) as ex: + package.discounted_capital_investment_storage() + assert ( + "Cannot calculate DiscountedCapitalInvestmentStorage due to missing data" + in str(ex) + ) + + +class TestDiscountedOperationalCost: + def test_calculate_discounted_operational_cost( + self, + region, + year, + discount_rate, + annual_fixed_operating_cost, + annual_variable_operating_cost, + ): + + results = { + "REGION": region, + "YEAR": year, + "DiscountRate": discount_rate, + "AnnualFixedOperatingCost": annual_fixed_operating_cost, + "AnnualVariableOperatingCost": annual_variable_operating_cost, + } + + package = ResultsPackage(results) + actual = package.discounted_operational_cost() + expected = pd.DataFrame( + data=[ + ["SIMPLICITY", "DUMMY", 2014, 19.51800146], + ["SIMPLICITY", "DUMMY", 2015, 9.29428640], + ["SIMPLICITY", "DUMMY", 2016, 8.85170134], + ["SIMPLICITY", "GAS_EXTRACTION", 2014, 433.29963238], + ["SIMPLICITY", "GAS_EXTRACTION", 2015, 1031.66579140], + ["SIMPLICITY", "GAS_EXTRACTION", 2016, 1572.06215832], + ], + columns=["REGION", "TECHNOLOGY", "YEAR", "VALUE"], + ).set_index(["REGION", "TECHNOLOGY", "YEAR"]) + + assert_frame_equal(actual, expected) + + def test_null(self, null: ResultsPackage): + """ """ + package = null + with raises(KeyError) as ex: + package.discounted_operational_cost() + assert "Cannot calculate DiscountedOperationalCost due to missing data" in str( + ex + ) + + +class TestDiscountedCostByTechnology: + def test_calculate_discounted_cost_by_technology( + self, + discounted_capital_costs, + discounted_operational_costs, + discounted_emissions_penalty, + discounted_salvage_value, + ): + + results = { + "DiscountedCapitalInvestment": discounted_capital_costs, + "DiscountedOperationalCost": discounted_operational_costs, + "DiscountedTechnologyEmissionsPenalty": discounted_emissions_penalty, + "DiscountedSalvageValue": discounted_salvage_value, + } + + package = ResultsPackage(results) + actual = package.discounted_technology_cost() + expected = pd.DataFrame( + data=[ + ["SIMPLICITY", "DUMMY", 2014, 24.0], + ["SIMPLICITY", "DUMMY", 2015, 8.0], + ["SIMPLICITY", "DUMMY", 2016, 17.0], + ["SIMPLICITY", "GAS_EXTRACTION", 2014, -333.0], + ["SIMPLICITY", "GAS_EXTRACTION", 2015, -222.0], + ["SIMPLICITY", "GAS_EXTRACTION", 2016, 1775.0], + ], + columns=["REGION", "TECHNOLOGY", "YEAR", "VALUE"], + ).set_index(["REGION", "TECHNOLOGY", "YEAR"]) + + assert_frame_equal(actual, expected) + + def test_null(self, null: ResultsPackage): + """ """ + package = null + with raises(KeyError) as ex: + package.discounted_technology_cost() + assert "Cannot calculate DiscountedCostByTechnology due to missing data" in str( + ex + ) + + +class TestDiscountedCostByStorage: + def test_calculate_discounted_cost_by_storage( + self, + discounted_capital_costs_storage, + discounted_salvage_value_storage, + ): + + results = { + "DiscountedCapitalInvestmentStorage": discounted_capital_costs_storage, + "DiscountedSalvageValueStorage": discounted_salvage_value_storage, + } + + package = ResultsPackage(results) + actual = package.discounted_storage_cost() + expected = pd.DataFrame( + data=[ + ["SIMPLICITY", "BATTERY", 2014, 6.54], + ["SIMPLICITY", "BATTERY", 2015, -5.67], + ["SIMPLICITY", "BATTERY", 2016, 15.42], + ["SIMPLICITY", "DAM", 2014, 32.07], + ["SIMPLICITY", "DAM", 2015, 42.06], + ["SIMPLICITY", "DAM", 2016, 52.05], + ], + columns=["REGION", "STORAGE", "YEAR", "VALUE"], + ).set_index(["REGION", "STORAGE", "YEAR"]) + + assert_frame_equal(actual, expected) + + def test_null(self, null: ResultsPackage): + """ """ + package = null + with raises(KeyError) as ex: + package.discounted_storage_cost() + assert ( + "Cannot calculate TotalDiscountedCostByStorage due to missing data" + in str(ex) + ) + + +class TestTotalDiscountedCost: + def test_calculate_total_discounted_cost( + self, discounted_technology_cost, discounted_storage_cost + ): + + results = { + "DiscountedCostByTechnology": discounted_technology_cost, + "DiscountedCostByStorage": discounted_storage_cost, + } + + package = ResultsPackage(results) + actual = package.total_discounted_cost() + expected = pd.DataFrame( + data=[ + ["SIMPLICITY", 2014, 610.5], + ["SIMPLICITY", 2015, 854.7], + ["SIMPLICITY", 2016, 1098.9], + ], + columns=["REGION", "YEAR", "VALUE"], + ).set_index(["REGION", "YEAR"]) + + assert_frame_equal(actual, expected) + + def test_calculate_total_discounted_cost_no_storage( + self, discounted_technology_cost + ): + """Situations where NewStorageCapacity not available""" + + results = { + "DiscountedCostByTechnology": discounted_technology_cost, + } + + package = ResultsPackage(results) + actual = package.total_discounted_cost() + expected = pd.DataFrame( + data=[ + ["SIMPLICITY", 2014, 555.0], + ["SIMPLICITY", 2015, 777.0], + ["SIMPLICITY", 2016, 999.0], + ], + columns=["REGION", "YEAR", "VALUE"], + ).set_index(["REGION", "YEAR"]) + + assert_frame_equal(actual, expected) + + def test_null(self, null: ResultsPackage): + """ """ + package = null + with raises(KeyError) as ex: + package.total_discounted_cost() + assert "Cannot calculate TotalDiscountedCost due to missing data" in str(ex) + + +class TestDiscountedSalvageValueStorage: + def test_calculate_discounted_salvage_value_storage( + self, region, year, storage, salvage_value_storage, discount_rate_storage + ): + + results = { + "REGION": region, + "YEAR": year, + "STORAGE": storage, + "DiscountRateStorage": discount_rate_storage, + "SalvageValueStorage": salvage_value_storage, + } + + package = ResultsPackage(results) + actual = package.discounted_salvage_value_storage() + expected = pd.DataFrame( + data=[ + ["SIMPLICITY", "DAM", 2014, 0.87413804], + ["SIMPLICITY", "DAM", 2015, 1.66299431], + ["SIMPLICITY", "DAM", 2016, 2.45185059], + ], + columns=["REGION", "STORAGE", "YEAR", "VALUE"], + ).set_index(["REGION", "STORAGE", "YEAR"]) + + assert_frame_equal(actual, expected) + + def test_null(self, null: ResultsPackage): + """ """ + package = null + with raises(KeyError) as ex: + package.discounted_salvage_value_storage() + assert ( + "Cannot calculate DiscountedSalvageValueStorage due to missing data" + in str(ex) + ) + class TestCapitalRecoveryFactor: def test_crf(self, region, discount_rate_idv, operational_life): @@ -1017,6 +1583,55 @@ def test_df_storage_empty_discount_rate( ) +class TestDiscountFactorStorageSalvage: + def test_discount_factor_storage_salvage(self, region, year, discount_rate_storage): + + storages = ["DAM"] + regions = region["VALUE"].to_list() + years = year["VALUE"].to_list() + actual = discount_factor_storage_salvage( + regions, storages, years, discount_rate_storage + ) + + expected = pd.DataFrame( + data=[ + ["SIMPLICITY", "DAM", 2014, 1.40710042], + ["SIMPLICITY", "DAM", 2015, 1.40710042], + ["SIMPLICITY", "DAM", 2016, 1.40710042], + ["SIMPLICITY", "DAM", 2017, 1.40710042], + ["SIMPLICITY", "DAM", 2018, 1.40710042], + ["SIMPLICITY", "DAM", 2019, 1.40710042], + ["SIMPLICITY", "DAM", 2020, 1.40710042], + ], + columns=["REGION", "STORAGE", "YEAR", "VALUE"], + ).set_index(["REGION", "STORAGE", "YEAR"]) + + assert_frame_equal(actual, expected) + + def test_df_null(self, discount_rate_storage): + + actual = discount_factor_storage_salvage([], [], [], discount_rate_storage) + + expected = pd.DataFrame( + data=[], + columns=["REGION", "STORAGE", "YEAR", "VALUE"], + ).set_index(["REGION", "STORAGE", "YEAR"]) + + assert_frame_equal(actual, expected) + + def test_df_storage_empty_discount_rate( + self, region, year, discount_rate_storage_empty + ): + storages = ["DAM"] + regions = region["VALUE"].to_list() + years = year["VALUE"].to_list() + + with raises(ValueError): + discount_factor_storage_salvage( + regions, storages, years, discount_rate_storage_empty + ) + + class TestResultsPackage: def test_results_package_init(self):