Skip to content

Commit

Permalink
Merge pull request #234 from OSeMOSYS/issue-217
Browse files Browse the repository at this point in the history
Correct CRF calcualtions for empty discount rates
  • Loading branch information
trevorb1 authored Sep 23, 2024
2 parents 5fcd21e + b789afc commit cf61d2b
Show file tree
Hide file tree
Showing 5 changed files with 174 additions and 10 deletions.
17 changes: 17 additions & 0 deletions src/otoole/results/result_package.py
Original file line number Diff line number Diff line change
Expand Up @@ -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=[],
Expand Down Expand Up @@ -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"]
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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"]
Expand Down
29 changes: 20 additions & 9 deletions src/otoole/results/results.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,22 +35,15 @@ 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
) # type: Dict[str, pd.DataFrame]

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
Expand Down Expand Up @@ -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):
Expand Down
27 changes: 27 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
41 changes: 40 additions & 1 deletion tests/results/test_results_package.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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)

Expand All @@ -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):
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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):
Expand Down
70 changes: 70 additions & 0 deletions tests/test_read_strategies.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,20 @@
ReadCplex,
ReadGlpk,
ReadGurobi,
ReadResults,
check_for_duplicates,
identify_duplicate,
rename_duplicate_column,
)
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 = """<?xml version = "1.0" encoding="UTF-8" standalone="yes"?>
Expand Down Expand Up @@ -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)

0 comments on commit cf61d2b

Please sign in to comment.