From bb7aa89e4df40485f72c610bee8d8195418b925f Mon Sep 17 00:00:00 2001 From: trevorb1 Date: Sun, 22 Sep 2024 21:18:57 -0700 Subject: [PATCH 1/6] add expand required result param tests --- tests/conftest.py | 18 +++++++++ tests/test_read_strategies.py | 70 +++++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index bd41e713..b6f83539 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -83,6 +83,24 @@ 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 emission_activity_ratio(): df = pd.DataFrame( diff --git a/tests/test_read_strategies.py b/tests/test_read_strategies.py index 7b4670ca..494a5b78 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) From e254be14589116053acad393625d4db573be5007 Mon Sep 17 00:00:00 2001 From: trevorb1 Date: Sun, 22 Sep 2024 21:20:15 -0700 Subject: [PATCH 2/6] make expansion of required params a function --- src/otoole/results/results.py | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/src/otoole/results/results.py b/src/otoole/results/results.py index 982c11c4..6dd04208 100644 --- a/src/otoole/results/results.py +++ b/src/otoole/results/results.py @@ -42,15 +42,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 +79,24 @@ def calculate_results( return results + def _expand_required_params( + self, + input_data: dict[str, pd.DataFrame], + param_defaults: dict[str, str | int | float], + ) -> 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): From 1f6c09abe8db4a3cf9115bb08b3ee372d611b81b Mon Sep 17 00:00:00 2001 From: trevorb1 Date: Sun, 22 Sep 2024 21:25:55 -0700 Subject: [PATCH 3/6] Requires discount rate for crf calc --- src/otoole/results/result_package.py | 3 +++ tests/results/test_results_package.py | 11 +++++++++++ 2 files changed, 14 insertions(+) diff --git a/src/otoole/results/result_package.py b/src/otoole/results/result_package.py index a63de927..9c200f18 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: + raise ValueError("Cannot calculate CRF due to missing discount rate data") + if not regions and not technologies: return pd.DataFrame( data=[], diff --git a/tests/results/test_results_package.py b/tests/results/test_results_package.py index 33c784b9..9bcbd5e7 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): From 1b400494133cd93ac2caa546884783a2fbd9384f Mon Sep 17 00:00:00 2001 From: trevorb1 Date: Sun, 22 Sep 2024 21:32:03 -0700 Subject: [PATCH 4/6] fix for empty defaults --- src/otoole/results/results.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/otoole/results/results.py b/src/otoole/results/results.py index 6dd04208..b2afbea0 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 From 08ff0094b4fab0240a61a3aa6c3c9327d6a65689 Mon Sep 17 00:00:00 2001 From: trevorb1 Date: Sun, 22 Sep 2024 21:53:15 -0700 Subject: [PATCH 5/6] add discount rate checks to result processing --- src/otoole/results/result_package.py | 18 ++++++++++++++-- tests/conftest.py | 9 ++++++++ tests/results/test_results_package.py | 30 ++++++++++++++++++++++++++- 3 files changed, 54 insertions(+), 3 deletions(-) diff --git a/src/otoole/results/result_package.py b/src/otoole/results/result_package.py index 9c200f18..c961d5e3 100644 --- a/src/otoole/results/result_package.py +++ b/src/otoole/results/result_package.py @@ -775,8 +775,8 @@ def calc_crf(df: pd.DataFrame, operational_life: pd.Series) -> pd.Series: return numerator / denominator - if discount_rate_idv.empty: - raise ValueError("Cannot calculate CRF due to missing discount rate data") + 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( @@ -826,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"] @@ -876,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") @@ -920,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/tests/conftest.py b/tests/conftest.py index b6f83539..d22edb59 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -101,6 +101,15 @@ def discount_rate_idv_empty(): 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 9bcbd5e7..418599c9 100644 --- a/tests/results/test_results_package.py +++ b/tests/results/test_results_package.py @@ -698,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) @@ -709,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): @@ -785,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): @@ -870,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): From b789afcca20dd53877c21e67d660059dee70b1db Mon Sep 17 00:00:00 2001 From: trevorb1 Date: Sun, 22 Sep 2024 22:04:38 -0700 Subject: [PATCH 6/6] mypy fix --- src/otoole/results/results.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/otoole/results/results.py b/src/otoole/results/results.py index b2afbea0..a5be439e 100644 --- a/src/otoole/results/results.py +++ b/src/otoole/results/results.py @@ -83,7 +83,7 @@ def calculate_results( def _expand_required_params( self, input_data: dict[str, pd.DataFrame], - param_defaults: dict[str, str | int | float], + param_defaults: dict[str, Any], ) -> dict[str, pd.DataFrame]: """Expands required default values for results processing"""