From dd9dde8f1c74719737b62f7a7f2f29c9d54808a5 Mon Sep 17 00:00:00 2001 From: Panos Athanasiou Date: Wed, 17 Jan 2024 15:11:04 +0100 Subject: [PATCH 01/10] small refactoring of benefit.py --- flood_adapt/object_model/benefit.py | 292 +++++++++++++++++----------- 1 file changed, 175 insertions(+), 117 deletions(-) diff --git a/flood_adapt/object_model/benefit.py b/flood_adapt/object_model/benefit.py index 2eec2dce6..cd316b65a 100644 --- a/flood_adapt/object_model/benefit.py +++ b/flood_adapt/object_model/benefit.py @@ -18,7 +18,7 @@ class Benefit(IBenefit): - """Class holding all information related to a benefit analysis""" + """Object holding all attributes and methods related to a benefit analysis""" attrs: BenefitModel database_input_path: Union[str, os.PathLike] @@ -26,13 +26,15 @@ class Benefit(IBenefit): scenarios: pd.DataFrame has_run: bool = False - def init(self): - """Initiation function called when object is created through the file or dict options""" + def _init(self): + """Initiation function called when object is created through the load_file or load_dict methods""" + # Get output path based on database path self.results_path = Path(self.database_input_path).parent.joinpath( "output", "Benefits", self.attrs.name ) self.check_scenarios() self.has_run = self.has_run_check() + self.get_output() # Get site config self.site_toml_path = ( Path(self.database_input_path).parent / "static" / "site" / "site.toml" @@ -41,8 +43,15 @@ def init(self): # Get monetary units self.unit = self.site_info.attrs.fiat.damage_unit - def has_run_check(self): - """Check if the benefit assessment has already been run""" + def has_run_check(self) -> bool: + """Check if the benefit analysis has already been run + + Returns + ------- + bool + True if the analysis has already been run, else False + """ + # Output files to check results_toml = self.results_path.joinpath("results.toml") results_csv = self.results_path.joinpath("time_series.csv") results_html = self.results_path.joinpath("benefits.html") @@ -50,15 +59,25 @@ def has_run_check(self): check = all( result.exists() for result in [results_toml, results_csv, results_html] ) - if check: - with open(results_toml, mode="rb") as fp: - self.results = tomli.load(fp) - self.results["html"] = str(results_html) return check - def check_scenarios(self): - """Check which scenarios are needed for this benefit calculation and if they have already been created""" + def get_output(self): + """Reads the benefit analysis results and the path of the html output""" + results_toml = self.results_path.joinpath("results.toml") + results_html = self.results_path.joinpath("benefits.html") + with open(results_toml, mode="rb") as fp: + self.results = tomli.load(fp) + self.results["html"] = str(results_html) + + def check_scenarios(self) -> pd.DataFrame: + """Check which scenarios are needed for this benefit calculation and if they have already been created. + The scenarios attribute of the object is updated accordingly and the table of the scenarios is returned + Returns + ------- + pd.DataFrame + a table with the scenarios of the Benefit analysis and their status + """ # Define names of scenarios scenarios_calc = { "current_no_measures": {}, @@ -124,14 +143,21 @@ def check_scenarios(self): ) return self.scenarios - def ready_to_run(self): - """Checks if all the required scenarios have already been run""" + def ready_to_run(self) -> bool: + """Check if all the required scenarios have already been run + + Returns + ------- + bool + True if required scenarios have been already run + """ self.check_scenarios() check = all(self.scenarios["scenario run"]) + return check def run_cost_benefit(self): - """Runs the cost-benefit calculation""" + """Run the cost-benefit calculation for the total study area and the different aggregation levels""" # Throw an error if not all runs are finished if not self.ready_to_run(): @@ -145,103 +171,10 @@ def run_cost_benefit(self): self.cba_aggregation() # Updates results self.has_run_check() - - @staticmethod - def _calc_benefits( - years: list[int, int], - risk_no_measures: list[float, float], - risk_with_strategy: list[float, float], - discount_rate: float, - ) -> pd.DataFrame: - """Calculates per year benefits and discounted benefits - - Parameters - ---------- - years : list[int, int] - the current and future year for the analysis - risk_no_measures : list[float, float] - the current and future risk value without any measures - risk_with_strategy : list[float, float] - the current and future risk value with the strategy under investigation - discount_rate : float - the yearly discount rate used to calculated the total benefit - - Returns - ------- - pd.DataFrame - Dataframe containing the time-series of risks and benefits per year - """ - benefits = pd.DataFrame( - data={"risk_no_measures": np.nan, "risk_with_strategy": np.nan}, - index=np.arange(years[0], years[1] + 1), - ) - benefits.index.names = ["year"] - - # Fill in dataframe - for strat, risk in zip( - ["no_measures", "with_strategy"], [risk_no_measures, risk_with_strategy] - ): - benefits.loc[years[0], f"risk_{strat}"] = risk[0] - benefits.loc[years[1], f"risk_{strat}"] = risk[1] - - # Assume linear trend between current and future - benefits = benefits.interpolate(method="linear") - - # Calculate benefits - benefits["benefits"] = ( - benefits["risk_no_measures"] - benefits["risk_with_strategy"] - ) - # Calculate discounted benefits using the provided discount rate - benefits["benefits_discounted"] = benefits["benefits"] / ( - 1 + discount_rate - ) ** (benefits.index - benefits.index[0]) - - return benefits - - @staticmethod - def _calc_costs( - benefits: pd.DataFrame, - implementation_cost: float, - annual_maint_cost: float, - discount_rate: float, - ) -> pd.DataFrame: - """Calculates per year costs and discounted costs - - Parameters - ---------- - benefits : pd.DataFrame - a time series of benefits per year (produced with __calc_benefits method) - implementation_cost : float - initial costs of implementing the adaptation strategy - annual_maint_cost : float - annual maintenance cost of the adaptation strategy - discount_rate : float - yearly discount rate - - Returns - ------- - pd.DataFrame - Dataframe containing the time-series of benefits, costs and profits per year - """ - benefits = benefits.copy() - benefits["costs"] = np.nan - # implementations costs at current year and maintenance from year 1 - benefits.loc[benefits.index[0], "costs"] = implementation_cost - benefits.loc[benefits.index[1:], "costs"] = annual_maint_cost - benefits["costs_discounted"] = benefits["costs"] / (1 + discount_rate) ** ( - benefits.index - benefits.index[0] - ) - - # Benefit to Cost Ratio - benefits["profits"] = benefits["benefits"] - benefits["costs"] - benefits["profits_discounted"] = benefits["profits"] / (1 + discount_rate) ** ( - benefits.index - benefits.index[0] - ) - - return benefits + self.get_output() def cba(self): - """Cost-benefit analysis""" + """Cost-benefit analysis for the whole study area""" # Get EAD for each scenario and save to new dataframe scenarios = self.scenarios.copy(deep=True) scenarios["EAD"] = None @@ -327,7 +260,7 @@ def cba(self): self._make_html(cba) def cba_aggregation(self): - """Zonal Benefits per aggregation""" + """Zonal Benefits analysis for the different aggregation areas""" results_path = self.database_input_path.parent.joinpath("output", "Scenarios") # Get years of interest year_start = self.attrs.current_situation.year @@ -430,6 +363,100 @@ def cba_aggregation(self): ) aggr_areas.to_file(outpath, driver="GPKG") + @staticmethod + def _calc_benefits( + years: list[int, int], + risk_no_measures: list[float, float], + risk_with_strategy: list[float, float], + discount_rate: float, + ) -> pd.DataFrame: + """Calculates per year benefits and discounted benefits + + Parameters + ---------- + years : list[int, int] + the current and future year for the analysis + risk_no_measures : list[float, float] + the current and future risk value without any measures + risk_with_strategy : list[float, float] + the current and future risk value with the strategy under investigation + discount_rate : float + the yearly discount rate used to calculated the total benefit + + Returns + ------- + pd.DataFrame + Dataframe containing the time-series of risks and benefits per year + """ + benefits = pd.DataFrame( + data={"risk_no_measures": np.nan, "risk_with_strategy": np.nan}, + index=np.arange(years[0], years[1] + 1), + ) + benefits.index.names = ["year"] + + # Fill in dataframe + for strat, risk in zip( + ["no_measures", "with_strategy"], [risk_no_measures, risk_with_strategy] + ): + benefits.loc[years[0], f"risk_{strat}"] = risk[0] + benefits.loc[years[1], f"risk_{strat}"] = risk[1] + + # Assume linear trend between current and future + benefits = benefits.interpolate(method="linear") + + # Calculate benefits + benefits["benefits"] = ( + benefits["risk_no_measures"] - benefits["risk_with_strategy"] + ) + # Calculate discounted benefits using the provided discount rate + benefits["benefits_discounted"] = benefits["benefits"] / ( + 1 + discount_rate + ) ** (benefits.index - benefits.index[0]) + + return benefits + + @staticmethod + def _calc_costs( + benefits: pd.DataFrame, + implementation_cost: float, + annual_maint_cost: float, + discount_rate: float, + ) -> pd.DataFrame: + """Calculates per year costs and discounted costs + + Parameters + ---------- + benefits : pd.DataFrame + a time series of benefits per year (produced with __calc_benefits method) + implementation_cost : float + initial costs of implementing the adaptation strategy + annual_maint_cost : float + annual maintenance cost of the adaptation strategy + discount_rate : float + yearly discount rate + + Returns + ------- + pd.DataFrame + Dataframe containing the time-series of benefits, costs and profits per year + """ + benefits = benefits.copy() + benefits["costs"] = np.nan + # implementations costs at current year and maintenance from year 1 + benefits.loc[benefits.index[0], "costs"] = implementation_cost + benefits.loc[benefits.index[1:], "costs"] = annual_maint_cost + benefits["costs_discounted"] = benefits["costs"] / (1 + discount_rate) ** ( + benefits.index - benefits.index[0] + ) + + # Benefit to Cost Ratio + benefits["profits"] = benefits["benefits"] - benefits["costs"] + benefits["profits_discounted"] = benefits["profits"] / (1 + discount_rate) ** ( + benefits.index - benefits.index[0] + ) + + return benefits + def _make_html(self, cba): "Make an html with the time-series of the benefits and discounted benefits" # Save a plotly graph in an html @@ -498,8 +525,19 @@ def _make_html(self, cba): fig.write_html(html) @staticmethod - def load_file(filepath: Union[str, os.PathLike]): - """create Benefit object from toml file""" + def load_file(filepath: Union[str, os.PathLike]) -> IBenefit: + """Create a Benefit object from a toml file + + Parameters + ---------- + filepath : Union[str, os.PathLike] + path to a toml file holding the attributes of a Benefit object + + Returns + ------- + IBenefit + a Benefit object + """ obj = Benefit() with open(filepath, mode="rb") as fp: @@ -507,20 +545,40 @@ def load_file(filepath: Union[str, os.PathLike]): obj.attrs = BenefitModel.parse_obj(toml) # if benefits is created by path use that to get to the database path obj.database_input_path = Path(filepath).parents[2] - obj.init() + obj._init() return obj @staticmethod - def load_dict(data: dict[str, Any], database_input_path: Union[str, os.PathLike]): - """create Benefit object from dictionary, e.g. when initialized from GUI""" + def load_dict( + data: dict[str, Any], database_input_path: Union[str, os.PathLike] + ) -> IBenefit: + """Create a Benefit object from a dictionary, e.g. when initialized from GUI + Parameters + ---------- + data : dict[str, Any] + a dictionary with the Benefit attributes + database_input_path : Union[str, os.PathLike] + the path where the FloodAdapt database is located + + Returns + ------- + IBenefit + a Benefit object + """ obj = Benefit() obj.attrs = BenefitModel.parse_obj(data) obj.database_input_path = Path(database_input_path) - obj.init() + obj._init() return obj def save(self, filepath: Union[str, os.PathLike]): - """save Benefit object to a toml file""" + """Save the Benefit attributes as a toml file + + Parameters + ---------- + filepath : Union[str, os.PathLike] + path for saving the toml file + """ with open(filepath, "wb") as f: tomli_w.dump(self.attrs.dict(exclude_none=True), f) From 3df666270aef121a5b278e71d3b16b957330e581 Mon Sep 17 00:00:00 2001 From: Panos Athanasiou Date: Wed, 17 Jan 2024 15:30:46 +0100 Subject: [PATCH 02/10] small correction in benefit code --- flood_adapt/object_model/benefit.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/flood_adapt/object_model/benefit.py b/flood_adapt/object_model/benefit.py index cd316b65a..44e9b0c8a 100644 --- a/flood_adapt/object_model/benefit.py +++ b/flood_adapt/object_model/benefit.py @@ -34,7 +34,8 @@ def _init(self): ) self.check_scenarios() self.has_run = self.has_run_check() - self.get_output() + if self.has_run: + self.get_output() # Get site config self.site_toml_path = ( Path(self.database_input_path).parent / "static" / "site" / "site.toml" From 222a102c7998c43f2847f7a5c23a31cd62cc0454 Mon Sep 17 00:00:00 2001 From: Panos Athanasiou Date: Fri, 19 Jan 2024 09:52:32 +0100 Subject: [PATCH 03/10] initial update of benefit unit tests --- flood_adapt/object_model/benefit.py | 3 + tests/test_object_model/test_benefit.py | 135 ++++++++++++++++++ ...{test_benefits.py => test_benefits_old.py} | 0 3 files changed, 138 insertions(+) create mode 100644 tests/test_object_model/test_benefit.py rename tests/test_object_model/{test_benefits.py => test_benefits_old.py} (100%) diff --git a/flood_adapt/object_model/benefit.py b/flood_adapt/object_model/benefit.py index 44e9b0c8a..1d4cf57bf 100644 --- a/flood_adapt/object_model/benefit.py +++ b/flood_adapt/object_model/benefit.py @@ -162,6 +162,9 @@ def run_cost_benefit(self): # Throw an error if not all runs are finished if not self.ready_to_run(): + # First check is scenarios are there + if "No" in self.scenarios["scenario created"].to_numpy(): + raise RuntimeError("Necessary scenarios have not been created yet.") scens = self.scenarios["scenario created"][~self.scenarios["scenario run"]] raise RuntimeError( f"Scenarios {', '.join(scens.values)} need to be run before the cost-benefit analysis can be performed" diff --git a/tests/test_object_model/test_benefit.py b/tests/test_object_model/test_benefit.py new file mode 100644 index 000000000..de3be2b41 --- /dev/null +++ b/tests/test_object_model/test_benefit.py @@ -0,0 +1,135 @@ +import pandas as pd +import pytest + +from flood_adapt.object_model.benefit import Benefit +from flood_adapt.object_model.interface.benefits import IBenefit + + +# Create Benefit object using example benefit toml in test database +def test_loadFile_fromTestBenefitToml_createBenefit(test_db): + benefit_path = test_db.input_path.joinpath( + "benefits", + "benefit_raise_properties_2050", + "benefit_raise_properties_2050.toml", + ) + + benefit = Benefit.load_file(benefit_path) + + assert isinstance(benefit, IBenefit) + + +# Create Benefit object using using example benefit toml in test database to see if test with no costs is load normally +def test_loadFile_fromTestBenefitNoCostsToml_createBenefit(test_db): + benefit_path = test_db.input_path.joinpath( + "benefits", + "benefit_raise_properties_2050_no_costs", + "benefit_raise_properties_2050_no_costs.toml", + ) + + benefit = Benefit.load_file(benefit_path) + + assert isinstance(benefit, IBenefit) + + +# Get FileNotFoundError when the path to the benefit toml does not exist +def test_loadFile_nonExistingFile_FileNotFoundError(test_db): + benefit_path = test_db.input_path.joinpath( + "benefits", + "benefit_raise_properties_2050_random", + "benefit_raise_properties_2050_random.toml", + ) + with pytest.raises(FileNotFoundError): + Benefit.load_file(benefit_path) + + +# Create Benefit object using using a dictionary +def test_loadDict_fromTestDict_createBenefit(test_db): + test_dict = { + "name": "benefit_raise_properties_2080", + "description": "", + "event_set": "test_set", + "strategy": "elevate_comb_correct", + "projection": "all_projections", + "future_year": 2080, + "current_situation": {"projection": "current", "year": "2023"}, + "baseline_strategy": "no_measures", + "discount_rate": 0.07, + "implementation_cost": 200000000, + "annual_maint_cost": 100000, + } + + benefit = Benefit.load_dict(test_dict, test_db.input_path) + + assert isinstance(benefit, IBenefit) + + +# Fixture to create a Benefit object +@pytest.fixture(scope="function") +def benefit_obj(test_db): + benefit_path = test_db.input_path.joinpath( + "benefits", + "benefit_raise_properties_2050", + "benefit_raise_properties_2050.toml", + ) + + benefit = Benefit.load_file(benefit_path) + return benefit + + +# Tests for when the scenarios needed for the benefit analysis are not there yet +class TestBenefitScenariosNotCreated: + # When benefit analysis is not run yet the has_run_check method should return False + def test_hasRunCheck_notCreated_false(self, benefit_obj): + assert not benefit_obj.has_run_check() + + # The check_scenarios methods should always return a table with the scenarios that are needed to run the benefit analysis + def test_checkScenarios_notCreated_table(self, benefit_obj): + scenarios = benefit_obj.check_scenarios() + assert isinstance(scenarios, pd.DataFrame) + assert len(scenarios) == 4 + + # When the needed scenarios are not there, the ready_to_run method should return false + def test_readyToRun_notCreated_false(self, benefit_obj): + assert not benefit_obj.ready_to_run() + + # When the needed scenarios are not there, the run_cost_benefit method should return a RunTimeError + def test_runCostBenefit_notCreated_raiseRunTimeError(self, benefit_obj): + with pytest.raises(RuntimeError) as exception_info: + benefit_obj.run_cost_benefit() + assert ( + str(exception_info.value) + == "Necessary scenarios have not been created yet." + ) + + +# Tests for when the scenarios needed for the benefit analysis are not there yet +class TestBenefitScenariosCreated: + # Fixture to create a Benefit object + @pytest.fixture(scope="class") + def create_scenarios(self): + pass + # Create missing scenarios + # api_benefits.create_benefit_scenarios(benefit, database) + + # When benefit analysis is not run yet the has_run_check method should return False + def test_hasRunCheck_notRun_false(self, benefit_obj): + assert not benefit_obj.has_run_check() + + # The check_scenarios methods should always return a table with the scenarios that are needed to run the benefit analysis + def test_checkScenarios_notReadyToRun_table(self, benefit_obj): + scenarios = benefit_obj.check_scenarios() + assert isinstance(scenarios, pd.DataFrame) + assert len(scenarios) == 4 + + # When the needed scenarios are not there, the ready_to_run method should return false + def test_readyToRun_notReadyToRun_raiseRunTimeError(self, benefit_obj): + assert not benefit_obj.ready_to_run() + + # When the needed scenarios are not there, the run_cost_benefit method should return a RunTimeError + def test_runCostBenefit_notReadyToRun_raiseRunTimeError(self, benefit_obj): + with pytest.raises(RuntimeError) as exception_info: + benefit_obj.run_cost_benefit() + assert ( + str(exception_info.value) + == "Necessary scenarios have not been created yet." + ) diff --git a/tests/test_object_model/test_benefits.py b/tests/test_object_model/test_benefits_old.py similarity index 100% rename from tests/test_object_model/test_benefits.py rename to tests/test_object_model/test_benefits_old.py From 3694cbbca64a3655035b356b2e71628428a1431f Mon Sep 17 00:00:00 2001 From: Panos Athanasiou Date: Tue, 23 Jan 2024 16:13:41 +0100 Subject: [PATCH 04/10] small refactoring of benefit class --- flood_adapt/object_model/benefit.py | 31 +++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/flood_adapt/object_model/benefit.py b/flood_adapt/object_model/benefit.py index 1d4cf57bf..6c4358e1c 100644 --- a/flood_adapt/object_model/benefit.py +++ b/flood_adapt/object_model/benefit.py @@ -62,13 +62,24 @@ def has_run_check(self) -> bool: ) return check - def get_output(self): - """Reads the benefit analysis results and the path of the html output""" + def get_output(self) -> dict: + """Reads the benefit analysis results and the path of the html output + + Returns + ------- + dict + results of benefit calculation + """ + if not self.has_run_check(): + raise RuntimeError( + f"Cannot read output since benefit analysis '{self.attrs.name}' has not been run yet." + ) results_toml = self.results_path.joinpath("results.toml") results_html = self.results_path.joinpath("benefits.html") with open(results_toml, mode="rb") as fp: self.results = tomli.load(fp) self.results["html"] = str(results_html) + return self.results def check_scenarios(self) -> pd.DataFrame: """Check which scenarios are needed for this benefit calculation and if they have already been created. @@ -169,6 +180,14 @@ def run_cost_benefit(self): raise RuntimeError( f"Scenarios {', '.join(scens.values)} need to be run before the cost-benefit analysis can be performed" ) + + # If path for results does not yet exist, make it, and if it does delete it and recreate it + if not self.results_path.is_dir(): + self.results_path.mkdir(parents=True) + else: + shutil.rmtree(self.results_path) + self.results_path.mkdir(parents=True) + # Run the cost-benefit analysis self.cba() # Run aggregation benefits @@ -244,12 +263,8 @@ def cba(self): results["IRR"] = np.round(npf.irr(cba["profits"]), 3) # Save results - # If path for results does not yet exist, make it if not self.results_path.is_dir(): self.results_path.mkdir(parents=True) - else: - shutil.rmtree(self.results_path) - self.results_path.mkdir(parents=True) # Save indicators in a toml file indicators = self.results_path.joinpath("results.toml") @@ -342,6 +357,10 @@ def cba_aggregation(self): "benefits_discounted" ].sum() + # Save results + if not self.results_path.is_dir(): + self.results_path.mkdir(parents=True) + # Save benefits per aggregation area (csv and gpkg) for i, aggr_name in enumerate(aggregations): csv_filename = self.results_path.joinpath(f"benefits_{aggr_name}.csv") From 5103dcc476279eaa2ae31970e306ea2748c1fb4a Mon Sep 17 00:00:00 2001 From: Panos Athanasiou Date: Tue, 23 Jan 2024 16:13:57 +0100 Subject: [PATCH 05/10] final update in benefit unit tests --- tests/conftest.py | 27 +- tests/test_object_model/test_benefit.py | 376 +++++++++++++++++++++--- 2 files changed, 362 insertions(+), 41 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 79ef7f6b7..ba1abe1fe 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,6 +6,11 @@ from flood_adapt.api.startup import read_database +# Get the database file structure before the tests +rootPath = Path().absolute().parent / "Database" # the path to the database +site_name = "charleston_test" # the name of the test site +database_path = str(rootPath.joinpath(site_name)) + def get_file_structure(path: str) -> list: """Get the file structure of a directory and store it in a list""" @@ -37,19 +42,27 @@ def test_db(): """This fixture is used for testing in general to setup the test database, perform the test, and clean the database after each test. It is used by other fixtures to set up and clean the test_database""" + dbs = read_database(rootPath, site_name) + # NOTE: to access the contents of this function in the test, + # the first line of your test needs to initialize the yielded variables: + # 'dbs, folders = test_db' + file_structure = get_file_structure(database_path) + # Run the test + yield dbs + # Remove all files and folders that were not present before the test + remove_files_and_folders(database_path, file_structure) - # Get the database file structure before the test - rootPath = Path().absolute().parent / "Database" # the path to the database - site_name = "charleston_test" # the name of the test site - database_path = str(rootPath.joinpath(site_name)) - file_structure = get_file_structure(database_path) +@pytest.fixture(scope="class") +def test_db_class(): + """This fixture is used for testing in general to setup the test database, + perform the test, and clean the database after each test. + It is used by other fixtures to set up and clean the test_database""" dbs = read_database(rootPath, site_name) - # NOTE: to access the contents of this function in the test, # the first line of your test needs to initialize the yielded variables: # 'dbs, folders = test_db' - + file_structure = get_file_structure(database_path) # Run the test yield dbs # Remove all files and folders that were not present before the test diff --git a/tests/test_object_model/test_benefit.py b/tests/test_object_model/test_benefit.py index de3be2b41..2340a4775 100644 --- a/tests/test_object_model/test_benefit.py +++ b/tests/test_object_model/test_benefit.py @@ -1,16 +1,26 @@ +import geopandas as gpd +import numpy as np import pandas as pd import pytest +import tomli from flood_adapt.object_model.benefit import Benefit from flood_adapt.object_model.interface.benefits import IBenefit +_RAND = np.random.default_rng(2021) # Value to make sure randomizing is always the same +_TEST_NAMES = { + "benefit_with_costs": "benefit_raise_properties_2050", + "benefit_without_costs": "benefit_raise_properties_2050_no_costs", +} + # Create Benefit object using example benefit toml in test database def test_loadFile_fromTestBenefitToml_createBenefit(test_db): + name = "benefit_raise_properties_2050" benefit_path = test_db.input_path.joinpath( "benefits", - "benefit_raise_properties_2050", - "benefit_raise_properties_2050.toml", + name, + f"{name}.toml", ) benefit = Benefit.load_file(benefit_path) @@ -20,10 +30,11 @@ def test_loadFile_fromTestBenefitToml_createBenefit(test_db): # Create Benefit object using using example benefit toml in test database to see if test with no costs is load normally def test_loadFile_fromTestBenefitNoCostsToml_createBenefit(test_db): + name = "benefit_raise_properties_2050_no_costs" benefit_path = test_db.input_path.joinpath( "benefits", - "benefit_raise_properties_2050_no_costs", - "benefit_raise_properties_2050_no_costs.toml", + name, + f"{name}.toml", ) benefit = Benefit.load_file(benefit_path) @@ -33,11 +44,13 @@ def test_loadFile_fromTestBenefitNoCostsToml_createBenefit(test_db): # Get FileNotFoundError when the path to the benefit toml does not exist def test_loadFile_nonExistingFile_FileNotFoundError(test_db): + name = "benefit_raise_properties_2050_random" benefit_path = test_db.input_path.joinpath( "benefits", - "benefit_raise_properties_2050_random", - "benefit_raise_properties_2050_random.toml", + name, + f"{name}.toml", ) + with pytest.raises(FileNotFoundError): Benefit.load_file(benefit_path) @@ -63,36 +76,70 @@ def test_loadDict_fromTestDict_createBenefit(test_db): assert isinstance(benefit, IBenefit) -# Fixture to create a Benefit object -@pytest.fixture(scope="function") -def benefit_obj(test_db): - benefit_path = test_db.input_path.joinpath( - "benefits", - "benefit_raise_properties_2050", - "benefit_raise_properties_2050.toml", - ) - - benefit = Benefit.load_file(benefit_path) - return benefit - - # Tests for when the scenarios needed for the benefit analysis are not there yet class TestBenefitScenariosNotCreated: + # Fixture to create a Benefit object + @pytest.fixture(scope="function") + def benefit_obj(self, test_db): + name = "benefit_raise_properties_2050" + benefit_path = test_db.input_path.joinpath( + "benefits", + name, + f"{name}.toml", + ) + + benefit = Benefit.load_file(benefit_path) + return benefit + # When benefit analysis is not run yet the has_run_check method should return False def test_hasRunCheck_notCreated_false(self, benefit_obj): assert not benefit_obj.has_run_check() # The check_scenarios methods should always return a table with the scenarios that are needed to run the benefit analysis - def test_checkScenarios_notCreated_table(self, benefit_obj): + def test_checkScenarios_notCreated_scenariosTable(self, benefit_obj): scenarios = benefit_obj.check_scenarios() assert isinstance(scenarios, pd.DataFrame) assert len(scenarios) == 4 + assert "No" in scenarios["scenario created"].to_list() + assert all(scenarios["event"] == benefit_obj.site_info.attrs.benefits.event_set) + assert ( + scenarios.loc["current_no_measures", "strategy"] + == benefit_obj.site_info.attrs.benefits.baseline_strategy + ) + assert ( + scenarios.loc["future_no_measures", "strategy"] + == benefit_obj.site_info.attrs.benefits.baseline_strategy + ) + assert ( + scenarios.loc["current_with_strategy", "strategy"] + == benefit_obj.attrs.strategy + ) + assert ( + scenarios.loc["future_with_strategy", "strategy"] + == benefit_obj.attrs.strategy + ) + assert ( + scenarios.loc["current_no_measures", "projection"] + == benefit_obj.site_info.attrs.benefits.current_projection + ) + assert ( + scenarios.loc["future_no_measures", "projection"] + == benefit_obj.attrs.projection + ) + assert ( + scenarios.loc["current_with_strategy", "projection"] + == benefit_obj.site_info.attrs.benefits.current_projection + ) + assert ( + scenarios.loc["future_with_strategy", "projection"] + == benefit_obj.attrs.projection + ) - # When the needed scenarios are not there, the ready_to_run method should return false + # When the needed scenarios are not run yet, the ready_to_run method should return false def test_readyToRun_notCreated_false(self, benefit_obj): assert not benefit_obj.ready_to_run() - # When the needed scenarios are not there, the run_cost_benefit method should return a RunTimeError + # When the needed scenarios are not run yet, the run_cost_benefit method should return a RunTimeError def test_runCostBenefit_notCreated_raiseRunTimeError(self, benefit_obj): with pytest.raises(RuntimeError) as exception_info: benefit_obj.run_cost_benefit() @@ -101,35 +148,296 @@ def test_runCostBenefit_notCreated_raiseRunTimeError(self, benefit_obj): == "Necessary scenarios have not been created yet." ) + # When the benefit analysis not run yet, the get_output method should return a RunTimeError + def test_getOutput_notRun_raiseRunTimeError(self, benefit_obj): + with pytest.raises(RuntimeError) as exception_info: + benefit_obj.get_output() + assert "Cannot read output since benefit analysis" in str( + exception_info.value + ) -# Tests for when the scenarios needed for the benefit analysis are not there yet + +# Tests for when the scenarios needed for the benefit analysis are created but not run class TestBenefitScenariosCreated: - # Fixture to create a Benefit object - @pytest.fixture(scope="class") - def create_scenarios(self): - pass + # Fixture to create a Benefit object and create missing scenarios + @pytest.fixture(scope="class", autouse=True) + def benefit_obj(self, test_db_class): + test_db = test_db_class + name = "benefit_raise_properties_2050" + benefit_path = test_db.input_path.joinpath( + "benefits", + name, + f"{name}.toml", + ) + + benefit = Benefit.load_file(benefit_path) # Create missing scenarios - # api_benefits.create_benefit_scenarios(benefit, database) + test_db.create_benefit_scenarios(benefit) + return benefit - # When benefit analysis is not run yet the has_run_check method should return False + # When benefit analysis is not run yet, the has_run_check method should return False def test_hasRunCheck_notRun_false(self, benefit_obj): assert not benefit_obj.has_run_check() # The check_scenarios methods should always return a table with the scenarios that are needed to run the benefit analysis - def test_checkScenarios_notReadyToRun_table(self, benefit_obj): + def test_checkScenarios_notReadyToRun_scenariosTable(self, benefit_obj): scenarios = benefit_obj.check_scenarios() assert isinstance(scenarios, pd.DataFrame) assert len(scenarios) == 4 + assert "No" not in scenarios["scenario created"].to_list() - # When the needed scenarios are not there, the ready_to_run method should return false + # When the needed scenarios are not run yet, the ready_to_run method should return false def test_readyToRun_notReadyToRun_raiseRunTimeError(self, benefit_obj): assert not benefit_obj.ready_to_run() - # When the needed scenarios are not there, the run_cost_benefit method should return a RunTimeError + # When the needed scenarios are not run yet, the run_cost_benefit method should return a RunTimeError def test_runCostBenefit_notReadyToRun_raiseRunTimeError(self, benefit_obj): with pytest.raises(RuntimeError) as exception_info: benefit_obj.run_cost_benefit() + assert ( + "need to be run before the cost-benefit analysis can be performed" + in str(exception_info.value) + ) + + # When the benefit analysis not run yet, the get_output method should return a RunTimeError + def test_getOutput_notRun_raiseRunTimeError(self, benefit_obj): + with pytest.raises(RuntimeError) as exception_info: + benefit_obj.get_output() + assert "Cannot read output since benefit analysis" in str( + exception_info.value + ) + + +# Tests for when the scenarios needed for the benefit analysis are run +@pytest.mark.parametrize("benefit_name", _TEST_NAMES.keys(), scope="class") +class TestBenefitScenariosRun: + # Fixture to create a Benefit object, missing scenarios and scenarios output + @pytest.fixture(scope="class") + def prepare_outputs(self, test_db_class, benefit_name): + benefit_name = _TEST_NAMES[benefit_name] + test_db = test_db_class + benefit_path = test_db.input_path.joinpath( + "benefits", + benefit_name, + f"{benefit_name}.toml", + ) + + benefit = Benefit.load_file(benefit_path) + # Create missing scenarios + test_db.create_benefit_scenarios(benefit) + + # Create dummy results to use for benefit analysis + damages_dummy = { + "current_no_measures": 100e6, + "current_with_strategy": 50e6, + "future_no_measures": 300e6, + "future_with_strategy": 180e6, + } + # Get aggregation areas of test database + aggrs = test_db.get_aggregation_areas() + + # Iterate through the 4 scenarios + for name, row in benefit.scenarios.iterrows(): + # Create output folder + output_path = ( + test_db.input_path.parent + / "output" + / "Scenarios" + / row["scenario created"] + ) + if not output_path.exists(): + output_path.mkdir(parents=True) + # Creaty dummy fiat log + fiat_path = output_path.joinpath("Impacts", "fiat_model") + if not fiat_path.exists(): + fiat_path.mkdir(parents=True) + with open(fiat_path.joinpath("fiat.log"), "w") as f: + f.write("Geom calculation are done!") + # Create dummy metrics file + dummy_metrics = pd.DataFrame( + { + "Description": "", + "Show In Metrics Table": "TRUE", + "Long Name": "", + "Value": damages_dummy[name], + }, + index=["ExpectedAnnualDamages"], + ) + + dummy_metrics.to_csv( + output_path.joinpath(f"Infometrics_{row['scenario created']}.csv") + ) + # Create dummy metrics for aggregation areas + for aggr_type in aggrs.keys(): + aggr = aggrs[aggr_type] + # Generate random distribution of damage per aggregation area + dmgs = np.random.random(len(aggr)) + dmgs = dmgs / dmgs.sum() * damages_dummy[name] + + dict0 = { + "Description": ["", ""], + "Show In Metrics Table": ["TRUE", "TRUE"], + "Long Name": ["", ""], + } + + for i, aggr_area in enumerate(aggr["name"]): + dict0[aggr_area] = [dmgs[i], _RAND.normal(1, 0.2) * dmgs[i]] + + dummy_metrics_aggr = pd.DataFrame( + dict0, index=["ExpectedAnnualDamages", "EWEAD"] + ).T + + dummy_metrics_aggr.to_csv( + output_path.joinpath( + f"Infometrics_{row['scenario created']}_{aggr_type}.csv" + ) + ) + + return benefit, aggrs + + # When benefit analysis is not run yet, the has_run_check method should return False + def test_hasRunCheck_notRun_false(self, prepare_outputs): + benefit_obj = prepare_outputs[0] + assert not benefit_obj.has_run_check() + + # The check_scenarios methods should always return a table with the scenarios that are needed to run the benefit analysis + def test_checkScenarios_ReadyToRun_scenariosTable(self, prepare_outputs): + benefit_obj = prepare_outputs[0] + + scenarios = benefit_obj.check_scenarios() + assert isinstance(scenarios, pd.DataFrame) + assert len(scenarios) == 4 + assert "No" not in scenarios["scenario created"].to_list() + + # When the needed scenarios are run, the ready_to_run method should return true + def test_readyToRun_readyToRun_true(self, prepare_outputs): + benefit_obj = prepare_outputs[0] + assert benefit_obj.ready_to_run() + + # the cba method should run the cost benefit analysis for the whole region + def test_cba_readyToRun_correctOutput(self, prepare_outputs): + benefit_obj = prepare_outputs[0] + benefit_obj.cba() + + # assert if individual output files are there + time_series_path = benefit_obj.results_path.joinpath("time_series.csv") + results_path = benefit_obj.results_path.joinpath("results.toml") + assert time_series_path.exists() + assert results_path.exists() + assert benefit_obj.results_path.joinpath("benefits.html").exists() + + # assert if time_series.csv has correct data + time_series = pd.read_csv(time_series_path) # read csv + + main_columns = [ + "year", + "risk_no_measures", + "risk_with_strategy", + "benefits", + "benefits_discounted", + ] + cost_columns = ["costs", "costs_discounted", "profits", "profits_discounted"] + + assert set(main_columns).issubset(time_series.columns) assert ( - str(exception_info.value) - == "Necessary scenarios have not been created yet." + time_series["year"].min() + == benefit_obj.site_info.attrs.benefits.current_year + ) + assert time_series["year"].max() == benefit_obj.attrs.future_year + assert ( + len(time_series) + == benefit_obj.attrs.future_year + - benefit_obj.site_info.attrs.benefits.current_year + + 1 ) + + # assert if results.toml has correct values + with open(results_path, mode="rb") as fp: + results = tomli.load(fp) + + # assert if time-series and totals are consistent + assert results["benefits"] == time_series["benefits_discounted"].sum() + + # assert if results are equal to the expected values based on the input + assert pytest.approx(results["benefits"], 2) == 963433925 + + # assert if cost specific output is correctly presented + if benefit_obj.attrs.implementation_cost: + assert set(cost_columns).issubset(time_series.columns) + assert results["costs"] == time_series["costs_discounted"].sum() + assert pytest.approx(results["costs"], 2) == 201198670 + assert ( + pytest.approx(results["BCR"], 2) + == results["benefits"] / results["costs"] + ) + assert pytest.approx(results["BCR"], 2) == 4.79 + assert pytest.approx(results["NPV"], 2) == 762235253 + assert pytest.approx(results["IRR"], 2) == 0.394 + else: + assert not set(cost_columns).issubset(time_series.columns) + assert "costs" not in results + assert "BCR" not in results + assert "NPV" not in results + assert "IRR" not in results + + # the cba_aggregation method should run the cost benefit analysis for each individual aggregation type + def test_cbaAggregation_ReadyToRun_correctOutput(self, prepare_outputs): + benefit_obj = prepare_outputs[0] + aggrs = prepare_outputs[1] + benefit_obj.cba_aggregation() + # loop through aggregation types + for aggr_type in benefit_obj.site_info.attrs.fiat.aggregation: + # assert existence of output files + csv_path = benefit_obj.results_path.joinpath( + f"benefits_{aggr_type.name}.csv" + ) + gpkg_path = benefit_obj.results_path.joinpath( + f"benefits_{aggr_type.name}.gpkg" + ) + assert csv_path.exists() + assert gpkg_path.exists() + # assert correct structure of table + agg_results = pd.read_csv(csv_path) + assert "Benefits" in agg_results.columns + if aggr_type.equity: + assert "Equity Weighted Benefits" in agg_results.columns + assert len(agg_results) == len(aggrs[aggr_type.name]) + # assert correct total benefits + assert pytest.approx(agg_results["Benefits"].sum(), 2) == 963433925 + # assert existence and content of geopackages + polygons = gpd.read_file(gpkg_path) + assert len(polygons) == len(aggrs[aggr_type.name]) + + # When the benefit analysis is run, the get_output method should return correct outputs + def test_getOutput_Run_correctOutput(self, prepare_outputs): + benefit_obj = prepare_outputs[0] + benefit_obj = prepare_outputs[0] + benefit_obj.cba() + results = benefit_obj.get_output() + assert hasattr(benefit_obj, "results") + assert "html" in results + + # When the needed scenarios are run, the run_cost_benefit method should run the benefit analysis and save the results + def test_runCostBenefit_ReadyToRun_raiseRunTimeError(self, prepare_outputs): + benefit_obj = prepare_outputs[0] + benefit_obj.run_cost_benefit() + + # get results + results_path = benefit_obj.results_path.joinpath("results.toml") + with open(results_path, mode="rb") as fp: + results = tomli.load(fp) + # get aggregation + for aggr_type in benefit_obj.site_info.attrs.fiat.aggregation: + csv_agg_results = pd.read_csv( + benefit_obj.results_path.joinpath(f"benefits_{aggr_type.name}.csv") + ) + tot_benefits_agg = csv_agg_results["Benefits"].sum() + + # assert if the results per aggregation area sum to the same total as the basic calculation + assert pytest.approx(tot_benefits_agg, 2) == results["benefits"] + + # When benefit analysis is run already, the has_run_check method should return True + def test_hasRunCheck_Run_true(self, prepare_outputs): + benefit_obj = prepare_outputs[0] + benefit_obj.run_cost_benefit() + assert benefit_obj.has_run_check() From e50dd6995bd95d089f0fdafd3eedbbf5f2245e78 Mon Sep 17 00:00:00 2001 From: Panos Athanasiou Date: Tue, 23 Jan 2024 16:44:20 +0100 Subject: [PATCH 06/10] deleted the old benefit tests --- tests/test_object_model/test_benefits_old.py | 253 ------------------- 1 file changed, 253 deletions(-) delete mode 100644 tests/test_object_model/test_benefits_old.py diff --git a/tests/test_object_model/test_benefits_old.py b/tests/test_object_model/test_benefits_old.py deleted file mode 100644 index 400f1f3ad..000000000 --- a/tests/test_object_model/test_benefits_old.py +++ /dev/null @@ -1,253 +0,0 @@ -from pathlib import Path - -import numpy as np -import pandas as pd -import pytest -import tomli - -from flood_adapt.dbs_controller import Database -from flood_adapt.object_model.benefit import Benefit - -test_database = Path().absolute() / "tests" / "test_database" -rng = np.random.default_rng(2021) - - -def test_benefit_read(cleanup_database): - benefit_toml = ( - test_database - / "charleston" - / "input" - / "benefits" - / "benefit_raise_properties_2050" - / "benefit_raise_properties_2050.toml" - ) - - assert benefit_toml.is_file() - benefit = Benefit.load_file(benefit_toml) - assert isinstance(benefit, Benefit) - - -def test_check_scenarios(cleanup_database): - benefit_toml = ( - test_database - / "charleston" - / "input" - / "benefits" - / "benefit_raise_properties_2050" - / "benefit_raise_properties_2050.toml" - ) - - assert benefit_toml.is_file() - - benefit = Benefit.load_file(benefit_toml) - df_check = benefit.check_scenarios() - assert isinstance(df_check, pd.DataFrame) - - -def test_run_benefit_analysis(cleanup_database): - dbs = Database(test_database, "charleston") - - benefit_toml = ( - test_database - / "charleston" - / "input" - / "benefits" - / "benefit_raise_properties_2050_no_costs" - / "benefit_raise_properties_2050_no_costs.toml" - ) - - assert benefit_toml.is_file() - - benefit = Benefit.load_file(benefit_toml) - - # Create missing scenarios - dbs.create_benefit_scenarios(benefit) - aggrs = dbs.get_aggregation_areas() - - # Check that error is returned if not all runs are finished - if not all(benefit.scenarios["scenario run"]): - with pytest.raises(RuntimeError): - # Assert error if not yet run - benefit.run_cost_benefit() - - # Create dummy results to use for benefit analysis - damages_dummy = { - "current_no_measures": 100e6, - "current_with_strategy": 50e6, - "future_no_measures": 300e6, - "future_with_strategy": 180e6, - } - - for name, row in benefit.scenarios.iterrows(): - # Create output folder - output_path = ( - test_database - / "charleston" - / "output" - / "Scenarios" - / row["scenario created"] - ) - if not output_path.exists(): - output_path.mkdir(parents=True) - # Create dummy metrics file - dummy_metrics = pd.DataFrame( - { - "Description": "", - "Show In Metrics Table": "TRUE", - "Long Name": "", - "Value": damages_dummy[name], - }, - index=["ExpectedAnnualDamages"], - ) - - dummy_metrics.to_csv( - output_path.joinpath(f"Infometrics_{row['scenario created']}.csv") - ) - - # Create dummy metrics for aggregation areas - for aggr_type in aggrs.keys(): - aggr = aggrs[aggr_type] - # Generate random distribution of damage per aggregation area - dmgs = np.random.random(len(aggr)) - dmgs = dmgs / dmgs.sum() * damages_dummy[name] - - dict0 = { - "Description": ["", ""], - "Show In Metrics Table": ["TRUE", "TRUE"], - "Long Name": ["", ""], - } - - for i, aggr_area in enumerate(aggr["name"]): - dict0[aggr_area] = [dmgs[i], rng.normal(1, 0.2) * dmgs[i]] - - dummy_metrics_aggr = pd.DataFrame( - dict0, index=["ExpectedAnnualDamages", "EWEAD"] - ).T - - dummy_metrics_aggr.to_csv( - output_path.joinpath( - f"Infometrics_{row['scenario created']}_{aggr_type}.csv" - ) - ) - - # Run benefit analysis with dummy data - benefit.cba() - - # Read results - results_path = ( - test_database / "charleston" / "output" / "Benefits" / benefit.attrs.name - ) - with open(results_path.joinpath("results.toml"), mode="rb") as fp: - results = tomli.load(fp) - - # get results - tot_benefits = results["benefits"] - # get time-series - csv_results = pd.read_csv(results_path.joinpath("time_series.csv")) - tot_benefits2 = csv_results["benefits_discounted"].sum() - - # assert if time-series and totals are consistent - assert tot_benefits == tot_benefits2 - - # assert if results are equal to the expected values based on the input - assert pytest.approx(tot_benefits, 2) == 963433925 - - # Run benefit analysis with dummy data - benefit.cba_aggregation() - # get aggregation - for aggr_type in aggrs.keys(): - csv_agg_results = pd.read_csv( - results_path.joinpath(f"benefits_{aggr_type}.csv") - ) - tot_benefits_agg = csv_agg_results["Benefits"].sum() - - # assert if results are equal to the expected values based on the input - assert pytest.approx(tot_benefits_agg, 2) == tot_benefits - - -def test_run_CBA(cleanup_database): - dbs = Database(test_database, "charleston") - - benefit_toml = ( - test_database - / "charleston" - / "input" - / "benefits" - / "benefit_raise_properties_2050" - / "benefit_raise_properties_2050.toml" - ) - - assert benefit_toml.is_file() - - benefit = Benefit.load_file(benefit_toml) - - # Create missing scenarios - dbs.create_benefit_scenarios(benefit) - - # Check that error is returned if not all runs are finished - if not all(benefit.scenarios["scenario run"]): - with pytest.raises(RuntimeError): - # Assert error if not yet run - benefit.run_cost_benefit() - - # Create dummy results to use for benefit analysis - damages_dummy = { - "current_no_measures": 100e6, - "current_with_strategy": 50e6, - "future_no_measures": 300e6, - "future_with_strategy": 180e6, - } - - for name, row in benefit.scenarios.iterrows(): - # Create output folder - output_path = ( - test_database - / "charleston" - / "output" - / "Scenarios" - / row["scenario created"] - ) - if not output_path.exists(): - output_path.mkdir(parents=True) - # Create dummy metrics file - dummy_metrics = pd.DataFrame( - { - "Description": "", - "Show In Metrics Table": "TRUE", - "Long Name": "", - "Value": damages_dummy[name], - }, - index=["ExpectedAnnualDamages"], - ) - dummy_metrics.to_csv( - output_path.joinpath(f"Infometrics_{row['scenario created']}.csv") - ) - - # Run benefit analysis with dummy data - benefit.cba() - - # Read results - results_path = ( - test_database / "charleston" / "output" / "Benefits" / benefit.attrs.name - ) - with open(results_path.joinpath("results.toml"), mode="rb") as fp: - results = tomli.load(fp) - - # get results - tot_benefits = results["benefits"] - tot_costs = results["costs"] - # get time-series - csv_results = pd.read_csv(results_path.joinpath("time_series.csv")) - tot_benefits2 = csv_results["benefits_discounted"].sum() - tot_costs2 = csv_results["costs_discounted"].sum() - - # assert if time-series and totals are consistent - assert tot_benefits == tot_benefits2 - assert tot_costs == tot_costs2 - - # assert if results are equal to the expected values based on the input - assert pytest.approx(tot_benefits, 2) == 963433925 - assert pytest.approx(tot_costs, 2) == 201198671 - assert pytest.approx(results["BCR"], 0.01) == 4.79 - assert pytest.approx(results["NPV"], 2) == 762235253 - assert pytest.approx(results["IRR"], 0.01) == 0.394 From 12e85a4c6f5f9c0730dd3dfa5470ef24e0b7515e Mon Sep 17 00:00:00 2001 From: Panos Athanasiou Date: Tue, 7 May 2024 15:28:47 +0200 Subject: [PATCH 07/10] small update for test data creation --- tests/test_object_model/test_benefit.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/tests/test_object_model/test_benefit.py b/tests/test_object_model/test_benefit.py index 2340a4775..64b8f60dd 100644 --- a/tests/test_object_model/test_benefit.py +++ b/tests/test_object_model/test_benefit.py @@ -247,12 +247,14 @@ def prepare_outputs(self, test_db_class, benefit_name): ) if not output_path.exists(): output_path.mkdir(parents=True) - # Creaty dummy fiat log - fiat_path = output_path.joinpath("Impacts", "fiat_model") - if not fiat_path.exists(): - fiat_path.mkdir(parents=True) - with open(fiat_path.joinpath("fiat.log"), "w") as f: - f.write("Geom calculation are done!") + # Create dummy building impact output csv file + fiat_path = output_path.joinpath( + "Impacts", f"Impacts_detailed_{row['scenario created']}.csv" + ) + if not fiat_path.parent.exists(): + fiat_path.parent.mkdir() + dummy_impacts = pd.DataFrame() + dummy_impacts.to_csv(fiat_path) # Create dummy metrics file dummy_metrics = pd.DataFrame( { From 0155a214102a9e75e6bc8bd354fd55bf744be0e8 Mon Sep 17 00:00:00 2001 From: Panos Athanasiou Date: Tue, 7 May 2024 17:48:49 +0200 Subject: [PATCH 08/10] small update --- tests/test_object_model/test_benefit.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_object_model/test_benefit.py b/tests/test_object_model/test_benefit.py index 64b8f60dd..3955eb245 100644 --- a/tests/test_object_model/test_benefit.py +++ b/tests/test_object_model/test_benefit.py @@ -80,7 +80,8 @@ def test_loadDict_fromTestDict_createBenefit(test_db): class TestBenefitScenariosNotCreated: # Fixture to create a Benefit object @pytest.fixture(scope="function") - def benefit_obj(self, test_db): + def benefit_obj(self, test_db_class): + test_db = test_db_class name = "benefit_raise_properties_2050" benefit_path = test_db.input_path.joinpath( "benefits", From 8f564658406ab3975d0834dc86400f182428a953 Mon Sep 17 00:00:00 2001 From: Panos Athanasiou Date: Tue, 7 May 2024 17:54:22 +0200 Subject: [PATCH 09/10] ruff fix --- tests/test_object_model/test_benefit.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_object_model/test_benefit.py b/tests/test_object_model/test_benefit.py index 3955eb245..c58e1a1ef 100644 --- a/tests/test_object_model/test_benefit.py +++ b/tests/test_object_model/test_benefit.py @@ -274,7 +274,8 @@ def prepare_outputs(self, test_db_class, benefit_name): for aggr_type in aggrs.keys(): aggr = aggrs[aggr_type] # Generate random distribution of damage per aggregation area - dmgs = np.random.random(len(aggr)) + generator = np.random.default_rng() + dmgs = generator.random(len(aggr)) dmgs = dmgs / dmgs.sum() * damages_dummy[name] dict0 = { From 9fc38a6694bca43e6624dfe6442671406b5c5d3d Mon Sep 17 00:00:00 2001 From: Panos Athanasiou Date: Tue, 21 May 2024 15:36:03 +0200 Subject: [PATCH 10/10] added a small test for saving a toml --- tests/test_object_model/test_benefit.py | 43 ++++++++++++++++--------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/tests/test_object_model/test_benefit.py b/tests/test_object_model/test_benefit.py index c58e1a1ef..5dfd7acf9 100644 --- a/tests/test_object_model/test_benefit.py +++ b/tests/test_object_model/test_benefit.py @@ -13,6 +13,20 @@ "benefit_without_costs": "benefit_raise_properties_2050_no_costs", } +_TEST_DICT = { + "name": "benefit_raise_properties_2080", + "description": "", + "event_set": "test_set", + "strategy": "elevate_comb_correct", + "projection": "all_projections", + "future_year": 2080, + "current_situation": {"projection": "current", "year": "2023"}, + "baseline_strategy": "no_measures", + "discount_rate": 0.07, + "implementation_cost": 200000000, + "annual_maint_cost": 100000, +} + # Create Benefit object using example benefit toml in test database def test_loadFile_fromTestBenefitToml_createBenefit(test_db): @@ -57,25 +71,24 @@ def test_loadFile_nonExistingFile_FileNotFoundError(test_db): # Create Benefit object using using a dictionary def test_loadDict_fromTestDict_createBenefit(test_db): - test_dict = { - "name": "benefit_raise_properties_2080", - "description": "", - "event_set": "test_set", - "strategy": "elevate_comb_correct", - "projection": "all_projections", - "future_year": 2080, - "current_situation": {"projection": "current", "year": "2023"}, - "baseline_strategy": "no_measures", - "discount_rate": 0.07, - "implementation_cost": 200000000, - "annual_maint_cost": 100000, - } - - benefit = Benefit.load_dict(test_dict, test_db.input_path) + benefit = Benefit.load_dict(_TEST_DICT, test_db.input_path) assert isinstance(benefit, IBenefit) +# Save a toml from a test benefit dictionary +def test_save_fromTestDict_saveToml(test_db): + benefit = Benefit.load_dict(_TEST_DICT, test_db.input_path) + output_path = test_db.input_path.joinpath( + "benefits", "test_benefit", "test_benefit.toml" + ) + if not output_path.parent.exists(): + output_path.parent.mkdir() + assert not output_path.exists() + benefit.save(output_path) + assert output_path.exists() + + # Tests for when the scenarios needed for the benefit analysis are not there yet class TestBenefitScenariosNotCreated: # Fixture to create a Benefit object