From 2a1b424a4ab6cb2cd90799a0fe5b0de840b4f3ce Mon Sep 17 00:00:00 2001 From: "c.winger" Date: Fri, 13 Sep 2024 14:40:15 +0200 Subject: [PATCH] fix storage cost (capacity) using CAP_F Fixes #553 --- ptxboa/api_calc.py | 23 ++++++++++++++++--- ptxboa/api_optimize.py | 7 ++++-- tests/test_api.py | 51 +++++++++++++++++++++++++++++++++++++++--- tests/test_api_data.py | 2 ++ 4 files changed, 75 insertions(+), 8 deletions(-) diff --git a/ptxboa/api_calc.py b/ptxboa/api_calc.py index b0884f96..bc2043b1 100644 --- a/ptxboa/api_calc.py +++ b/ptxboa/api_calc.py @@ -24,6 +24,14 @@ def calculate(data: CalculateDataType) -> pd.DataFrame: # start main chain calculation main_output_value = 1 # start with normalized value of 1 + # pre-calculate main_output_value before transport + # for correct scaling of storeages. + # storage units use capacity factor CAP_F + # per produced unit (before transport losses) + main_output_value_before_transport = main_output_value + for step_data in data["main_process_chain"]: + main_output_value_before_transport *= step_data["EFF"] + # accumulate needed electric input sum_el = main_output_value results = [] @@ -51,10 +59,19 @@ def calculate(data: CalculateDataType) -> pd.DataFrame: if not is_transport: flh = step_data["FLH"] liefetime = step_data["LIFETIME"] - capex = step_data["CAPEX"] + capex_rel = step_data["CAPEX"] opex_f = step_data["OPEX-F"] - capacity = main_output_value / flh - capex = capacity * capex + + if "CAP_F" in step_data: + # Storage unit: capacity + # TODO: double check units (division by 8760 h)? + capacity = ( + main_output_value_before_transport * step_data["CAP_F"] / 8760 + ) + else: + capacity = main_output_value / flh + + capex = capacity * capex_rel capex_ann = annuity(wacc, liefetime, capex) opex = opex_f * capacity + opex_o * main_output_value diff --git a/ptxboa/api_optimize.py b/ptxboa/api_optimize.py index 0e8f5d1c..4e39b817 100644 --- a/ptxboa/api_optimize.py +++ b/ptxboa/api_optimize.py @@ -377,6 +377,11 @@ def _merge_data(input_data: CalculateDataType, opt_output_data: OptOutputDataTyp step["FLH"] = opt_output_data["ELY"]["FLH"] * 8760 elif step["step"] == "DERIV": step["FLH"] = opt_output_data["DERIV"]["FLH"] * 8760 + elif step["step"] == "EL_STR": + step["CAP_F"] = opt_output_data["EL_STR"]["CAP_F"] + elif step["step"] == "H2_STR": + step["CAP_F"] = opt_output_data["H2_STR"]["CAP_F"] + # secondary processes for step, flow_code in [("H2O", "H2O-L"), ("CO2", "CO2-G")]: sec_process_data = input_data["secondary_process"].get(flow_code) @@ -389,8 +394,6 @@ def _merge_data(input_data: CalculateDataType, opt_output_data: OptOutputDataTyp logger.warning(f"Solver status:{opt_output_data['model_status'][0]}") logger.warning(f"Model status:{opt_output_data['model_status'][1]}") - # TODO: Storage: "CAP_F" - def _get_hashsum(self, data, opt_input_data): src_reg = data["context"]["source_region_code"] # find res diff --git a/tests/test_api.py b/tests/test_api.py index f5589fbb..ef44d8b0 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -4,6 +4,7 @@ import logging import unittest from pathlib import Path +from tempfile import TemporaryDirectory import numpy as np import pytest @@ -26,10 +27,19 @@ class TestApi(unittest.TestCase): @classmethod def setUpClass(cls): """Set up code for class.""" - cls.api = PtxboaAPI(data_dir=ptxdata_dir_static) + cls.temp_dir = TemporaryDirectory() + # create cahce dir (start context) + cache_dir = cls.temp_dir.__enter__() + cls.api = PtxboaAPI(data_dir=ptxdata_dir_static, cache_dir=cache_dir) - def _test_api_call(self, settings): - res, _metadata = self.api.calculate(**settings, optimize_flh=False) + @classmethod + def tearDownClass(cls): + """Tear down code for class.""" + # cleanup cache dir + cls.temp_dir.__exit__(None, None, None) + + def _test_api_call(self, settings, optimize_flh=False): + res, _metadata = self.api.calculate(**settings, optimize_flh=optimize_flh) # test that settings are in results for k, v in settings.items(): if k in ["ship_own_fuel", "output_unit"]: # skip some @@ -344,6 +354,41 @@ def test_pmt(self): self.assertAlmostEqual(annuity(0.5, 10, 1), 0.5088237828522) self.assertAlmostEqual(annuity(1, 10, 1), 1.0009775171) + def test_issue_553_storage_cost(self): + """See https://github.com/agoenergy/ptx-boa/issues/553.""" + settings = { + "region": "United Arab Emirates", + "country": "Germany", + "chain": "Ammonia (AEL) + reconv. to H2", + "res_gen": "PV tilted", + "scenario": "2040 (medium)", + "secproc_co2": "Direct Air Capture", + "secproc_water": "Sea Water desalination", + "transport": "Ship", + "ship_own_fuel": False, + "output_unit": "USD/t", + } + res = self._test_api_call(settings, optimize_flh=False) + res_opt = self._test_api_call(settings, optimize_flh=True) + self.assertAlmostEqual( + res.at[("Electricity and H2 storage", "CAPEX"), "values"], + 896.432599, + places=4, + ) + self.assertAlmostEqual( + res.at[("Electricity and H2 storage", "OPEX"), "values"], 4.396162, places=4 + ) + self.assertAlmostEqual( + res_opt.at[("Electricity and H2 storage", "CAPEX"), "values"], + 221.907535, + places=4, + ) + self.assertAlmostEqual( + res_opt.at[("Electricity and H2 storage", "OPEX"), "values"], + 6.732894, + places=4, + ) + class TestRegression(unittest.TestCase): def test_issue_355_unique_index(self): diff --git a/tests/test_api_data.py b/tests/test_api_data.py index 363401f3..5597b7f8 100644 --- a/tests/test_api_data.py +++ b/tests/test_api_data.py @@ -392,6 +392,7 @@ def test_get_calculation_data_w_opt(ptxdata_dir, scenario, kwargs, request): "CONV": {}, "step": "EL_STR", "process_code": "EL-STR", + "CAP_F": 0.0, }, { "EFF": 0.715, @@ -414,6 +415,7 @@ def test_get_calculation_data_w_opt(ptxdata_dir, scenario, kwargs, request): "CONV": {}, "step": "H2_STR", "process_code": "H2-STR", + "CAP_F": 0.6816605398116187, }, { "EFF": 0.819,