diff --git a/CHANGELOG.md b/CHANGELOG.md index e766013..9746430 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] ### Added +- Independent recovery analysis [#5](https://github.com/IN-CORE/pyincore-incubator/issues/5) - GitHub action to check linting [#16](https://github.com/IN-CORE/pyincore-incubator/issues/16) ### Fixed diff --git a/pyincore_incubator/analyses/independentrecovery/__init__.py b/pyincore_incubator/analyses/independentrecovery/__init__.py new file mode 100644 index 0000000..72a2faa --- /dev/null +++ b/pyincore_incubator/analyses/independentrecovery/__init__.py @@ -0,0 +1,9 @@ +# +# This program and the accompanying materials are made available under the +# terms of the Mozilla Public License v2.0 which accompanies this distribution, +# and is available at https://www.mozilla.org/en-US/MPL/2.0/ + + +from pyincore_incubator.analyses.independentrecovery.independentrecovery import ( + IndependentRecovery, +) diff --git a/pyincore_incubator/analyses/independentrecovery/independentrecovery.py b/pyincore_incubator/analyses/independentrecovery/independentrecovery.py new file mode 100644 index 0000000..502a5c9 --- /dev/null +++ b/pyincore_incubator/analyses/independentrecovery/independentrecovery.py @@ -0,0 +1,330 @@ +# Copyright (c) 2024 University of Illinois and others. All rights reserved. +# +# This program and the accompanying materials are made available under the +# terms of the Mozilla Public License v2.0 which accompanies this distribution, +# and is available at https://www.mozilla.org/en-US/MPL/2.0/ + +import numpy as np +import pandas as pd +import time + +from pyincore import BaseAnalysis + + +class IndependentRecovery(BaseAnalysis): + """ + This analysis computes the household housing recovery time considering the availability of their residential + buildings and housing vacancies in the community. Currently, supported hazards are tornadoes. + + The methodology examines the interdependent community recovery process across physical infrastructure and social + systems + + The outputs of this analysis is a CSV file with the modified history of housing recovery changes and the + calculated housing housing recovery time. + + Contributors + | Science: Wanting Lisa Wang, John W. van de Lindt, Elaina J. Sutley, and Sara Hamideh + | Implementation: Wanting Lisa Wang, and NCSA IN-CORE Dev Team + + Related publications + Wang, W., van de Lindt, J.W., Sutley, E. and Hamideh, S. "Interdependent Recovery Methodology for + Residential Buildings and Household Housing in Community Resilience Modeling." + ASCE OPEN: Multidisciplinary Journal of Civil Engineering (accepted). + + Args: + incore_client (IncoreClient): Service authentication. + + """ + + def __init__(self, incore_client): + super(IndependentRecovery, self).__init__(incore_client) + + def run(self): + """Executes the independent recovery analysis. + + Returns: + bool: True if successful, False otherwise. + + """ + # TODO: Start using seed + result_name = self.get_parameter("result_name") + + building_damage = self.get_input_dataset( + "building_damage" + ).get_dataframe_from_csv(low_memory=False) + household_inventory = self.get_input_dataset( + "housing_unit_inventory" + ).get_dataframe_from_csv(low_memory=False) + household_allocation = self.get_input_dataset( + "housing_unit_allocation" + ).get_dataframe_from_csv(low_memory=False) + household_dislocation = self.get_input_dataset( + "population_dislocation" + ).get_dataframe_from_csv(low_memory=False) + residential_recovery = self.get_input_dataset( + "residential_recovery" + ).get_dataframe_from_csv(low_memory=False) + housing_recovery = self.get_input_dataset( + "household_housing_recovery" + ).get_dataframe_from_csv(low_memory=False) + + # Returns dataframe + recovery_results = self.independent_recovery( + building_damage, + household_inventory, + household_allocation, + household_dislocation, + residential_recovery, + housing_recovery, + ) + self.set_result_csv_data("result", recovery_results, result_name, "dataframe") + + return True + + def independent_recovery( + self, + building_damage, + household_inventory, + household_allocation, + household_dislocation, + residential_recovery, + housing_recovery, + ): + """ + Calculates independent recovery of residential buildings and household housing + + Args: + building_damage (pd.DataFrame): Building damage results + household_inventory (pd.DataFrame): Household inventory + household_allocation (pd.DataFrame): Household allocation results + household_dislocation (pd.DataFrame): Household dislocation results + residential_recovery (pd.DataFrame): Residential building recovery results + housing_recovery (pd.DataFrame): Household housing recovery results + + Returns: + pd.DataFrame: household housing recovery sequences and time + + """ + + start_independent_recovery = time.process_time() + + merged_super = self.merge_damage_recovery_housing_building( + building_damage, + household_inventory, + household_dislocation, + residential_recovery, + housing_recovery, + ) + + modified_housing_recovery = self.modified_housing_recovery( + merged_super, household_allocation + ) + + housing_recovery_time = self.housing_recovery_time(modified_housing_recovery) + + end_start_independent_recovery = time.process_time() + print( + "Finished independent_recovery() in " + + str(end_start_independent_recovery - start_independent_recovery) + + " secs" + ) + + result = housing_recovery_time + + return result + + @staticmethod + def merge_damage_recovery_housing_building( + building_damage, + household_inventory, + household_dislocation, + residential_recovery, + housing_recovery, + ): + """ + Load CSV files to pandas Dataframes, merge them and drop unused columns. + + Args: + building_damage (pd.DataFrame): Building damage results + household_inventory (pd.DataFrame): Household inventory + household_dislocation (pd.DataFrame): Household dislocation results + residential_recovery (pd.DataFrame): Residential building recovery results + housing_recovery (pd.DataFrame): Household housing recovery results + + Returns: + pd.DataFrame: A merged table of all the inputs. + + """ + + building_damage = building_damage.loc[(building_damage["haz_expose"] == "yes")] + household_dislocation = household_dislocation.loc[ + (household_dislocation["plcname10"] == "Joplin") + & (household_dislocation["guid"].notnull()) + & (household_dislocation["huid"].notna()) + ] + + residential_recovery["bldgmean"] = round( + residential_recovery.drop(columns=["guid"]).mean(axis=1), 2 + ) + + housing_unit_inv_df = pd.merge( + left=household_inventory, + right=household_dislocation[ + ["huid", "guid", "DS_0", "DS_1", "DS_2", "DS_3", "prdis", "dislocated"] + ], + left_on="huid", + right_on="huid", + how="right", + ) + housing_unit_inv_df = pd.merge( + left=housing_unit_inv_df, + right=building_damage["guid"], + left_on="guid", + right_on="guid", + how="inner", + ) + housing_unit_inv_df = pd.merge( + left=housing_unit_inv_df, + right=housing_recovery.drop("guid", axis=1), + left_on="huid", + right_on="huid", + how="left", + ) + household = pd.merge( + left=housing_unit_inv_df, + right=residential_recovery[["guid", "bldgmean"]], + left_on="guid", + right_on="guid", + how="inner", + ) + + return household + + @staticmethod + def modified_housing_recovery(merged_super, household_allocation): + """Gets the modified housing unit sequences. + + Args: + merged_super (pd.DataFrame): A merged table of all the inputs described above + household_allocation (pd.DataFrame): Household allocation results + + Returns: + pd.DataFrame: Results of the modified housing unit sequences considering the availability of their + residential buildings and housing vacancies. + """ + + household_allocation = household_allocation.loc[ + (household_allocation["plcname10"] == "Joplin") + & (household_allocation["guid"].notnull()) + & (household_allocation["huid"].notna()) + ] + + for j in range(0, len(merged_super)): + rand = np.random.random() + for i in range(0, 90): + if ( + (merged_super.iloc[j, -91 + i] == 4) + and (4 * (i + 1) < merged_super.iloc[j, -91 + 90]) + and rand + > household_allocation["vacancy"].astype(bool).sum(axis=0) + / merged_super["ownershp"].count() + ): + merged_super.iloc[j, i + 4] = 3 + + # Construct a new DataFrame + modified_housing_recovery_results = merged_super + + return modified_housing_recovery_results + + @staticmethod + def housing_recovery_time(modified_housing_recovery): + """Gets household housing recovery time. + + Args: + modified_housing_recovery (pd.DataFrame): The modified housing unit sequences considering the availability + of their residential buildings and housing vacancies. + + Returns: + pd.DataFrame: Results of household housing recovery time + """ + modified_housing_recovery["hur"] = np.nan + modified_housing_recovery.insert(0, "hur", modified_housing_recovery.pop("hur")) + for j in range(0, len(modified_housing_recovery)): + for i in range(1, 90): + if modified_housing_recovery.iloc[j, -90 + i] == 4: + modified_housing_recovery["hur"][j] = i * 4 + break + if modified_housing_recovery.iloc[j, -90 + i] == 5: + modified_housing_recovery["hur"][j] = 360 + break + + housing_recovery_time_results = modified_housing_recovery + + return housing_recovery_time_results + + def get_spec(self): + """Get specifications of the residential building recovery analysis. + + Returns: + obj: A JSON object of specifications of the independent recovery analysis. + + """ + return { + "name": "independent-recovery", + "description": "calculate household housing recovery considering the availability of their residential " + "buildings and housing vacancies", + "input_parameters": [ + { + "id": "result_name", + "required": True, + "description": "name of the result", + "type": str, + } + ], + "input_datasets": [ + { + "id": "building_damage", + "required": True, + "description": "Building Damage Probability", + "type": ["ergo:buildingDamageVer4"], + }, + { + "id": "housing_unit_inventory", + "required": True, + "description": "Dataset containing detailed characteristics of the housing units.", + "type": ["incore:housingUnitInventory"], + }, + { + "id": "housing_unit_allocation", + "required": True, + "description": "Dataset containing the housing unit allocation.", + "type": ["incore:housingUnitAllocation"], + }, + { + "id": "population_dislocation", + "required": True, + "description": "Population dislocation.", + "type": ["incore:popDislocation"], + }, + { + "id": "residential_recovery", + "required": True, + "description": "Residential Building Recovery", + "type": ["incore:buildingRecovery"], + }, + { + "id": "household_housing_recovery", + "required": True, + "description": "Household Housing Recovery", + "type": ["incore:housingRecoveryHistory"], + }, + ], + "output_datasets": [ + { + "id": "result", + "description": "CSV file of household housing recovery considering the availability of their" + " residential buildings and housing vacancies", + "type": "incore:housingRecovery", + } + ], + } diff --git a/tests/pyincore_incubator/analyses/independentrecovery/test_independentrecovery.py b/tests/pyincore_incubator/analyses/independentrecovery/test_independentrecovery.py new file mode 100644 index 0000000..e20a295 --- /dev/null +++ b/tests/pyincore_incubator/analyses/independentrecovery/test_independentrecovery.py @@ -0,0 +1,51 @@ +from pyincore import IncoreClient +from pyincore_incubator.analyses.independentrecovery import IndependentRecovery +import pyincore.globals as pyglobals + + +def run_with_base_class(): + # Connect to IN-CORE service + client = IncoreClient(pyglobals.INCORE_API_DEV_URL) + client.clear_cache() + + independent_recovery = IndependentRecovery(client) + + building_dmg_id = "6824dff3ce15ce6bdf893282" + housing_inventory_id = "6824e19fce15ce6bdf89328a" + housing_unit_id = "6824e277ce15ce6bdf893293" + population_dislocation_id = "6824e49ace15ce6bdf8932a5" + residential_recovery_id = "6824e4c1ce15ce6bdf8932a6" + housing_recovery_id = "6824e4fdce15ce6bdf8932a7" + + independent_recovery.load_remote_input_dataset("building_damage", building_dmg_id) + independent_recovery.load_remote_input_dataset( + "housing_unit_inventory", housing_inventory_id + ) + + independent_recovery.load_remote_input_dataset( + "housing_unit_allocation", housing_unit_id + ) + + independent_recovery.load_remote_input_dataset( + "population_dislocation", population_dislocation_id + ) + + independent_recovery.load_remote_input_dataset( + "residential_recovery", residential_recovery_id + ) + independent_recovery.load_remote_input_dataset( + "household_housing_recovery", housing_recovery_id + ) + + # Specify the result name + result_name = "independent_recovery" + + # Set analysis parameters + independent_recovery.set_parameter("result_name", result_name) + + # Run the analysis (NOTE: with SettingWithCopyWarning) + independent_recovery.run_analysis() + + +if __name__ == "__main__": + run_with_base_class()