diff --git a/src/otoole/results/result_package.py b/src/otoole/results/result_package.py index a63de92..c961d5e 100644 --- a/src/otoole/results/result_package.py +++ b/src/otoole/results/result_package.py @@ -775,6 +775,9 @@ def calc_crf(df: pd.DataFrame, operational_life: pd.Series) -> pd.Series: return numerator / denominator + if discount_rate_idv.empty or operational_life.empty: + raise ValueError("Cannot calculate PV Annuity due to missing data") + if not regions and not technologies: return pd.DataFrame( data=[], @@ -823,6 +826,10 @@ def pv_annuity( param PvAnnuity{r in REGION, t in TECHNOLOGY} := (1 - (1 + DiscountRate[r])^(-(OperationalLife[r,t]))) * (1 + DiscountRate[r]) / DiscountRate[r]; """ + + if discount_rate.empty or operational_life.empty: + raise ValueError("Cannot calculate PV Annuity due to missing data") + if regions and technologies: index = pd.MultiIndex.from_product( [regions, technologies], names=["REGION", "TECHNOLOGY"] @@ -873,6 +880,11 @@ def discount_factor( (1 + DiscountRate[r]) ^ (y - min{yy in YEAR} min(yy) + 0.5); """ + if discount_rate.empty: + raise ValueError( + "Cannot calculate discount factor due to missing discount rate" + ) + if regions and years: discount_rate["YEAR"] = [years] discount_factor = discount_rate.explode("YEAR").reset_index(level="REGION") @@ -917,6 +929,11 @@ def discount_factor_storage( (1 + DiscountRateStorage[r,s]) ^ (y - min{yy in YEAR} min(yy) + 0.0); """ + if discount_rate_storage.empty: + raise ValueError( + "Cannot calculate discount_factor_storage due to missing discount rate" + ) + if regions and years: index = pd.MultiIndex.from_product( [regions, storages, years], names=["REGION", "STORAGE", "YEAR"] diff --git a/src/otoole/results/results.py b/src/otoole/results/results.py index 982c11c..a5be439 100644 --- a/src/otoole/results/results.py +++ b/src/otoole/results/results.py @@ -35,6 +35,7 @@ def read( param_default_values = self._read_default_values(self.input_config) else: input_data = {} + param_default_values = {} available_results = self.get_results_from_file( filepath, input_data @@ -42,15 +43,7 @@ def read( default_values = self._read_default_values(self.results_config) # type: Dict - # need to expand discount rate for results processing - if "DiscountRate" in input_data: - input_data["DiscountRate"] = self._expand_dataframe( - "DiscountRate", input_data, param_default_values - ) - if "DiscountRateIdv" in input_data: - input_data["DiscountRateIdv"] = self._expand_dataframe( - "DiscountRateIdv", input_data, param_default_values - ) + input_data = self._expand_required_params(input_data, param_default_values) results = self.calculate_results( available_results, input_data @@ -87,6 +80,24 @@ def calculate_results( return results + def _expand_required_params( + self, + input_data: dict[str, pd.DataFrame], + param_defaults: dict[str, Any], + ) -> dict[str, pd.DataFrame]: + """Expands required default values for results processing""" + + if "DiscountRate" in input_data: + input_data["DiscountRate"] = self._expand_dataframe( + "DiscountRate", input_data, param_defaults + ) + if "DiscountRateIdv" in input_data: + input_data["DiscountRateIdv"] = self._expand_dataframe( + "DiscountRateIdv", input_data, param_defaults + ) + + return input_data + class ReadWideResults(ReadResults): def get_results_from_file(self, filepath, input_data): diff --git a/tests/conftest.py b/tests/conftest.py index bd41e71..d22edb5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -83,6 +83,33 @@ def discount_rate_storage(): return df +@fixture +def discount_rate_empty(): + df = pd.DataFrame( + data=[], + columns=["REGION", "VALUE"], + ).set_index(["REGION"]) + return df + + +@fixture +def discount_rate_idv_empty(): + df = pd.DataFrame( + data=[], + columns=["REGION", "TECHNOLOGY", "VALUE"], + ).set_index(["REGION", "TECHNOLOGY"]) + return df + + +@fixture +def discount_rate_storage_empty(): + df = pd.DataFrame( + data=[], + columns=["REGION", "STORAGE", "VALUE"], + ).set_index(["REGION", "STORAGE"]) + return df + + @fixture def emission_activity_ratio(): df = pd.DataFrame( diff --git a/tests/results/test_results_package.py b/tests/results/test_results_package.py index 33c784b..418599c 100644 --- a/tests/results/test_results_package.py +++ b/tests/results/test_results_package.py @@ -669,6 +669,17 @@ def test_crf_no_tech_discount_rate(self, region, discount_rate, operational_life assert_frame_equal(actual, expected) + def test_crf_empty_discount_rate( + self, region, discount_rate_empty, operational_life + ): + technologies = ["GAS_EXTRACTION", "DUMMY"] + regions = region["VALUE"].to_list() + + with raises(ValueError): + capital_recovery_factor( + regions, technologies, discount_rate_empty, operational_life + ) + class TestPvAnnuity: def test_pva(self, region, discount_rate, operational_life): @@ -687,7 +698,7 @@ def test_pva(self, region, discount_rate, operational_life): assert_frame_equal(actual, expected) - def test_pva_null(self, discount_rate): + def test_pva_null(self, discount_rate, operational_life): actual = pv_annuity([], [], discount_rate, operational_life) @@ -698,6 +709,15 @@ def test_pva_null(self, discount_rate): assert_frame_equal(actual, expected) + def test_pva_empty_discount_rate( + self, region, discount_rate_empty, operational_life + ): + technologies = ["GAS_EXTRACTION", "DUMMY"] + regions = region["VALUE"].to_list() + + with raises(ValueError): + pv_annuity(regions, technologies, discount_rate_empty, operational_life) + class TestDiscountFactor: def test_df_start(self, region, year, discount_rate): @@ -774,6 +794,13 @@ def test_df_null(self, discount_rate): assert_frame_equal(actual, expected) + def test_df_empty_discount_rate(self, region, year, discount_rate_empty): + regions = region["VALUE"].to_list() + years = year["VALUE"].to_list() + + with raises(ValueError): + discount_factor(regions, years, discount_rate_empty, 1.0) + class TestDiscountFactorStorage: def test_dfs_start(self, region, year, discount_rate_storage): @@ -859,6 +886,18 @@ def test_df_null(self, discount_rate_storage): 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( + regions, storages, years, discount_rate_storage_empty, 1.0 + ) + class TestResultsPackage: def test_results_package_init(self): diff --git a/tests/test_read_strategies.py b/tests/test_read_strategies.py index 7b4670c..494a5b7 100644 --- a/tests/test_read_strategies.py +++ b/tests/test_read_strategies.py @@ -14,6 +14,7 @@ ReadCplex, ReadGlpk, ReadGurobi, + ReadResults, check_for_duplicates, identify_duplicate, rename_duplicate_column, @@ -21,6 +22,12 @@ from otoole.utils import _read_file +# To instantiate abstract class ReadResults +class DummyReadResults(ReadResults): + def get_results_from_file(self, filepath, input_data): + raise NotImplementedError() + + class TestReadCplex: cplex_data = """ @@ -1275,3 +1282,66 @@ def test_check_datatypes_invalid(self, user_config): with raises(ValueError): check_datatypes(df, user_config, "AvailabilityFactor") + + +class TestExpandRequiredParameters: + """Tests the expansion of required parameters for results processing""" + + region = pd.DataFrame(data=["SIMPLICITY"], columns=["VALUE"]) + + technology = pd.DataFrame(data=["NGCC"], columns=["VALUE"]) + + def test_no_expansion(self): + + user_config = { + "REGION": { + "dtype": "str", + "type": "set", + }, + } + + reader = DummyReadResults(user_config=user_config) + defaults = {} + input_data = {} + + actual = reader._expand_required_params(input_data, defaults) + + assert not actual + + def test_expansion(self, user_config, discount_rate_empty, discount_rate_idv_empty): + + user_config["DiscountRateIdv"] = { + "indices": ["REGION", "TECHNOLOGY"], + "type": "param", + "dtype": "float", + "default": 0.10, + } + + reader = DummyReadResults(user_config=user_config) + defaults = reader._read_default_values(user_config) + input_data = { + "REGION": self.region, + "TECHNOLOGY": self.technology, + "DiscountRate": discount_rate_empty, + "DiscountRateIdv": discount_rate_idv_empty, + } + + actual = reader._expand_required_params(input_data, defaults) + + actual_dr = actual["DiscountRate"] + + expected_dr = pd.DataFrame( + data=[["SIMPLICITY", 0.05]], + columns=["REGION", "VALUE"], + ).set_index(["REGION"]) + + pd.testing.assert_frame_equal(actual_dr, expected_dr) + + actual_dr_idv = actual["DiscountRateIdv"] + + expected_dr_idv = pd.DataFrame( + data=[["SIMPLICITY", "NGCC", 0.10]], + columns=["REGION", "TECHNOLOGY", "VALUE"], + ).set_index(["REGION", "TECHNOLOGY"]) + + pd.testing.assert_frame_equal(actual_dr_idv, expected_dr_idv)