Repository: IN-CORE/pyincore (language: python)

Issue:
Add combined building loss for wind, wave and surge
Add building structural and content loss that handles wind, surge-wave and flood damage.

total loss = frame loss + roof loss + content loss + flooring/foundation loss
  


Discussion / hints:

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 670b62970..6d399d9f2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
 
 ## [1.9.0] - Unreleased
 
+### Added
+- Combined wind, surge-wave, and flood building loss [#276](https://github.com/IN-CORE/pyincore/issues/276)
+
 ### Changed
 - Rewrote clustering utility function to use flexible archetype column [#247](https://github.com/IN-CORE/pyincore/issues/247)
 - Made documentation containter to use requirements instead of environemt [#257](https://github.com/IN-CORE/pyincore/issues/257)
diff --git a/docs/source/modules.rst b/docs/source/modules.rst
index b73eab9f0..2b278a61d 100644
--- a/docs/source/modules.rst
+++ b/docs/source/modules.rst
@@ -42,9 +42,15 @@ analyses/capitalshocks
     :members:
 
 analyses/combinedwindwavesurgebuildingdamage
+============================================
 ..  autoclass:: pyincore.analyses.combinedwindwavesurgebuildingdamage.CombinedWindWaveSurgeBuildingDamage
     :members:
 
+analyses/combinedwindwavesurgebuildingloss
+============================================
+..  autoclass:: pyincore.analyses.combinedwindwavesurgebuildingloss.CombinedWindWaveSurgeBuildingLoss
+    :members:
+
 analyses/cumulativebuildingdamage
 =================================
 ..  autoclass:: cumulativebuildingdamage.cumulativebuildingdamage.CumulativeBuildingDamage
diff --git a/pyincore/analyses/combinedwindwavesurgebuildingloss/__init__.py b/pyincore/analyses/combinedwindwavesurgebuildingloss/__init__.py
new file mode 100644
index 000000000..47de6aefe
--- /dev/null
+++ b/pyincore/analyses/combinedwindwavesurgebuildingloss/__init__.py
@@ -0,0 +1,10 @@
+# Copyright (c) 2023 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/
+
+
+from pyincore.analyses.combinedwindwavesurgebuildingloss.combinedwindwavesurgebuildingloss import \
+    CombinedWindWaveSurgeBuildingLoss
+
diff --git a/pyincore/analyses/combinedwindwavesurgebuildingloss/combinedwindwavesurgebuildingloss.py b/pyincore/analyses/combinedwindwavesurgebuildingloss/combinedwindwavesurgebuildingloss.py
new file mode 100755
index 000000000..84919fadb
--- /dev/null
+++ b/pyincore/analyses/combinedwindwavesurgebuildingloss/combinedwindwavesurgebuildingloss.py
@@ -0,0 +1,209 @@
+# Copyright (c) 2023 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 pandas as pd
+from pyincore import BaseAnalysis
+import numpy as np
+
+
+class CombinedWindWaveSurgeBuildingLoss(BaseAnalysis):
+    """ 
+    This analysis computes the building structural and content loss from wind, flood and surge-wave damage
+
+    Contributors
+        | Science: Omar Nofal, John W. van de Lindt, Trung Do, Guirong Yan, Sara Hamideh, Daniel Cox, Joel Dietrich
+        | Implementation: Jiate Li, Chris Navarro and NCSA IN-CORE Dev Team
+
+    Related publications
+        Nofal, Omar & Lindt, John & Do, Trung & Yan, Guirong & Hamideh, Sara & Cox, Daniel & Dietrich, Joel. (2021).
+        Methodology for Regional Multi-Hazard Hurricane Damage and Risk Assessment. Journal of Structural Engineering.
+        147. 04021185. 10.1061/(ASCE)ST.1943-541X.0003144.
+        
+    Args:
+        incore_client (IncoreClient): Service authentication.
+    """
+
+    def __init__(self, incore_client):
+        super(CombinedWindWaveSurgeBuildingLoss, self).__init__(incore_client)
+
+    def run(self):
+        """Executes combined wind, wave, surge building loss analysis."""
+
+        # Read buildings
+        buildings = self.get_input_dataset("buildings").get_dataframe_from_shapefile()
+
+        # Read Building wind damage
+        wind_damage = self.get_input_dataset("wind_damage").get_dataframe_from_csv()
+
+        # Read Building surge-wave damage
+        surge_wave_damage = self.get_input_dataset("surge_wave_damage").get_dataframe_from_csv()
+
+        # Read Building flood damage
+        flood_damage = self.get_input_dataset("flood_damage").get_dataframe_from_csv()
+
+        # Read cumulative replacement cost ratio of building content
+        content_cost = self.get_input_dataset("content_cost").get_dataframe_from_csv()
+
+        # Read cumulative replacement cost ratio of structural damage
+        structure_cost = self.get_input_dataset("structural_cost").get_dataframe_from_csv()
+
+        combined_loss = self.get_combined_loss(wind_damage, surge_wave_damage, flood_damage, buildings, content_cost,
+                                               structure_cost)
+
+        # Create the result dataset
+        self.set_result_csv_data("result", combined_loss, self.get_parameter("result_name") + "_loss",
+                                 "dataframe")
+
+        return True
+    
+    def get_combined_loss(self, wind_dmg: pd.DataFrame, sw_dmg: pd.DataFrame, flood_dmg: pd.DataFrame, buildings:
+                          pd.DataFrame, content_cost: pd.DataFrame, structure_cost: pd.DataFrame):
+        """Calculates structural and content loss from wind, surge-wave and flood damage
+
+                Args:
+                    wind_dmg (pd.DataFrame): Table of wind damage for the building inventory
+                    sw_dmg (pd.DataFrame): Table of surge-wave damage for the building inventory
+                    flood_dmg (pd.DataFrame): Table of flood damage for the building inventory
+                    buildings (pd.DataFrame): Table of building attributes
+                    content_cost (pd.DataFrame): Table of content cost ratios for each archetype  
+                    structure_cost (pd.DataFrame): Table of structural cost ratio for each archetype and loss type 
+
+                Returns:
+                    pd.DataFrame: An table of structural and content loss for each building
+
+                """
+        # Rename the columns so there are no overlapping names among the hazard damage states
+        flood_dmg.rename(columns={'LS_0': 'f_LS_0', 'LS_1': 'f_LS_1', 'LS_2': 'f_LS_2', 'DS_0': 'f_DS_0',
+                                  'DS_1': 'f_DS_1', 'DS_2': 'f_DS_2', 'DS_3': 'f_DS_3', 'haz_expose': 'f_haz_expose'},
+                         inplace=True)
+        
+        sw_dmg.rename(columns={'LS_0': 'sw_LS_0', 'LS_1': 'sw_LS_1', 'LS_2': 'sw_LS_2', 'DS_0': 'sw_DS_0',
+                               'DS_1': 'sw_DS_1', 'DS_2': 'sw_DS_2', 'DS_3': 'sw_DS_3', 'haz_expose': 
+                                   'sw_haz_expose'}, inplace=True)
+
+        wind_dmg.rename(columns={'LS_0': 'w_LS_0', 'LS_1': 'w_LS_1', 'LS_2': 'w_LS_2', 'DS_0': 'w_DS_0',
+                                 'DS_1': 'w_DS_1', 'DS_2': 'w_DS_2', 'DS_3': 'w_DS_3', 'haz_expose': 'w_haz_expose'},
+                        inplace=True)
+
+        # Combine all three sets of damages into a single data frame
+        combined_df = pd.merge(pd.merge(wind_dmg, sw_dmg, on='guid'), flood_dmg, on='guid')
+
+        # Add the flood archetype to the combined damage since this is used to find loss multipliers
+        new_combined_df = pd.merge(combined_df, buildings[["guid", "arch_flood"]], on="guid")
+
+        # Create a result data frame for the loss calculations
+        loss_df = new_combined_df[['guid']].copy()
+
+        # Compute content and structural loss
+        loss_df['cont_loss'] = new_combined_df.apply(lambda row: np.dot(row[['f_DS_0', 'f_DS_1', 'f_DS_2']],
+                                                                        content_cost.iloc[int(row['arch_flood']-1),
+                                                                        1:4]), axis=1)
+
+        loss_df['roof_loss'] = new_combined_df.apply(lambda row: np.dot(row[['w_DS_0', 'w_DS_1', 'w_DS_2', 'w_DS_3']],
+                                                                        np.array([0.15, 0.5, 0.75, 1])) *
+                                                     structure_cost.loc[int(row['arch_flood']-1), "Roofing"], axis=1)
+
+        loss_df['ff_loss'] = new_combined_df.apply(lambda row: np.dot(row[['sw_DS_0', 'sw_DS_1', 'sw_DS_2',
+                                                                           'sw_DS_3']], np.array([0.1, 0.5, 0.75, 1])) *
+                                                   structure_cost.loc[int(row['arch_flood'] - 1),
+                                                   "Flooring and Foundation"], axis=1)
+
+        # Computes frame loss from the dominant hazard between wind and surge-wave intensities
+        def compute_frame_loss(row):
+            if row['w_DS_3'] >= row['sw_DS_3']:
+                return np.dot(row[['w_DS_0', 'w_DS_1', 'w_DS_2', 'w_DS_3']],np.array([0.25, 0.5, 0.75, 1])) * \
+                    structure_cost.loc[int(row['arch_flood'] - 1), "Wood Framing"]
+            elif row['sw_DS_3'] > row['w_DS_3']:
+                return np.dot(row[['sw_DS_0', 'sw_DS_1', 'sw_DS_2', 'sw_DS_3']], np.array([0.25, 0.5, 0.75, 1])) * \
+                    structure_cost.loc[int(row['arch_flood'] - 1), "Wood Framing"]
+            else:
+                # If one or both are nan, they can't be compared
+                if pd.isnull(row['sw_DS_3']) and pd.isnull(row['w_DS_3']):
+                    return 0
+                elif not pd.isnull(row['sw_DS_3']):
+                    return np.dot(row[['sw_DS_0', 'sw_DS_1', 'sw_DS_2', 'sw_DS_3']], np.array([0.25, 0.5, 0.75, 1])) * \
+                                  structure_cost.loc[int(row['arch_flood'] - 1), "Wood Framing"]
+                else:
+                    return np.dot(row[['w_DS_0', 'w_DS_1', 'w_DS_2', 'w_DS_3']], np.array([0.25, 0.5, 0.75,
+                                                                                           1])) * structure_cost.loc[
+                        int(row['arch_flood'] - 1), "Wood Framing"]
+
+        loss_df['frame_loss'] = new_combined_df.apply(lambda row: compute_frame_loss(row), axis=1)
+
+        # Fill NA values with 0 otherwise we can't compute the total loss since empty are NaN values and won't add
+        loss_df.fillna(0, inplace=True)
+        loss_df['total_loss'] = loss_df.apply(lambda row: row['cont_loss'] + row['frame_loss'] + row[
+            'roof_loss'] + row['ff_loss'], axis=1)
+
+        return loss_df
+    
+    def get_spec(self):
+        """Get specifications of the combined wind, wave, and surge building loss analysis.
+
+        Returns:
+            obj: A JSON object of specifications of the combined wind, wave, and surge building damage analysis.
+
+        """
+        return {
+            'name': 'combined-wind-wave-surge-building-loss',
+            'description': 'Combined wind wave and surge building loss analysis',
+            'input_parameters': [
+                {
+                    'id': 'result_name',
+                    'required': True,
+                    'description': 'result dataset name',
+                    'type': str
+                },
+            ],
+            'input_datasets': [
+                {
+                    'id': 'buildings',
+                    'required': True,
+                    'description': 'Building Inventory',
+                    'type': ['ergo:buildingInventoryVer4', 'ergo:buildingInventoryVer5',
+                             'ergo:buildingInventoryVer6', 'ergo:buildingInventoryVer7'],
+                },
+                {
+                    'id': 'wind_damage',
+                    'required': True,
+                    'description': 'Wind damage result that has damage intervals in it',
+                    'type': ['ergo:buildingDamageVer6']
+                },
+                {
+                    'id': 'surge_wave_damage',
+                    'required': True,
+                    'description': 'Surge-wave damage result that has damage intervals in it',
+                    'type': ['ergo:buildingDamageVer6']
+                },
+                {
+                    'id': 'flood_damage',
+                    'required': True,
+                    'description': 'Flood damage result that has damage intervals in it',
+                    'type': ['ergo:buildingDamageVer6']
+                },
+                {
+                    'id': 'structural_cost',
+                    'required': True,
+                    'description': 'Structural cost ratio for each archetype and loss type',
+                    'type': ['incore:structuralCostRatio']
+                },
+                {
+                    'id': 'content_cost',
+                    'required': True,
+                    'description': 'Content cost ratio for each archetype and damage state',
+                    'type': ['incore:contentCostRatio']
+                }
+
+            ],
+            'output_datasets': [
+                {
+                    'id': 'result',
+                    'parent_type': 'buildings',
+                    'description': 'CSV file of building structural and content loss',
+                    'type': 'incore:buildingLoss'
+                }
+            ]
+        }


Test patch:
diff --git a/tests/pyincore/analyses/combinedwindwavesurgebuildingloss/test_combinedwindwavesurgebuildingloss.py b/tests/pyincore/analyses/combinedwindwavesurgebuildingloss/test_combinedwindwavesurgebuildingloss.py
new file mode 100644
index 000000000..7a6c6c38e
--- /dev/null
+++ b/tests/pyincore/analyses/combinedwindwavesurgebuildingloss/test_combinedwindwavesurgebuildingloss.py
@@ -0,0 +1,87 @@
+from pyincore import IncoreClient, FragilityService, MappingSet
+from pyincore.analyses.combinedwindwavesurgebuildingloss import CombinedWindWaveSurgeBuildingLoss
+from pyincore.analyses.buildingdamage import BuildingDamage
+import pyincore.globals as pyglobals
+from timeit import default_timer as timer
+
+
+def run_with_base_class():
+
+    client = IncoreClient(pyglobals.INCORE_API_DEV_URL)
+
+    # Galveston building inventory
+    bldg_dataset_id = "63053e74c8f8b7614f6cff9f"
+
+    # Hazard information
+    hazard_id = "5fa472033c1f0c73fe81461a"
+    hazard_type = "hurricane"
+
+    # Fragility Service
+    fragility_service = FragilityService(client)
+
+    # Surge-wave mapping
+    mapping_id = "6303e51bd76c6d0e1f6be080"
+    mapping_set = MappingSet(fragility_service.get_mapping(mapping_id))
+
+    # surge-wave building damage
+    sw_bldg_dmg = BuildingDamage(client)
+    sw_bldg_dmg.load_remote_input_dataset("buildings", bldg_dataset_id)
+    sw_bldg_dmg.set_input_dataset('dfr3_mapping_set', mapping_set)
+    sw_bldg_dmg.set_parameter("result_name", "Galveston-sw-dmg")
+    sw_bldg_dmg.set_parameter("hazard_type", hazard_type)
+    sw_bldg_dmg.set_parameter("hazard_id", hazard_id)
+    sw_bldg_dmg.set_parameter("num_cpu", 4)
+    sw_bldg_dmg.run_analysis()
+
+    # wind mapping
+    mapping_id = "62fef3a6cef2881193f2261d"
+    mapping_set = MappingSet(fragility_service.get_mapping(mapping_id))
+
+    # wind building damage
+    w_bldg_dmg = BuildingDamage(client)
+    w_bldg_dmg.load_remote_input_dataset("buildings", bldg_dataset_id)
+    w_bldg_dmg.set_input_dataset('dfr3_mapping_set', mapping_set)
+    w_bldg_dmg.set_parameter("result_name", "Galveston-wind-dmg")
+    w_bldg_dmg.set_parameter("hazard_type", hazard_type)
+    w_bldg_dmg.set_parameter("hazard_id", hazard_id)
+    w_bldg_dmg.set_parameter("num_cpu", 4)
+    w_bldg_dmg.run_analysis()
+
+    # flood mapping
+    mapping_id = "62fefd688a30d30dac57bbd7"
+    mapping_set = MappingSet(fragility_service.get_mapping(mapping_id))
+
+    # flood building damage
+    f_bldg_dmg = BuildingDamage(client)
+    f_bldg_dmg.load_remote_input_dataset("buildings", bldg_dataset_id)
+    f_bldg_dmg.set_input_dataset('dfr3_mapping_set', mapping_set)
+    f_bldg_dmg.set_parameter("result_name", "Galveston-flood-dmg")
+    f_bldg_dmg.set_parameter("hazard_type", hazard_type)
+    f_bldg_dmg.set_parameter("hazard_id", hazard_id)
+    f_bldg_dmg.set_parameter("num_cpu", 4)
+    f_bldg_dmg.run_analysis()
+
+    # Get damage outputs from different hazards
+    surge_wave_damage = sw_bldg_dmg.get_output_dataset("ds_result")
+    wind_damage = w_bldg_dmg.get_output_dataset("ds_result")
+    flood_damage = f_bldg_dmg.get_output_dataset("ds_result")
+
+    start = timer()
+
+    # Combined building loss from each hazard
+    combined_bldg_loss = CombinedWindWaveSurgeBuildingLoss(client)
+    combined_bldg_loss.load_remote_input_dataset("buildings", bldg_dataset_id)
+    combined_bldg_loss.set_input_dataset("surge_wave_damage", surge_wave_damage)
+    combined_bldg_loss.set_input_dataset("wind_damage", wind_damage)
+    combined_bldg_loss.set_input_dataset("flood_damage", flood_damage)
+    combined_bldg_loss.load_remote_input_dataset("structural_cost", "63fd15716d3b2a308ba914c8")
+    combined_bldg_loss.load_remote_input_dataset("content_cost", "63fd16956d3b2a308ba9269a")
+    combined_bldg_loss.set_parameter("result_name", "Galveston")
+    combined_bldg_loss.run_analysis()
+
+    end = timer()
+    print(f'Elapsed time: {end - start:.3f} seconds')
+
+
+if __name__ == '__main__':
+    run_with_base_class()