From 6e4e98cb6ed6c8f3a033fa82cfb7687cd9c816d7 Mon Sep 17 00:00:00 2001 From: Adam Zsarnoczay <33822153+zsarnoczay@users.noreply.github.com> Date: Wed, 10 Sep 2025 11:54:44 -0700 Subject: [PATCH 1/4] feat: relax input validation rules for Hazus assessments Relax validation constraints for seismic and flood assessments to improve usability while maintaining analysis accuracy: - Allow HeightClass attribute for seismic structural systems (W1, W2, S3, PC1, MH) that don't require it in Hazus methodology - Remove PlanArea field from auto-populated seismic configuration as it's no longer needed - Allow RES1 occupancy buildings to have more than 3 stories in flood assessments, aligning with FEMA technical manual interpretation These changes provide users more flexibility in input specification while ensuring unused attributes don't affect analysis results. --- flood/building/portfolio/Hazus v6.1/FloodRulesets.py | 6 +++--- seismic/building/portfolio/Hazus v6.1/input_schema.json | 8 -------- seismic/building/portfolio/Hazus v6.1/pelicun_config.py | 7 +++++-- 3 files changed, 8 insertions(+), 13 deletions(-) diff --git a/flood/building/portfolio/Hazus v6.1/FloodRulesets.py b/flood/building/portfolio/Hazus v6.1/FloodRulesets.py index 2410f419..0f6a7061 100644 --- a/flood/building/portfolio/Hazus v6.1/FloodRulesets.py +++ b/flood/building/portfolio/Hazus v6.1/FloodRulesets.py @@ -218,7 +218,7 @@ def FL_config(BIM): else: fl_config = 'structural.108.RES1.FIA_Modified.two_floors.with_basement.a_zone' - elif BIM['NumberOfStories'] == 3: + elif BIM['NumberOfStories'] >= 3: if basement_type == 'bn': fl_config = 'structural.109.RES1.FIA.three_or_more_floors.no_basement.a_zone' else: @@ -243,7 +243,7 @@ def FL_config(BIM): else: fl_config = 'structural.116.RES1.FIA_Modified.two_floors.with_basement.v_zone' - elif BIM['NumberOfStories'] == 3: + elif BIM['NumberOfStories'] >= 3: if basement_type == 'bn': fl_config = 'structural.117.RES1.FIA.three_or_more_floors.no_basement.v_zone' else: @@ -271,7 +271,7 @@ def FL_config(BIM): else: fl_config = 'structural.108.RES1.FIA_Modified.two_floors.with_basement.a_zone' - elif BIM['NumberOfStories'] == 3: + elif BIM['NumberOfStories'] >= 3: if basement_type == 'bn': fl_config = 'structural.109.RES1.FIA.three_or_more_floors.no_basement.a_zone' else: diff --git a/seismic/building/portfolio/Hazus v6.1/input_schema.json b/seismic/building/portfolio/Hazus v6.1/input_schema.json index 60ff872f..63525d28 100644 --- a/seismic/building/portfolio/Hazus v6.1/input_schema.json +++ b/seismic/building/portfolio/Hazus v6.1/input_schema.json @@ -78,14 +78,6 @@ } }, "required": ["HeightClass"] - }, - "else": { - "properties": { - "HeightClass": { - "type": "null" - } - }, - "required": [] } }, { diff --git a/seismic/building/portfolio/Hazus v6.1/pelicun_config.py b/seismic/building/portfolio/Hazus v6.1/pelicun_config.py index 39f99613..303b15c8 100644 --- a/seismic/building/portfolio/Hazus v6.1/pelicun_config.py +++ b/seismic/building/portfolio/Hazus v6.1/pelicun_config.py @@ -124,6 +124,10 @@ def auto_populate(aim): height_class_map = {"Low-Rise": "L", "Mid-Rise": "M", "High-Rise": "H"} height_class_data = gi.get("HeightClass") + # some structural systems have no height class defined in Hazus + if structure_type in ['W1', 'W2', 'S3', 'PC1', 'MH']: + height_class_data = None + if gi.get("LifelineFacility"): if height_class_data is not None: @@ -183,8 +187,7 @@ def auto_populate(aim): "ComponentAssignmentFile": "CMP_QNT.csv", "ComponentDatabase": "Hazus Earthquake - Buildings", "NumberOfStories": 1, - "OccupancyType": f"{occupancy_type}", - "PlanArea": "1", # TODO(adamzs): check if this is even needed + "OccupancyType": f"{occupancy_type}" }, "Damage": {"DamageProcess": "Hazus Earthquake - Buildings"}, "Demands": {}, From ae0c25d4cbe490904bba894f72bb2a2901bb9bde Mon Sep 17 00:00:00 2001 From: Adam Zsarnoczay <33822153+zsarnoczay@users.noreply.github.com> Date: Thu, 11 Sep 2025 22:42:27 -0700 Subject: [PATCH 2/4] chore: Apply code quality improvements across DamageAndLossLibrary This commit addresses code quality issues identified by ruff format, ruff check, and codespell tools across the entire codebase. The changes maintain existing functionality while improving code consistency, readability, and adherence to Python best practices. ## Changes Made: ### Code Formatting and Linting (ruff format & ruff check) - Applied consistent code formatting across 15 Python files - Fixed docstring formatting and missing docstring issues (D100, D211, D212) - Cleaned up import statements and unused code - Standardized quote usage and line spacing ### Configuration Updates - Updated `pyproject.toml` to include additional ruff ignore rules (PLR2004, D100) - Updated `.gitignore` for better file exclusion patterns ## Testing: All changes have been validated to ensure: - Code passes ruff format checks - Code passes ruff linting rules - Spelling issues resolved via codespell - No breaking changes to existing functionality --- .gitignore | 1 + doc/source/_extensions/visuals.py | 12 +- .../portfolio/Hazus v6.1/FloodRulesets.py | 249 +++--- .../portfolio/Hazus v6.1/pelicun_config.py | 195 ++--- .../data_sources/generate_library_files.py | 90 +- .../Hazus v5.1 coupled/pelicun_config.py | 775 +++++++++--------- pyproject.toml | 4 +- .../data_sources/generate_library_files.py | 12 +- .../data_sources/generate_library_files.py | 12 +- .../portfolio/Hazus v6.1/pelicun_config.py | 145 ++-- .../data_sources/generate_library_files.py | 6 +- .../subassembly/Hazus v5.1/pelicun_config.py | 83 +- .../portfolio/Hazus v5.1/pelicun_config.py | 32 +- .../portfolio/Hazus v5.1/pelicun_config.py | 241 ++++-- .../portfolio/Hazus v6.1/pelicun_config.py | 39 +- 15 files changed, 1022 insertions(+), 874 deletions(-) diff --git a/.gitignore b/.gitignore index 4fa32410..73fad9b8 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ /data_sources/HAZUS_MH_4.2_HU/session.dill /doc/build/ *.pyc +*.ipynb /.ropeproject/ /doc/cache/ /doc/source/dl_doc/ diff --git a/doc/source/_extensions/visuals.py b/doc/source/_extensions/visuals.py index d18dddab..99d6dfa9 100644 --- a/doc/source/_extensions/visuals.py +++ b/doc/source/_extensions/visuals.py @@ -1,4 +1,4 @@ -# # noqa: D100 +# # Copyright (c) 2023 Leland Stanford Junior University # Copyright (c) 2023 The Regents of the University of California # @@ -280,7 +280,7 @@ def plot_fragility(comp_db_path, output_path, create_zip='0'): # noqa: C901, D1 table_vals[1] = np.array(ds_list) font_size = 16 - if ds_i > 8: # noqa: PLR2004 + if ds_i > 8: font_size = 8.5 fig.add_trace( @@ -317,7 +317,7 @@ def plot_fragility(comp_db_path, output_path, create_zip='0'): # noqa: C901, D1 ds_offset = 0.086 info_font_size = 10 - if ds_i > 8: # noqa: PLR2004 + if ds_i > 8: x_loc = 0.4928 y_loc = 0.705 + 0.123 ds_offset = 0.0455 @@ -575,9 +575,9 @@ def plot_repair( # noqa: C901, PLR0912, PLR0915 conseq_val = float(val) if conseq_val < 1: table_vals[1][ds_i] = f'{conseq_val:.4g}' - elif conseq_val < 10: # noqa: PLR2004 + elif conseq_val < 10: table_vals[1][ds_i] = f'{conseq_val:.3g}' - elif conseq_val < 1e6: # noqa: PLR2004 + elif conseq_val < 1e6: table_vals[1][ds_i] = f'{conseq_val:.0f}' else: table_vals[1][ds_i] = f'{conseq_val:.3g}' @@ -594,7 +594,7 @@ def plot_repair( # noqa: C901, PLR0912, PLR0915 ] # converted simultaneous damage models might have a lot of DSs - if table_vals.shape[1] > 8: # noqa: PLR2004 + if table_vals.shape[1] > 8: lots_of_ds = True else: lots_of_ds = False diff --git a/flood/building/portfolio/Hazus v6.1/FloodRulesets.py b/flood/building/portfolio/Hazus v6.1/FloodRulesets.py index 0f6a7061..a6e8e064 100644 --- a/flood/building/portfolio/Hazus v6.1/FloodRulesets.py +++ b/flood/building/portfolio/Hazus v6.1/FloodRulesets.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (c) 2018 Leland Stanford Junior University # Copyright (c) 2018 The Regents of the University of California @@ -45,50 +44,51 @@ import numpy as np -def FL_config(BIM): + +def configure_flood_vulnerability(bim): # noqa: C901, PLR0912, PLR0915 """ - Rules to identify the flood vunerability category + Rules to identify the flood vulnerability category. Parameters ---------- - BIM: dictionary + bim: dictionary Information about the building characteristics. Returns ------- config: str - A string that identifies a specific configration within this buidling + A string that identifies a specific configuration within this building class. """ - year = BIM['YearBuilt'] # just for the sake of brevity + year = bim['YearBuilt'] # just for the sake of brevity # Flood Type - if BIM['FloodZone'] == 'AO': - flood_type = 'raz' # Riverine/A-Zone - elif BIM['FloodZone'] in ['A', 'AE', 'AH']: - flood_type = 'caz' # Costal-Zone A - elif BIM['FloodZone'].startswith('V'): - flood_type = 'cvz' # Costal-Zone V + if bim['FloodZone'] == 'AO': + flood_type = 'raz' # Riverine/A-Zone + elif bim['FloodZone'] in ['A', 'AE', 'AH']: + flood_type = 'caz' # Costal-Zone A + elif bim['FloodZone'].startswith('V'): + flood_type = 'cvz' # Costal-Zone V else: - flood_type = 'caz' # Default + flood_type = 'caz' # Default # First Floor Elevation (FFE) - # For A Zone, top of finished floor; - # for V Zone, bottom of floor beam of lowest floor; - # define X based on typical depth of girders assuming bottom of door is used to - # estimate first floor ht - # (https://www.apawood.org/Data/Sites/1/documents/raised-wood-floor-foundations-guide.pdf) + # For A Zone, top of finished floor; + # for V Zone, bottom of floor beam of lowest floor; + # define X based on typical depth of girders assuming bottom of door is used to + # estimate first floor ht + # (https://www.apawood.org/Data/Sites/1/documents/raised-wood-floor-foundations-guide.pdf) # -- take X=1 ft as average value of different options (depths) if flood_type in ['raz', 'caz']: - FFE = BIM['FirstFloorElevation'] + first_floor_elevation = bim['FirstFloorElevation'] else: - FFE = BIM['FirstFloorElevation'] - 1.0 + first_floor_elevation = bim['FirstFloorElevation'] - 1.0 # noqa: F841 # PostFIRM - #Based on FEMA FLOOD INSURANCE STUDY NUMBER 34001CV000A (Atlantic County, NJ ) + # Based on FEMA FLOOD INSURANCE STUDY NUMBER 34001CV000A (Atlantic County, NJ ) # Version Number 2.1.1.1 (See Table 9) # Yes=Post-FIRM, No=Pre-FIRM - PostFIRM_year_by_city = { + post_firm_year_by_city = { 'Absecon': 1976, 'Atlantic': 1971, 'Brigantine': 1971, @@ -111,23 +111,23 @@ def FL_config(BIM): 'Port Republic': 1983, 'Somers Point': 1982, 'Ventnor City': 1971, - 'Weymouth':1979 + 'Weymouth': 1979, } - if BIM['City'] in PostFIRM_year_by_city: - PostFIRM_year = PostFIRM_year_by_city[BIM['City']] - PostFIRM = year > PostFIRM_year + if bim['City'] in post_firm_year_by_city: + post_firm_construction_year = post_firm_year_by_city[bim['City']] + is_post_firm_construction = year > post_firm_construction_year else: - PostFIRM = False + is_post_firm_construction = False # Basement Type - if BIM['SplitLevel'] and (BIM['FoundationType'] == 3504): - basement_type = 'spt' # Split-Level Basement - elif BIM['FoundationType'] in [3501, 3502, 3503, 3505, 3506, 3507]: - basement_type = 'bn' # No Basement - elif (not BIM['SplitLevel']) and (BIM['FoundationType'] == 3504): - basement_type = 'bw' # Basement + if bim['SplitLevel'] and (bim['FoundationType'] == 3504): + basement_type = 'spt' # Split-Level Basement + elif bim['FoundationType'] in [3501, 3502, 3503, 3505, 3506, 3507]: + basement_type = 'bn' # No Basement + elif (not bim['SplitLevel']) and (bim['FoundationType'] == 3504): + basement_type = 'bw' # Basement else: - basement_type = 'bw' # Default + basement_type = 'bw' # Default # Duration # The New Orleans District has developed expert opinion damage functions for @@ -138,32 +138,31 @@ def FL_config(BIM): # • Hurricane flooding, long duration (one week), salt water # • Hurricane flooding, short duration (one day), salt water # So everything we do in NJ is short duration according to the damage curves. - dur = 'short' + dur = 'short' # noqa: F841 # Occupancy Type - if BIM['OccupancyClass'] == 'RES1': - if BIM['NumberOfStories'] == 1: + if bim['OccupancyClass'] == 'RES1': + if bim['NumberOfStories'] == 1: if flood_type == 'raz': occupancy_type = 'SF1XA' elif flood_type == 'cvz': occupancy_type = 'SF1XV' - else: - if basement_type == 'nav': - if flood_type == 'raz': - occupancy_type = 'SF2XA' - elif flood_type == 'cvz': - occupancy_type = 'SF2XV' - elif basement_type == 'bmt': - if flood_type == 'raz': - occupancy_type = 'SF2BA' - elif flood_type == 'cvz': - occupancy_type = 'SF2BV' - elif basement_type == 'spt': - if flood_type == 'raz': - occupancy_type = 'SF2SA' - elif flood_type == 'cvz': - occupancy_type = 'SF2SV' - elif 'RES3' in BIM['OccupancyClass']: + elif basement_type == 'nav': + if flood_type == 'raz': + occupancy_type = 'SF2XA' + elif flood_type == 'cvz': + occupancy_type = 'SF2XV' + elif basement_type == 'bmt': + if flood_type == 'raz': + occupancy_type = 'SF2BA' + elif flood_type == 'cvz': + occupancy_type = 'SF2BV' + elif basement_type == 'spt': + if flood_type == 'raz': + occupancy_type = 'SF2SA' + elif flood_type == 'cvz': + occupancy_type = 'SF2SV' + elif 'RES3' in bim['OccupancyClass']: occupancy_type = 'APT' else: ap_ot = { @@ -192,72 +191,77 @@ def FL_config(BIM): 'GOV1': 'CITY', 'GOV2': 'EMERG', 'EDU1': 'SCHOOL', - 'EDU2': 'SCHOOL' + 'EDU2': 'SCHOOL', } - occupancy_type = ap_ot[BIM['OccupancyClass']] - + occupancy_type = ap_ot[bim['OccupancyClass']] # noqa: F841 fl_config = None - if BIM['OccupancyClass'] == 'RES1': + if bim['OccupancyClass'] == 'RES1': if flood_type == 'raz': - if BIM['SplitLevel']: + if bim['SplitLevel']: if basement_type == 'bn': - fl_config = 'structural.111.RES1.FIA.split_level.no_basement.a_zone' + fl_config = ( + 'structural.111.RES1.FIA.split_level.no_basement.a_zone' + ) else: fl_config = 'structural.112.RES1.FIA_Modified.split_level.with_basement.a_zone' - elif BIM['NumberOfStories'] == 1: + elif bim['NumberOfStories'] == 1: if basement_type == 'bn': fl_config = 'structural.129.RES1.USACE_IWR.one_story.no_basement' else: fl_config = 'structural.704.RES1.BCAR_Jan_2011.one_story.with_basement.b14' - elif BIM['NumberOfStories'] == 2: + elif bim['NumberOfStories'] == 2: if basement_type == 'bn': - fl_config = 'structural.107.RES1.FIA.two_floors.no_basement.a_zone' + fl_config = ( + 'structural.107.RES1.FIA.two_floors.no_basement.a_zone' + ) else: fl_config = 'structural.108.RES1.FIA_Modified.two_floors.with_basement.a_zone' - elif BIM['NumberOfStories'] >= 3: + elif bim['NumberOfStories'] >= 3: if basement_type == 'bn': fl_config = 'structural.109.RES1.FIA.three_or_more_floors.no_basement.a_zone' else: fl_config = 'structural.110.RES1.FIA_Modified.three_or_more_floors.with_basement.a_zone' elif flood_type == 'cvz': - if BIM['SplitLevel']: + if bim['SplitLevel']: if basement_type == 'bn': fl_config = 'structural.658.RES1.BCAR_Jan_2011.all_floors.slab_no_basement.coastal_a_or_v_zone' else: fl_config = 'structural.120.RES1.FIA_Modified.split_level.with_basement.v_zone' - elif BIM['NumberOfStories'] == 1: + elif bim['NumberOfStories'] == 1: if basement_type == 'bn': fl_config = 'structural.658.RES1.BCAR_Jan_2011.all_floors.slab_no_basement.coastal_a_or_v_zone' else: fl_config = 'structural.114.RES1.FIA_Modified.one_floor.with_basement.v_zone' - elif BIM['NumberOfStories'] == 2: + elif bim['NumberOfStories'] == 2: if basement_type == 'bn': - fl_config = 'structural.115.RES1.FIA.two_floors.no_basement.v_zone' + fl_config = ( + 'structural.115.RES1.FIA.two_floors.no_basement.v_zone' + ) else: fl_config = 'structural.116.RES1.FIA_Modified.two_floors.with_basement.v_zone' - elif BIM['NumberOfStories'] >= 3: + elif bim['NumberOfStories'] >= 3: if basement_type == 'bn': fl_config = 'structural.117.RES1.FIA.three_or_more_floors.no_basement.v_zone' else: fl_config = 'structural.118.RES1.FIA_Modified.three_or_more_floors.with_basement.v_zone' elif flood_type == 'caz': - if BIM['SplitLevel']: + if bim['SplitLevel']: if basement_type == 'bn': # copied from Coastal V zone as per Hazus guidelines fl_config = 'structural.658.RES1.BCAR_Jan_2011.all_floors.slab_no_basement.coastal_a_or_v_zone' else: fl_config = 'structural.112.RES1.FIA_Modified.split_level.with_basement.a_zone' - elif BIM['NumberOfStories'] == 1: + elif bim['NumberOfStories'] == 1: if basement_type == 'bn': # copied from Coastal V zone as per Hazus guidelines fl_config = 'structural.658.RES1.BCAR_Jan_2011.all_floors.slab_no_basement.coastal_a_or_v_zone' @@ -265,21 +269,22 @@ def FL_config(BIM): # copied from Coastal V zone as per Hazus guidelines fl_config = 'structural.114.RES1.FIA_Modified.one_floor.with_basement.v_zone' - elif BIM['NumberOfStories'] == 2: + elif bim['NumberOfStories'] == 2: if basement_type == 'bn': - fl_config = 'structural.107.RES1.FIA.two_floors.no_basement.a_zone' + fl_config = ( + 'structural.107.RES1.FIA.two_floors.no_basement.a_zone' + ) else: fl_config = 'structural.108.RES1.FIA_Modified.two_floors.with_basement.a_zone' - elif BIM['NumberOfStories'] >= 3: + elif bim['NumberOfStories'] >= 3: if basement_type == 'bn': fl_config = 'structural.109.RES1.FIA.three_or_more_floors.no_basement.a_zone' else: fl_config = 'structural.110.RES1.FIA_Modified.three_or_more_floors.with_basement.a_zone' - - elif BIM['OccupancyClass'] == 'RES2': - if BIM['NumberOfStories'] == 1: + elif bim['OccupancyClass'] == 'RES2': + if bim['NumberOfStories'] == 1: if flood_type == 'rvz': fl_config = 'structural.189.RES2.FIA.mobile_home.a_zone' @@ -292,105 +297,113 @@ def FL_config(BIM): elif flood_type == 'caz': fl_config = 'structural.189.RES2.FIA.mobile_home.a_zone' - elif 'RES3' in BIM['OccupancyClass']: - + elif 'RES3' in bim['OccupancyClass']: # the following rules are used for all flood-types as a default and replaced with a # more appropriate one if possible - if basement_type =='bn': + if basement_type == 'bn': fl_config = 'structural.204.RES3.USACE_Chicago.apartment_unit_grade' else: fl_config = 'structural.205.RES3.USACE_Chicago.apartment_unit_sub_grade' if flood_type == 'cvz': - if BIM['NumberOfStories'] in [1, 2]: + if bim['NumberOfStories'] in [1, 2]: if basement_type == 'bn': - if BIM['OccupancyClass'] == 'RES3A': + if bim['OccupancyClass'] == 'RES3A': fl_config = 'structural.659.RES3A.BCAR_Jan_2011.1to2_stories.slab_no_basement.coastal_a_or_v_zone' - elif BIM['OccupancyClass'] == 'RES3B': + elif bim['OccupancyClass'] == 'RES3B': fl_config = 'structural.660.RES3B.BCAR_Jan_2011.1to2_stories.slab_no_basement.coastal_a_or_v_zone' # the following rules are used for all flood-types as a default - elif BIM['OccupancyClass'] == 'RES4': + elif bim['OccupancyClass'] == 'RES4': fl_config = 'structural.209.RES4.USACE_Galveston.average_hotel_&_motel' - elif BIM['OccupancyClass'] == 'RES5': - fl_config = 'structural.214.RES5.USACE_Galveston.average_institutional_dormitory' + elif bim['OccupancyClass'] == 'RES5': + fl_config = ( + 'structural.214.RES5.USACE_Galveston.average_institutional_dormitory' + ) - elif BIM['OccupancyClass'] == 'RES6': + elif bim['OccupancyClass'] == 'RES6': fl_config = 'structural.215.RES6.USACE_Galveston.nursing_home' - elif BIM['OccupancyClass'] == 'COM1': + elif bim['OccupancyClass'] == 'COM1': fl_config = 'structural.217.COM1.USACE_Galveston.average_retail' - elif BIM['OccupancyClass'] == 'COM2': + elif bim['OccupancyClass'] == 'COM2': fl_config = 'structural.341.COM2.USACE_Galveston.average_wholesale' - elif BIM['OccupancyClass'] == 'COM3': - fl_config = 'structural.375.COM3.USACE_Galveston.average_personal_&_repair_services' + elif bim['OccupancyClass'] == 'COM3': + fl_config = ( + 'structural.375.COM3.USACE_Galveston.average_personal_&_repair_services' + ) - elif BIM['OccupancyClass'] == 'COM4': + elif bim['OccupancyClass'] == 'COM4': fl_config = 'structural.431.COM4.USACE_Galveston.average_prof/tech_services' - elif BIM['OccupancyClass'] == 'COM5': + elif bim['OccupancyClass'] == 'COM5': fl_config = 'structural.467.COM5.USACE_Galveston.bank' - elif BIM['OccupancyClass'] == 'COM6': + elif bim['OccupancyClass'] == 'COM6': fl_config = 'structural.474.COM6.USACE_Galveston.hospital' - elif BIM['OccupancyClass'] == 'COM7': + elif bim['OccupancyClass'] == 'COM7': fl_config = 'structural.475.COM7.USACE_Galveston.average_medical_office' - elif BIM['OccupancyClass'] == 'COM8': - fl_config = 'structural.493.COM8.USACE_Galveston.average_entertainment/recreation' + elif bim['OccupancyClass'] == 'COM8': + fl_config = ( + 'structural.493.COM8.USACE_Galveston.average_entertainment/recreation' + ) - elif BIM['OccupancyClass'] == 'COM9': + elif bim['OccupancyClass'] == 'COM9': fl_config = 'structural.532.COM9.USACE_Galveston.average_theatre' - elif BIM['OccupancyClass'] == 'COM10': + elif bim['OccupancyClass'] == 'COM10': fl_config = 'structural.543.COM10.USACE_Galveston.garage' - elif BIM['OccupancyClass'] == 'IND1': + elif bim['OccupancyClass'] == 'IND1': fl_config = 'structural.545.IND1.USACE_Galveston.average_heavy_industrial' - elif BIM['OccupancyClass'] == 'IND2': + elif bim['OccupancyClass'] == 'IND2': fl_config = 'structural.559.IND2.USACE_Galveston.average_light_industrial' - elif BIM['OccupancyClass'] == 'IND3': + elif bim['OccupancyClass'] == 'IND3': fl_config = 'structural.575.IND3.USACE_Galveston.average_food/drug/chem' - elif BIM['OccupancyClass'] == 'IND4': - fl_config = 'structural.586.IND4.USACE_Galveston.average_metals/minerals_processing' + elif bim['OccupancyClass'] == 'IND4': + fl_config = ( + 'structural.586.IND4.USACE_Galveston.average_metals/minerals_processing' + ) - elif BIM['OccupancyClass'] == 'IND5': + elif bim['OccupancyClass'] == 'IND5': fl_config = 'structural.591.IND5.USACE_Galveston.average_high_technology' - elif BIM['OccupancyClass'] == 'IND6': + elif bim['OccupancyClass'] == 'IND6': fl_config = 'structural.592.IND6.USACE_Galveston.average_construction' - elif BIM['OccupancyClass'] == 'AGR1': + elif bim['OccupancyClass'] == 'AGR1': fl_config = 'structural.616.AGR1.USACE_Galveston.average_agriculture' - elif BIM['OccupancyClass'] == 'REL1': + elif bim['OccupancyClass'] == 'REL1': fl_config = 'structural.624.REL1.USACE_Galveston.church' - elif BIM['OccupancyClass'] == 'GOV1': + elif bim['OccupancyClass'] == 'GOV1': fl_config = 'structural.631.GOV1.USACE_Galveston.average_government_services' - elif BIM['OccupancyClass'] == 'GOV2': + elif bim['OccupancyClass'] == 'GOV2': fl_config = 'structural.640.GOV2.USACE_Galveston.average_emergency_response' - elif BIM['OccupancyClass'] == 'EDU1': + elif bim['OccupancyClass'] == 'EDU1': fl_config = 'structural.643.EDU1.USACE_Galveston.average_school' - elif BIM['OccupancyClass'] == 'EDU2': + elif bim['OccupancyClass'] == 'EDU2': fl_config = 'structural.652.EDU2.USACE_Galveston.average_college/university' # extend the BIM dictionary - BIM.update(dict( - FloodType = flood_type, - BasementType=basement_type, - PostFIRM=PostFIRM, - )) + bim.update( + { + 'FloodType': flood_type, + 'BasementType': basement_type, + 'PostFIRM': is_post_firm_construction, + } + ) return fl_config - diff --git a/flood/building/portfolio/Hazus v6.1/pelicun_config.py b/flood/building/portfolio/Hazus v6.1/pelicun_config.py index 335cddc1..808ad4ea 100644 --- a/flood/building/portfolio/Hazus v6.1/pelicun_config.py +++ b/flood/building/portfolio/Hazus v6.1/pelicun_config.py @@ -40,14 +40,13 @@ from pathlib import Path import jsonschema -from jsonschema import validate import pandas as pd - +from FloodRulesets import configure_flood_vulnerability +from jsonschema import validate from pelicun import base -from FloodRulesets import FL_config -def auto_populate(aim): +def auto_populate(aim): # noqa: C901 """ Automatically creates a performance model for Hazus Hurricane analysis. @@ -71,9 +70,8 @@ def auto_populate(aim): Component assignment - Defines the components (in rows) and their location, direction, and quantity (in columns). """ - # extract the General Information - GI_in = aim.get('GeneralInformation', None) + general_information_input = aim.get('GeneralInformation', None) # parse the GI data @@ -81,11 +79,11 @@ def auto_populate(aim): alname_yearbuilt = ['YearBuiltNJDEP', 'yearBuilt', 'YearBuiltMODIV'] yearbuilt = None try: - yearbuilt = GI_in['YearBuilt'] - except: + yearbuilt = general_information_input['YearBuilt'] + except KeyError: for i in alname_yearbuilt: - if i in GI_in.keys(): - yearbuilt = GI_in[i] + if i in general_information_input: + yearbuilt = general_information_input[i] break # if none of the above works, set a default @@ -93,34 +91,24 @@ def auto_populate(aim): yearbuilt = 1985 # maps for split level - ap_SplitLevel = { - 'NO': 0, - 'YES': 1, - False: 0, - True: 1 - } + auto_populated_split_level = {'NO': 0, 'YES': 1, False: 0, True: 1} # maps for design level (Marginal Engineered is mapped to Engineered as default) - ap_DesignLevel = { - 'E': 'E', - 'NE': 'NE', - 'PE': 'PE', - 'ME': 'E' - } - design_level = GI_in.get('DesignLevel','E') + auto_populated_design_level = {'E': 'E', 'NE': 'NE', 'PE': 'PE', 'ME': 'E'} + design_level = general_information_input.get('DesignLevel', 'E') if pd.isna(design_level): design_level = 'E' - foundation = GI_in.get('FoundationType',3501) + foundation = general_information_input.get('FoundationType', 3501) if pd.isna(foundation): foundation = 3501 - nunits = GI_in.get('NoUnits',1) + nunits = general_information_input.get('NoUnits', 1) if pd.isna(nunits): nunits = 1 # maps for flood zone - ap_FloodZone = { + auto_populated_flood_zone_mapping = { # Coastal areas with a 1% or greater chance of flooding and an # additional hazard associated with storm waves. 6101: 'VE', @@ -138,78 +126,92 @@ def auto_populate(aim): 6113: 'OW', 6114: 'D', 6115: 'NA', - 6119: 'NA' + 6119: 'NA', } - if type(GI_in['FloodZone']) == int: + if isinstance(general_information_input['FloodZone'], int): # NJDEP code for flood zone (conversion to the FEMA designations) - floodzone_fema = ap_FloodZone[GI_in['FloodZone']] + floodzone_fema = auto_populated_flood_zone_mapping[ + general_information_input['FloodZone'] + ] else: # standard input should follow the FEMA flood zone designations - floodzone_fema = GI_in['FloodZone'] + floodzone_fema = general_information_input['FloodZone'] # add the parsed data to the BIM dict - GI_ap = GI_in.copy() - GI_ap.update(dict( - YearBuilt=int(yearbuilt), - DesignLevel=str(ap_DesignLevel[design_level]), # default engineered - NumberOfUnits=int(nunits), - FirstFloorElevation=float(GI_in.get('FirstFloorHt1',10.0)), - SplitLevel=bool(ap_SplitLevel[GI_in.get('SplitLevel','NO')]), # default: no - FoundationType=int(foundation), # default: pile - City=GI_in.get('City','NA'), - FloodZone =str(floodzone_fema) - )) + general_information_auto_populated = general_information_input.copy() + general_information_auto_populated.update( + { + 'YearBuilt': int(yearbuilt), + 'DesignLevel': str( + auto_populated_design_level[design_level] + ), # default engineered + 'NumberOfUnits': int(nunits), + 'FirstFloorElevation': float( + general_information_input.get('FirstFloorHt1', 10.0) + ), + 'SplitLevel': bool( + auto_populated_split_level[ + general_information_input.get('SplitLevel', 'NO') + ] + ), # default: no + 'FoundationType': int(foundation), # default: pile + 'City': general_information_input.get('City', 'NA'), + 'FloodZone': str(floodzone_fema), + } + ) # prepare the flood rulesets - fld_config = FL_config(GI_ap) + fld_config = configure_flood_vulnerability(general_information_auto_populated) if fld_config is None: - info_dict = {key: GI_ap.get(key, "") - for key in [ - "OccupancyClass", - "NumberOfStories", - "FloodType", - "BasementType", - "PostFIRM" - ]} - - #TODO(AZS): Once we have a proper inference engine in place, replace this - # print statement and raise an error instead - msg = (f'No matching flood archetype configuration available for the ' - f'following attributes:\n' - f'{info_dict}') - print(msg) - #raise ValueError(msg) + info_dict = { + key: general_information_auto_populated.get(key, '') + for key in [ + 'OccupancyClass', + 'NumberOfStories', + 'FloodType', + 'BasementType', + 'PostFIRM', + ] + } + + # TODO (azs): implement a logging system instead of printing these messages + msg = ( + f'No matching flood archetype configuration available for the ' + f'following attributes:\n' + f'{info_dict}' + ) + print(msg) # noqa: T201 + # raise ValueError(msg) # prepare the component assignment comp = pd.DataFrame( - {f'{fld_config}': [ 'ea', 1, 1, 1, 'N/A']}, - index = [ 'Units','Location','Direction','Theta_0','Family'] - ).T - - DL_ap = { - "Asset": { - "ComponentAssignmentFile": "CMP_QNT.csv", - "ComponentDatabase": "None", - "NumberOfStories": 1 - }, - "Demands": { + {f'{fld_config}': ['ea', 1, 1, 1, 'N/A']}, + index=['Units', 'Location', 'Direction', 'Theta_0', 'Family'], + ).T + + damage_loss_config_auto_populated = { + 'Asset': { + 'ComponentAssignmentFile': 'CMP_QNT.csv', + 'ComponentDatabase': 'None', + 'NumberOfStories': 1, }, - "Losses": { - "Repair": { - "ConsequenceDatabase": "Hazus Hurricane Storm Surge - Buildings", - "MapApproach": "Automatic", - #"MapFilePath": "loss_map.csv", - "DecisionVariables": { - "Cost": True, - "Carbon": False, - "Energy": False, - "Time": False - } + 'Demands': {}, + 'Losses': { + 'Repair': { + 'ConsequenceDatabase': 'Hazus Hurricane Storm Surge - Buildings', + 'MapApproach': 'Automatic', + # "MapFilePath": "loss_map.csv", + 'DecisionVariables': { + 'Cost': True, + 'Carbon': False, + 'Energy': False, + 'Time': False, + }, } }, - "Options": { - "NonDirectionalMultipliers": {"ALL": 1.0}, + 'Options': { + 'NonDirectionalMultipliers': {'ALL': 1.0}, }, } @@ -218,33 +220,38 @@ def auto_populate(aim): # get the length unit ft_to_demand_unit = base.convert_units( 1.0, - unit = 'ft', - to_unit = GI_ap['units']['length'], - category = 'length' + unit='ft', + to_unit=general_information_auto_populated['units']['length'], + category='length', ) demand_file = Path(aim['DL']['Demands']['DemandFilePath']).resolve() original_demands = pd.read_csv(demand_file, index_col=0) for col in original_demands.columns: - if "PIH" in col: + if 'PIH' in col: extension = original_demands[col] - break - col_original = col - + break + col_original = col + col_parts = col_original.split('-') - if "PIH"==col_parts[1]: + if col_parts[1] == 'PIH': col_parts[2] = '0' else: col_parts[1] = '0' col_mod = '-'.join(col_parts) # move the original demands to location 0 - original_demands[col_mod] = extension.values.copy() + original_demands[col_mod] = extension.to_numpy().copy() original_demands[col_original] = ( - extension.values - - GI_ap['FirstFloorElevation']*ft_to_demand_unit + extension.to_numpy() + - general_information_auto_populated['FirstFloorElevation'] + * ft_to_demand_unit ) original_demands.to_csv(demand_file) - return GI_ap, DL_ap, comp + return ( + general_information_auto_populated, + damage_loss_config_auto_populated, + comp, + ) diff --git a/hurricane/building/portfolio/Hazus v5.1 coupled/data_sources/generate_library_files.py b/hurricane/building/portfolio/Hazus v5.1 coupled/data_sources/generate_library_files.py index 989f8fe6..6412c941 100644 --- a/hurricane/building/portfolio/Hazus v5.1 coupled/data_sources/generate_library_files.py +++ b/hurricane/building/portfolio/Hazus v5.1 coupled/data_sources/generate_library_files.py @@ -239,20 +239,42 @@ def parse_description(descr, parsed_data): # noqa: C901, PLR0912, PLR0915 return descr -def create_Hazus_HU_damage_and_loss_files(fit_parameters=True, root_path='hurricane/building/portfolio/Hazus v5.1 coupled/'): # noqa: C901, D103, N802, PLR0912, PLR0915, FBT002 - +def create_hazus_hurricane_damage_loss_files( # noqa: C901, PLR0912, PLR0915 + fit_parameters=True, # noqa: FBT002 + root_path='hurricane/building/portfolio/Hazus v5.1 coupled/', +): + """ + Create Hazus hurricane damage and loss library files. + + This function processes raw Hazus hurricane data to generate damage and loss + library files. It can either fit new normal or lognormal functions to the + raw data or load existing fitted parameters from a fitted_parameters.csv + file. + + Parameters + ---------- + fit_parameters : bool, optional + If True, fits new parameters to raw data. If False, loads existing + fitted parameters from fitted_parameters.csv. Default is True. + root_path : str, optional + Path to the directory containing the Hazus data files. + Default is 'hurricane/building/portfolio/Hazus v5.1 coupled/'. + + Returns + ------- + None + The function generates library files in the specified directory structure. + """ root_path = Path(root_path) # The original path points to the folder where the original parameters are # stored. - original_path = root_path.parent / "Hazus v5.1 original/" + original_path = root_path.parent / 'Hazus v5.1 original/' if fit_parameters: # Load RAW Hazus data - raw_data_path = ( - root_path / 'data_sources/input_files/' - ) + raw_data_path = root_path / 'data_sources/input_files/' # read bldg data @@ -329,7 +351,7 @@ def create_Hazus_HU_damage_and_loss_files(fit_parameters=True, root_path='hurric ] # the problem affects DS4 probabilities - archetypes = (DS_data[2] - DS_data[3].to_numpy() < -0.02).max(axis=1) # noqa: PLR2004 + archetypes = (DS_data[2] - DS_data[3].to_numpy() < -0.02).max(axis=1) # go through each affected archetype and fix the problem for frag_id in archetypes[archetypes == True].index: # noqa: E712 # get the wbID and terrain_id @@ -354,7 +376,7 @@ def create_Hazus_HU_damage_and_loss_files(fit_parameters=True, root_path='hurric # then check where to store the values at DS4 to maintain # ascending exceedance probabilities - target_DS = np.where(np.argsort(median_capacities) == 3)[0][0] # noqa: N806, PLR2004 + target_DS = np.where(np.argsort(median_capacities) == 3)[0][0] # noqa: N806 # since this is always DS1 in the current database, # the script below works with that assumption and checks for exceptions @@ -429,11 +451,11 @@ def overwrite_ds4_data(): # extract the DS3 information DS3_data = frag_df_arch.loc[ # noqa: N806 - frag_df['DamLossDescID'] == 3, wind_speeds_str # noqa: PLR2004 + frag_df['DamLossDescID'] == 3, wind_speeds_str ].to_numpy() # and overwrite the DS4 values in the original dataset - DS4_index = frag_df_arch.loc[frag_df['DamLossDescID'] == 4].index # noqa: N806, PLR2004 + DS4_index = frag_df_arch.loc[frag_df['DamLossDescID'] == 4].index # noqa: N806 frag_df.loc[DS4_index, wind_speeds_str] = DS3_data overwrite_ds4_data() @@ -572,7 +594,7 @@ def overwrite_ds4_data(): beta_0 = 0.2 median_id = max(np.where(wind_speeds <= mu_0)[0]) + 1 - min_speed_id = max(np.where(wind_speeds <= 100)[0]) + 1 # noqa: PLR2004 + min_speed_id = max(np.where(wind_speeds <= 100)[0]) + 1 max_speed_id_mod = max( [min([median_id, max_speed_id]), min_speed_id] ) @@ -696,8 +718,8 @@ def MSE_lognormal(params, mu_min, res_type='MSE'): # noqa: N802 # are very close AND one model has substantially # smaller maximum error than the other, then # choose the model with the smaller maximum error - if (np.log(res_lognormal.fun / res_normal.fun) < 0.1) and ( # noqa: PLR2004 - np.log(res_normal.maxcv / res_lognormal.maxcv) > 0.1 # noqa: PLR2004 + if (np.log(res_lognormal.fun / res_normal.fun) < 0.1) and ( + np.log(res_normal.maxcv / res_lognormal.maxcv) > 0.1 ): dist_type = 'lognormal' res = res_lognormal @@ -706,8 +728,8 @@ def MSE_lognormal(params, mu_min, res_type='MSE'): # noqa: N802 dist_type = 'normal' res = res_normal - elif (np.log(res_normal.fun / res_lognormal.fun) < 0.1) and ( # noqa: PLR2004 - np.log(res_lognormal.maxcv / res_normal.maxcv) > 0.1 # noqa: PLR2004 + elif (np.log(res_normal.fun / res_lognormal.fun) < 0.1) and ( + np.log(res_lognormal.maxcv / res_normal.maxcv) > 0.1 ): dist_type = 'normal' res = res_normal @@ -732,7 +754,7 @@ def MSE_lognormal(params, mu_min, res_type='MSE'): # noqa: N802 # Focus on "Building losses" first L_ref = np.asarray( # noqa: N806 frag_df_arch_terrain.loc[ - frag_df_arch_terrain['DamLossDescID'] == 5, wind_speeds_str # noqa: PLR2004 + frag_df_arch_terrain['DamLossDescID'] == 5, wind_speeds_str ].to_numpy()[0] ) @@ -763,7 +785,7 @@ def MSE_lognormal(params, mu_min, res_type='MSE'): # noqa: N802 # The losses for DS4 are calculated based on outcomes at the # highest wind speeds L_max = frag_df_arch_terrain.loc[ # noqa: N806 - frag_df_arch_terrain['DamLossDescID'] == 5, 'WS250' # noqa: PLR2004 + frag_df_arch_terrain['DamLossDescID'] == 5, 'WS250' ].to_numpy()[0] DS4_max = DS_probs[3][-1] # noqa: N806 @@ -836,17 +858,13 @@ def SSE_loss(params, res_type='SSE'): # noqa: N802 main_df = pd.concat(rows, axis=0, ignore_index=True) - main_df.to_csv( - root_path/'data_sources/fitted_parameters.csv' - ) + main_df.to_csv(root_path / 'data_sources/fitted_parameters.csv') main_df = pd.read_csv( root_path / 'data_sources/fitted_parameters.csv', index_col=0, low_memory=False, - dtype = { - "joist_spacing": str - } + dtype={'joist_spacing': str}, ) # Prepare the Damage and Loss Model Data Files @@ -1286,9 +1304,7 @@ def SSE_loss(params, res_type='SSE'): # noqa: N802 ] df_db_fit.to_csv(root_path / 'fragility.csv') - df_db_original.to_csv( - original_path / 'fragility.csv' - ) + df_db_original.to_csv(original_path / 'fragility.csv') # initialize the output loss table # define the columns @@ -1331,22 +1347,18 @@ def SSE_loss(params, res_type='SSE'): # noqa: N802 df_db_fit = df_db_fit.loc[df_db_fit['ID'] != '-Cost'] df_db_fit = df_db_fit.set_index('ID').sort_index().convert_dtypes() - df_db_fit.to_csv( - root_path / 'consequence_repair.csv' - ) - df_db_original.to_csv( - original_path / 'loss_repair.csv' - ) + df_db_fit.to_csv(root_path / 'consequence_repair.csv') + df_db_original.to_csv(original_path / 'loss_repair.csv') -def create_Hazus_HU_metadata_files( # noqa: C901, N802 +def create_hazus_hurricane_metadata_files( # noqa: C901 source_file: str = 'fragility.csv', meta_file: str = 'data_sources/input_files/metadata.json', target_meta_file_damage: str = 'fragility.json', target_meta_file_loss: str = 'consequence_repair.json', target_meta_file_damage_original: str = 'fragility.json', target_meta_file_loss_original: str = 'loss_repair.json', - root_path = 'hurricane/building/portfolio/Hazus v5.1 coupled/' + root_path='hurricane/building/portfolio/Hazus v5.1 coupled/', ) -> None: """ Create a database metadata file for the HAZUS Hurricane fragilities. @@ -1398,14 +1410,16 @@ def create_Hazus_HU_metadata_files( # noqa: C901, N802 # The original path points to the folder where the original parameters are # stored. - original_path = Path(root_path).parent / "Hazus v5.1 original/" + original_path = Path(root_path).parent / 'Hazus v5.1 original/' # Combine paths: source_file = root_path / source_file meta_file = root_path / meta_file target_meta_file_damage = root_path / target_meta_file_damage target_meta_file_loss = root_path / target_meta_file_loss - target_meta_file_damage_original = original_path / target_meta_file_damage_original + target_meta_file_damage_original = ( + original_path / target_meta_file_damage_original + ) target_meta_file_loss_original = original_path / target_meta_file_loss_original # @@ -2029,8 +2043,8 @@ def find_class_type(entry: str) -> str | None: def main(): """Generate Hazus Hurricane damage and loss database files.""" - create_Hazus_HU_damage_and_loss_files() - create_Hazus_HU_metadata_files() + create_hazus_hurricane_damage_loss_files() + create_hazus_hurricane_metadata_files() if __name__ == '__main__': diff --git a/hurricane/building/portfolio/Hazus v5.1 coupled/pelicun_config.py b/hurricane/building/portfolio/Hazus v5.1 coupled/pelicun_config.py index 7ec5e93c..91d28234 100644 --- a/hurricane/building/portfolio/Hazus v5.1 coupled/pelicun_config.py +++ b/hurricane/building/portfolio/Hazus v5.1 coupled/pelicun_config.py @@ -40,11 +40,11 @@ from pathlib import Path import jsonschema -from jsonschema import validate import pandas as pd +from jsonschema import validate -def get_feature(feature, building_info): +def get_feature(feature, building_info): # noqa: C901, PLR0911 """ Parse and map building characteristics into features used in Hazus. @@ -54,135 +54,119 @@ def get_feature(feature, building_info): Name of feature to be mapped. """ - if feature == 'building_type': building_type_map = { - "Wood": "W", - "Masonry": "M", - "Concrete": "C", - "Steel": "S", - "Manufactured Housing": "MH", - "Essential Facility": "HUEF" + 'Wood': 'W', + 'Masonry': 'M', + 'Concrete': 'C', + 'Steel': 'S', + 'Manufactured Housing': 'MH', + 'Essential Facility': 'HUEF', } - return building_type_map[building_info["BuildingType"]] + return building_type_map[building_info['BuildingType']] - elif feature == 'structure_type': + if feature == 'structure_type': structure_type_map = { - "Single Family Housing": "SF", - "Multi-Unit Housing": "MUH", - "Low-Rise Strip Mall": "LRM", - "Low-Rise Industrial Building": "LRI", - "Engineered Residential Building": "ERB", - "Engineered Commercial Building": "ECB", - "Pre-Engineered Metal Building": "PMB", - "Pre-HUD": "PHUD", - "1976 HUD": "76HUD", - "1994 HUD Zone 1": "94HUDI", - "1994 HUD Zone 2": "94HUDII", - "1994 HUD Zone 3": "94HUDIII", - "Fire Station": "FS", - "Police Station": "PS", - "Emergency Operation Center": "EO", - "Hospital": "H", - "School": "S" + 'Single Family Housing': 'SF', + 'Multi-Unit Housing': 'MUH', + 'Low-Rise Strip Mall': 'LRM', + 'Low-Rise Industrial Building': 'LRI', + 'Engineered Residential Building': 'ERB', + 'Engineered Commercial Building': 'ECB', + 'Pre-Engineered Metal Building': 'PMB', + 'Pre-HUD': 'PHUD', + '1976 HUD': '76HUD', + '1994 HUD Zone 1': '94HUDI', + '1994 HUD Zone 2': '94HUDII', + '1994 HUD Zone 3': '94HUDIII', + 'Fire Station': 'FS', + 'Police Station': 'PS', + 'Emergency Operation Center': 'EO', + 'Hospital': 'H', + 'School': 'S', } - return structure_type_map[building_info["StructureType"]] - - elif feature == 'height_class': + return structure_type_map[building_info['StructureType']] + if feature == 'height_class': structure_type = get_feature('structure_type', building_info) if structure_type in ['SF', 'MUH', 'ERB', 'ECB', 'S', 'H']: - number_of_stories = int(building_info["NumberOfStories"]) + number_of_stories = int(building_info['NumberOfStories']) - if structure_type in ["LRM"]: - height = float(building_info["Height"]) + if structure_type in ['LRM']: + height = float(building_info['Height']) if structure_type in ['PMB']: - plan_area = float(building_info["PlanArea"]) + plan_area = float(building_info['PlanArea']) if structure_type == 'SF': if number_of_stories == 1: - return "1" # 1 Story - else: - return "2" # 2 or more Stories + return '1' # 1 Story + return '2' # 2 or more Stories - elif structure_type == 'MUH': + if structure_type == 'MUH': if number_of_stories == 1: - return "1" # 1 Story - elif number_of_stories == 2: - return "2" # 2 Stories - else: - return "3" # 3 or more Stories + return '1' # 1 Story + if number_of_stories == 2: + return '2' # 2 Stories + return '3' # 3 or more Stories - elif structure_type == 'LRM': + if structure_type == 'LRM': if height <= 15.0: - return "1" # Up to 15 ft high - else: - return "2" # More than 15 ft high + return '1' # Up to 15 ft high + return '2' # More than 15 ft high - elif structure_type in ['ERB', 'ECB']: + if structure_type in ['ERB', 'ECB']: if number_of_stories in [1, 2]: - return "L" # 1-2 Stories - elif number_of_stories in [3, 4, 5]: - return "M" # 3-5 Stories - else: - return "H" # 6 or more Stories + return 'L' # 1-2 Stories + if number_of_stories in [3, 4, 5]: + return 'M' # 3-5 Stories + return 'H' # 6 or more Stories - elif structure_type == 'S': + if structure_type == 'S': if number_of_stories == 1: - return "S" # Small - elif number_of_stories == 2: - return "M" # Medium - else: - return "L" # Large + return 'S' # Small + if number_of_stories == 2: + return 'M' # Medium + return 'L' # Large - elif structure_type == 'H': + if structure_type == 'H': if number_of_stories <= 2: - return "S" # Small - elif number_of_stories <= 6: - return "M" # Medium - else: - return "L" # Large + return 'S' # Small + if number_of_stories <= 6: + return 'M' # Medium + return 'L' # Large - elif structure_type == 'PMB': + if structure_type == 'PMB': if plan_area < 10000: - return "S" # Small, less than 10,000 ft2 - elif plan_area < 100000: - return "M" # Medium, less than 100,000 ft2 - else: - return "L" # Large + return 'S' # Small, less than 10,000 ft2 + if plan_area < 100000: + return 'M' # Medium, less than 100,000 ft2 + return 'L' # Large elif feature == 'roof_shape': - roof_shape_map = { - "Hip": "hip", - "Gable": "gab", - "Flat": "flt" - } - return roof_shape_map[building_info.get("RoofShape")] + roof_shape_map = {'Hip': 'hip', 'Gable': 'gab', 'Flat': 'flt'} + return roof_shape_map[building_info.get('RoofShape')] elif feature == 'roof_cover': roof_cover_map = { - "Single-Ply Membrane": "spm", - "Built-Up Roof": "bur", - "Sheet Metal": "smtl", - "Composite Shingle": "cshl" + 'Single-Ply Membrane': 'spm', + 'Built-Up Roof': 'bur', + 'Sheet Metal': 'smtl', + 'Composite Shingle': 'cshl', } - return roof_cover_map[building_info.get("RoofCover")] + return roof_cover_map[building_info.get('RoofCover')] elif feature == 'roof_quality': roof_quality_map = { - "Good": "god", - "Poor": "por", + 'Good': 'god', + 'Poor': 'por', } - return roof_quality_map[building_info.get("RoofQuality")] + return roof_quality_map[building_info.get('RoofQuality')] elif feature == 'roof_system': - roof_system_map = { - "Truss": 'trs', - "Open-Web Steel Joists": 'ows' - } - return roof_system_map[building_info.get("RoofSystem")] + roof_system_map = {'Truss': 'trs', 'Open-Web Steel Joists': 'ows'} + return roof_system_map[building_info.get('RoofSystem')] elif feature == 'roof_deck_attachment': roof_deck_attachment_map = { @@ -191,74 +175,70 @@ def get_feature(feature, building_info): '8d': '8d', '8s': '8s', 'Standard': 'std', - 'Superior': 'sup' + 'Superior': 'sup', } - return roof_deck_attachment_map[building_info.get("RoofDeckAttachment")] + return roof_deck_attachment_map[building_info.get('RoofDeckAttachment')] elif feature == 'roof_wall_connection': - roof_wall_connection_map = { - "Strap": "strap", - "Toe-nail": "tnail" - } - return roof_wall_connection_map[building_info.get("RoofToWallConnection")] + roof_wall_connection_map = {'Strap': 'strap', 'Toe-nail': 'tnail'} + return roof_wall_connection_map[building_info.get('RoofToWallConnection')] elif feature == 'secondary_water_resistance': - return int(building_info.get("SecondaryWaterResistance")) + return int(building_info.get('SecondaryWaterResistance')) elif feature == 'shutters': - return int(building_info.get("Shutters")) + return int(building_info.get('Shutters')) elif feature == 'garage': garage_map = { - "No": "no", - "Standard": "std", - "Weak": "wkd", - "Superior": "sup" + 'No': 'no', + 'Standard': 'std', + 'Weak': 'wkd', + 'Superior': 'sup', } - return garage_map[building_info.get("Garage")] + return garage_map[building_info.get('Garage')] elif feature == 'wind_debris_class': - return building_info.get("WindDebrisClass") + return building_info.get('WindDebrisClass') elif feature == 'unit_class': - number_of_units = building_info.get("NumberOfUnits") + number_of_units = building_info.get('NumberOfUnits') if number_of_units == 1: - return "sgl" - else: - return "mlt" + return 'sgl' + return 'mlt' elif feature == 'joist_spacing': - return building_info.get("JoistSpacing") + return building_info.get('JoistSpacing') elif feature == 'masonry_reinforcing': - return int(building_info.get("MasonryReinforcing")) + return int(building_info.get('MasonryReinforcing')) elif feature == 'tie_downs': - return int(building_info.get("TieDowns")) + return int(building_info.get('TieDowns')) elif feature == 'window_area': - window_area = building_info.get("WindowArea") + window_area = building_info.get('WindowArea') if window_area < 0.33: - return 'low' # Low - elif window_area < 0.5: - return 'med' # Medium - else: - return 'hig' # High + return 'low' # Low + if window_area < 0.5: + return 'med' # Medium + return 'hig' # High elif feature == 'terrain_roughness': terrain_roughness_map = { - "Open": 3, - "Light Suburban": 15, - "Suburban": 35, - "Light Trees": 70, - "Trees": 100 + 'Open': 3, + 'Light Suburban': 15, + 'Suburban': 35, + 'Light Trees': 70, + 'Trees': 100, } - return terrain_roughness_map[building_info.get("LandCover")] + return terrain_roughness_map[building_info.get('LandCover')] + + return 'null' - return "null" -def auto_populate(aim): +def auto_populate(aim): # noqa: C901 """ Automatically creates a performance model for Hazus Hurricane analysis. @@ -282,13 +262,12 @@ def auto_populate(aim): Component assignment - Defines the components (in rows) and their location, direction, and quantity (in columns). """ - # extract the General Information - gi = aim.get("GeneralInformation") + gi = aim.get('GeneralInformation') # make sure missing data is properly represented as null in the JSON for key, item in gi.items(): - if pd.isna(item) or item=="": + if pd.isna(item) or item == '': gi[key] = None # load the schema assuming it is called "input_schema.json" and it is @@ -296,7 +275,7 @@ def auto_populate(aim): current_file_path = Path(__file__) current_directory = current_file_path.parent - with Path(current_directory / "input_schema.json").open(encoding="utf-8") as f: + with Path(current_directory / 'input_schema.json').open(encoding='utf-8') as f: input_schema = json.load(f) # validate the provided features against the required inputs @@ -304,375 +283,373 @@ def auto_populate(aim): validate(instance=gi, schema=input_schema) except jsonschema.exceptions.ValidationError as exc: # type: ignore msg = ( - "The provided building information does not conform to the input" - " requirements for the chosen damage and loss model." + 'The provided building information does not conform to the input' + ' requirements for the chosen damage and loss model.' ) raise ValueError(msg) from exc - model_id = ".".join([get_feature(feature, gi) for feature in ['building_type','structure_type']]) + model_id = '.'.join( + [get_feature(feature, gi) for feature in ['building_type', 'structure_type']] + ) if model_id == 'W.SF': model_features = [ - "height_class", - "roof_shape", - "secondary_water_resistance", - "roof_deck_attachment", - "roof_wall_connection", - "garage", - "shutters", - "terrain_roughness" + 'height_class', + 'roof_shape', + 'secondary_water_resistance', + 'roof_deck_attachment', + 'roof_wall_connection', + 'garage', + 'shutters', + 'terrain_roughness', ] elif model_id == 'W.MUH': - roof_shape = get_feature("roof_shape", gi) + roof_shape = get_feature('roof_shape', gi) if roof_shape == 'flt': model_features = [ - "height_class", - "roof_shape", - "roof_cover", - "roof_quality", - "null", - "roof_deck_attachment", - "roof_wall_connection", - "shutters", - "terrain_roughness" + 'height_class', + 'roof_shape', + 'roof_cover', + 'roof_quality', + 'null', + 'roof_deck_attachment', + 'roof_wall_connection', + 'shutters', + 'terrain_roughness', ] else: model_features = [ - "height_class", - "roof_shape", - "null", - "null", - "secondary_water_resistance", - "roof_deck_attachment", - "roof_wall_connection", - "shutters", - "terrain_roughness" + 'height_class', + 'roof_shape', + 'null', + 'null', + 'secondary_water_resistance', + 'roof_deck_attachment', + 'roof_wall_connection', + 'shutters', + 'terrain_roughness', ] elif model_id == 'M.SF': - roof_system = get_feature("roof_system", gi) + roof_system = get_feature('roof_system', gi) if roof_system == 'trs': model_features = [ - "height_class", - "roof_shape", - "roof_wall_connection", - "roof_system", - "roof_deck_attachment", - "shutters", - "secondary_water_resistance", - "garage", - "masonry_reinforcing", - "null", - "terrain_roughness" + 'height_class', + 'roof_shape', + 'roof_wall_connection', + 'roof_system', + 'roof_deck_attachment', + 'shutters', + 'secondary_water_resistance', + 'garage', + 'masonry_reinforcing', + 'null', + 'terrain_roughness', ] else: - roof_cover = get_feature("roof_cover", gi) - roof_deck_attachment = get_feature("roof_deck_attachment", gi) + roof_cover = get_feature('roof_cover', gi) + roof_deck_attachment = get_feature('roof_deck_attachment', gi) if roof_cover == 'cshl' and roof_deck_attachment == 'su': model_features = [ - "height_class", - "roof_shape", - "roof_wall_connection", - "roof_system", - "roof_deck_attachment", - "shutters", - "secondary_water_resistance", - "null", - "null", - "roof_cover", - "terrain_roughness" + 'height_class', + 'roof_shape', + 'roof_wall_connection', + 'roof_system', + 'roof_deck_attachment', + 'shutters', + 'secondary_water_resistance', + 'null', + 'null', + 'roof_cover', + 'terrain_roughness', ] else: model_features = [ - "height_class", - "roof_shape", - "roof_wall_connection", - "roof_system", - "roof_deck_attachment", - "shutters", - "null", - "null", - "null", - "roof_cover", - "terrain_roughness" + 'height_class', + 'roof_shape', + 'roof_wall_connection', + 'roof_system', + 'roof_deck_attachment', + 'shutters', + 'null', + 'null', + 'null', + 'roof_cover', + 'terrain_roughness', ] - + elif model_id == 'M.MUH': - roof_shape = get_feature("roof_shape", gi) + roof_shape = get_feature('roof_shape', gi) if roof_shape == 'flt': model_features = [ - "height_class", - "roof_shape", - "null", - "roof_cover", - "roof_quality", - "roof_deck_attachment", - "roof_wall_connection", - "shutters", - "masonry_reinforcing", - "terrain_roughness" + 'height_class', + 'roof_shape', + 'null', + 'roof_cover', + 'roof_quality', + 'roof_deck_attachment', + 'roof_wall_connection', + 'shutters', + 'masonry_reinforcing', + 'terrain_roughness', ] else: model_features = [ - "height_class", - "roof_shape", - "secondary_water_resistance", - "null", - "null", - "roof_deck_attachment", - "roof_wall_connection", - "shutters", - "masonry_reinforcing", - "terrain_roughness" + 'height_class', + 'roof_shape', + 'secondary_water_resistance', + 'null', + 'null', + 'roof_deck_attachment', + 'roof_wall_connection', + 'shutters', + 'masonry_reinforcing', + 'terrain_roughness', ] - + elif model_id.startswith('M.LRM'): - height_class = get_feature("height_class", gi) - roof_system = get_feature("roof_system", gi) + height_class = get_feature('height_class', gi) + roof_system = get_feature('roof_system', gi) if height_class == '1': if roof_system == 'trs': model_features = [ - "height_class", - "roof_cover", - "shutters", - "masonry_reinforcing", - "wind_debris_class", - "roof_system", - "roof_deck_attachment", - "roof_wall_connection", - "null", - "null", - "terrain_roughness" + 'height_class', + 'roof_cover', + 'shutters', + 'masonry_reinforcing', + 'wind_debris_class', + 'roof_system', + 'roof_deck_attachment', + 'roof_wall_connection', + 'null', + 'null', + 'terrain_roughness', ] else: model_features = [ - "height_class", - "roof_cover", - "shutters", - "masonry_reinforcing", - "wind_debris_class", - "roof_system", - "null", - "null", - "roof_quality", - "roof_deck_attachment", - "terrain_roughness" + 'height_class', + 'roof_cover', + 'shutters', + 'masonry_reinforcing', + 'wind_debris_class', + 'roof_system', + 'null', + 'null', + 'roof_quality', + 'roof_deck_attachment', + 'terrain_roughness', ] + elif roof_system == 'trs': + model_features = [ + 'height_class', + 'roof_cover', + 'shutters', + 'masonry_reinforcing', + 'wind_debris_class', + 'roof_system', + 'roof_deck_attachment', + 'roof_wall_connection', + 'null', + 'null', + 'null', + 'null', + 'terrain_roughness', + ] + else: - if roof_system == 'trs': + unit_class = get_feature('unit_class', gi) + + if unit_class == 'sgl': model_features = [ - "height_class", - "roof_cover", - "shutters", - "masonry_reinforcing", - "wind_debris_class", - "roof_system", - "roof_deck_attachment", - "roof_wall_connection", - "null", - "null", - "null", - "null", - "terrain_roughness" - ] + 'height_class', + 'roof_cover', + 'shutters', + 'masonry_reinforcing', + 'wind_debris_class', + 'roof_system', + 'null', + 'null', + 'roof_quality', + 'roof_deck_attachment', + 'unit_class', + 'null', + 'terrain_roughness', + ] else: - unit_class = get_feature("unit_class", gi) - - if unit_class == 'sgl': - model_features = [ - "height_class", - "roof_cover", - "shutters", - "masonry_reinforcing", - "wind_debris_class", - "roof_system", - "null", - "null", - "roof_quality", - "roof_deck_attachment", - "unit_class", - "null", - "terrain_roughness" - ] - - else: - model_features = [ - "height_class", - "roof_cover", - "shutters", - "masonry_reinforcing", - "wind_debris_class", - "roof_system", - "null", - "null", - "roof_quality", - "roof_deck_attachment", - "unit_class", - "joist_spacing", - "terrain_roughness" - ] - + model_features = [ + 'height_class', + 'roof_cover', + 'shutters', + 'masonry_reinforcing', + 'wind_debris_class', + 'roof_system', + 'null', + 'null', + 'roof_quality', + 'roof_deck_attachment', + 'unit_class', + 'joist_spacing', + 'terrain_roughness', + ] + elif model_id == 'M.LRI': model_features = [ - "shutters", - "masonry_reinforcing", - "roof_quality", - "roof_deck_attachment", - "terrain_roughness" + 'shutters', + 'masonry_reinforcing', + 'roof_quality', + 'roof_deck_attachment', + 'terrain_roughness', ] - + elif model_id.startswith('M.E'): model_features = [ - "height_class", - "roof_cover", - "shutters", - "wind_debris_class", - "roof_deck_attachment", - "window_area", - "terrain_roughness" - ] - + 'height_class', + 'roof_cover', + 'shutters', + 'wind_debris_class', + 'roof_deck_attachment', + 'window_area', + 'terrain_roughness', + ] + elif model_id.startswith('C.E'): model_features = [ - "height_class", - "roof_cover", - "shutters", - "wind_debris_class", - "window_area", - "terrain_roughness" + 'height_class', + 'roof_cover', + 'shutters', + 'wind_debris_class', + 'window_area', + 'terrain_roughness', ] - + elif model_id.startswith('S.E'): model_features = [ - "height_class", - "roof_cover", - "shutters", - "wind_debris_class", - "roof_deck_attachment", - "window_area", - "terrain_roughness" + 'height_class', + 'roof_cover', + 'shutters', + 'wind_debris_class', + 'roof_deck_attachment', + 'window_area', + 'terrain_roughness', ] - + elif model_id == 'S.PMB': model_features = [ - "height_class", - "shutters", - "roof_quality", - "roof_deck_attachment", - "terrain_roughness" + 'height_class', + 'shutters', + 'roof_quality', + 'roof_deck_attachment', + 'terrain_roughness', ] - + elif model_id.startswith('MH.'): - model_features = [ - "shutters", - "tie_downs", - "terrain_roughness" - ] - + model_features = ['shutters', 'tie_downs', 'terrain_roughness'] + elif model_id.startswith('HUEF.FS'): model_features = [ - "roof_cover", - "shutters", - "wind_debris_class", - "roof_quality", - "roof_deck_attachment", - "terrain_roughness" + 'roof_cover', + 'shutters', + 'wind_debris_class', + 'roof_quality', + 'roof_deck_attachment', + 'terrain_roughness', ] elif model_id.startswith('HUEF.S'): - height_class = get_feature("height_class", gi) + height_class = get_feature('height_class', gi) if height_class == 'S': model_features = [ - "height_class", - "roof_cover", - "shutters", - "wind_debris_class", - "roof_quality", - "roof_deck_attachment", - "terrain_roughness" + 'height_class', + 'roof_cover', + 'shutters', + 'wind_debris_class', + 'roof_quality', + 'roof_deck_attachment', + 'terrain_roughness', ] else: model_features = [ - "height_class", - "roof_cover", - "shutters", - "wind_debris_class", - "null", - "roof_deck_attachment", - "terrain_roughness" + 'height_class', + 'roof_cover', + 'shutters', + 'wind_debris_class', + 'null', + 'roof_deck_attachment', + 'terrain_roughness', ] - - + elif model_id.startswith('HUEF.H'): model_features = [ - "height_class", - "roof_cover", - "wind_debris_class", - "roof_deck_attachment", - "shutters", - "terrain_roughness" + 'height_class', + 'roof_cover', + 'wind_debris_class', + 'roof_deck_attachment', + 'shutters', + 'terrain_roughness', ] - + elif model_id.startswith(('HUEF.PS', 'HUEF.EO')): model_features = [ - "roof_cover", - "shutters", - "wind_debris_class", - "roof_deck_attachment", - "window_area", - "terrain_roughness" + 'roof_cover', + 'shutters', + 'wind_debris_class', + 'roof_deck_attachment', + 'window_area', + 'terrain_roughness', ] - - model_id += "." + ".".join([f"{get_feature(feature, gi)}" for feature in model_features]) - #print("- - - - - - - MODEL ID - - - - - - -") - #print("current: ", model_id) - #print("original:", gi.get('Wind_Config','')) - #print("- - - - - - - MODEL ID - - - - - - -") + model_id += '.' + '.'.join( + [f'{get_feature(feature, gi)}' for feature in model_features] + ) + + # print("- - - - - - - MODEL ID - - - - - - -") + # print("current: ", model_id) + # print("original:", gi.get('Wind_Config','')) + # print("- - - - - - - MODEL ID - - - - - - -") comp = pd.DataFrame( - {f"{model_id}": ["ea", 1, 1, 1, "N/A"]}, # noqa: E241 - index=["Units", "Location", "Direction", "Theta_0", "Family"], # noqa: E231, E251 + {f'{model_id}': ['ea', 1, 1, 1, 'N/A']}, + index=['Units', 'Location', 'Direction', 'Theta_0', 'Family'], ).T dl_ap = { - "Asset": { - "ComponentAssignmentFile": "CMP_QNT.csv", - "ComponentDatabase": "Hazus Hurricane Wind - Buildings", - "NumberOfStories": 1, # there is only one component in a building-level resolution + 'Asset': { + 'ComponentAssignmentFile': 'CMP_QNT.csv', + 'ComponentDatabase': 'Hazus Hurricane Wind - Buildings', + 'NumberOfStories': 1, # there is only one component in a building-level resolution }, - "Damage": {"DamageProcess": "Hazus Hurricane"}, - "Demands": {}, - "Losses": { - "Repair": { - "ConsequenceDatabase": "Hazus Hurricane Wind - Buildings", - "MapApproach": "Automatic", - "DecisionVariables": { - "Cost": True, - "Carbon": False, - "Energy": False, - "Time": False - } + 'Damage': {'DamageProcess': 'Hazus Hurricane'}, + 'Demands': {}, + 'Losses': { + 'Repair': { + 'ConsequenceDatabase': 'Hazus Hurricane Wind - Buildings', + 'MapApproach': 'Automatic', + 'DecisionVariables': { + 'Cost': True, + 'Carbon': False, + 'Energy': False, + 'Time': False, + }, } }, - "Options": { - "NonDirectionalMultipliers": {"ALL": 1.0}, + 'Options': { + 'NonDirectionalMultipliers': {'ALL': 1.0}, }, } diff --git a/pyproject.toml b/pyproject.toml index df4d3179..1eced2f6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,7 @@ line-length = 85 [tool.ruff.lint] # Enable all known categories select = ["ALL"] -ignore = ["ANN", "D211", "D212", "Q000", "Q003", "COM812", "D203", "ISC001", "E501", "ERA001", "PGH003", "FIX002", "TD003", "S101", "N801", "S311", "G004", "SIM102", "SIM108", "NPY002", "F401", "INP001"] +ignore = ["ANN", "D211", "D212", "Q000", "Q003", "COM812", "D203", "ISC001", "E501", "ERA001", "PGH003", "FIX002", "TD003", "S101", "N801", "S311", "G004", "SIM102", "SIM108", "NPY002", "F401", "INP001", "PLR2004", "D100"] preview = false [tool.ruff.lint.per-file-ignores] @@ -36,4 +36,4 @@ quote-style = "single" [tool.codespell] ignore-words = ["ignore_words.txt"] -skip = ["*.html", "*/build/*"] +skip = ["*.html", "*/build/*", "*/DB/*"] diff --git a/seismic/building/portfolio/Hazus v5.1/data_sources/generate_library_files.py b/seismic/building/portfolio/Hazus v5.1/data_sources/generate_library_files.py index a1420a81..720c41dc 100644 --- a/seismic/building/portfolio/Hazus v5.1/data_sources/generate_library_files.py +++ b/seismic/building/portfolio/Hazus v5.1/data_sources/generate_library_files.py @@ -124,7 +124,7 @@ def create_Hazus_EQ_fragility_db( # noqa: C901, N802 for dl in design_levels: if bt in S_data['EDP_limits'][dl]: # add a dot in bt between structure and height labels, if needed - if (len(bt) > 2) and (bt[-1] in {'L', 'M', 'H'}): # noqa: PLR2004 + if (len(bt) > 2) and (bt[-1] in {'L', 'M', 'H'}): bt_exp = f'{bt[:-1]}.{bt[-1]}' st = bt[:-1] hc = bt[-1] @@ -212,7 +212,7 @@ def create_Hazus_EQ_fragility_db( # noqa: C901, N802 'Fragility_beta' ][dl] - if LS_i == 4: # noqa: PLR2004 + if LS_i == 4: p_coll = S_data['P_collapse'][bt] df_db.loc[counter, f'LS{LS_i}-DamageStateWeights'] = ( f'{1.0 - p_coll} | {p_coll}' @@ -341,7 +341,7 @@ def create_Hazus_EQ_fragility_db( # noqa: C901, N802 for dl in design_levels: if bt in LF_data['EDP_limits'][dl]: # add a dot in bt between structure and height labels, if needed - if (len(bt) > 2) and (bt[-1] in {'L', 'M', 'H'}): # noqa: PLR2004 + if (len(bt) > 2) and (bt[-1] in {'L', 'M', 'H'}): bt_exp = f'{bt[:-1]}.{bt[-1]}' st = bt[:-1] hc = bt[-1] @@ -428,7 +428,7 @@ def create_Hazus_EQ_fragility_db( # noqa: C901, N802 'Fragility_beta' ][dl] - if LS_i == 4: # noqa: PLR2004 + if LS_i == 4: p_coll = LF_data['P_collapse'][bt] df_db.loc[counter, f'LS{LS_i}-DamageStateWeights'] = ( f'{1.0 - p_coll} | {p_coll}' @@ -652,7 +652,7 @@ def create_Hazus_EQ_repair_db( # noqa: C901, N802 ) # DS4 and DS5 have identical repair consequences - if DS_i == 5: # noqa: PLR2004 + if DS_i == 5: ds_i = 4 else: ds_i = DS_i @@ -772,7 +772,7 @@ def create_Hazus_EQ_repair_db( # noqa: C901, N802 ds_meta = frag_meta['Meta']['Collections']['LF']['DamageStates'] for DS_i in range(1, 6): # noqa: N806 # DS4 and DS5 have identical repair consequences - if DS_i == 5: # noqa: PLR2004 + if DS_i == 5: ds_i = 4 else: ds_i = DS_i diff --git a/seismic/building/portfolio/Hazus v6.1/data_sources/generate_library_files.py b/seismic/building/portfolio/Hazus v6.1/data_sources/generate_library_files.py index 25cb97e2..7866eea4 100644 --- a/seismic/building/portfolio/Hazus v6.1/data_sources/generate_library_files.py +++ b/seismic/building/portfolio/Hazus v6.1/data_sources/generate_library_files.py @@ -126,7 +126,7 @@ def create_Hazus_EQ_fragility_db( # noqa: C901, N802 for dl in design_levels: if bt in S_data['EDP_limits'][dl]: # add a dot in bt between structure and height labels, if needed - if (len(bt) > 2) and (bt[-1] in {'L', 'M', 'H'}): # noqa: PLR2004 + if (len(bt) > 2) and (bt[-1] in {'L', 'M', 'H'}): bt_exp = f'{bt[:-1]}.{bt[-1]}' st = bt[:-1] hc = bt[-1] @@ -214,7 +214,7 @@ def create_Hazus_EQ_fragility_db( # noqa: C901, N802 'Fragility_beta' ][dl] - if LS_i == 4: # noqa: PLR2004 + if LS_i == 4: p_coll = S_data['P_collapse'][bt] df_db.loc[counter, f'LS{LS_i}-DamageStateWeights'] = ( f'{1.0 - p_coll} | {p_coll}' @@ -345,7 +345,7 @@ def create_Hazus_EQ_fragility_db( # noqa: C901, N802 continue if bt in LF_data['EDP_limits'][dl]: # add a dot in bt between structure and height labels, if needed - if (len(bt) > 2) and (bt[-1] in {'L', 'M', 'H'}): # noqa: PLR2004 + if (len(bt) > 2) and (bt[-1] in {'L', 'M', 'H'}): bt_exp = f'{bt[:-1]}.{bt[-1]}' st = bt[:-1] hc = bt[-1] @@ -432,7 +432,7 @@ def create_Hazus_EQ_fragility_db( # noqa: C901, N802 'Fragility_beta' ][dl] - if LS_i == 4: # noqa: PLR2004 + if LS_i == 4: p_coll = LF_data['P_collapse'][bt] df_db.loc[counter, f'LS{LS_i}-DamageStateWeights'] = ( f'{1.0 - p_coll} | {p_coll}' @@ -656,7 +656,7 @@ def create_Hazus_EQ_repair_db( # noqa: C901, N802 ) # DS4 and DS5 have identical repair consequences - if DS_i == 5: # noqa: PLR2004 + if DS_i == 5: ds_i = 4 else: ds_i = DS_i @@ -776,7 +776,7 @@ def create_Hazus_EQ_repair_db( # noqa: C901, N802 ds_meta = frag_meta['Meta']['Collections']['LF']['DamageStates'] for DS_i in range(1, 6): # noqa: N806 # DS4 and DS5 have identical repair consequences - if DS_i == 5: # noqa: PLR2004 + if DS_i == 5: ds_i = 4 else: ds_i = DS_i diff --git a/seismic/building/portfolio/Hazus v6.1/pelicun_config.py b/seismic/building/portfolio/Hazus v6.1/pelicun_config.py index 303b15c8..850075f6 100644 --- a/seismic/building/portfolio/Hazus v6.1/pelicun_config.py +++ b/seismic/building/portfolio/Hazus v6.1/pelicun_config.py @@ -41,12 +41,13 @@ from pathlib import Path import jsonschema -from jsonschema import validate import pandas as pd +from jsonschema import validate -def auto_populate(aim): + +def auto_populate(aim): # noqa: C901 """ - Automatically creates a performance model for PGA-based Hazus EQ analysis. + Automatically creates a performance model for Hazus EQ analysis. Parameters ---------- @@ -68,33 +69,27 @@ def auto_populate(aim): Component assignment - Defines the components (in rows) and their location, direction, and quantity (in columns). """ - # extract the General Information - gi = aim.get("GeneralInformation") + gi = aim.get('GeneralInformation') # make sure missing data is properly represented as null in the JSON for key, item in gi.items(): - if pd.isna(item) or item == "": + if pd.isna(item) or item == '': gi[key] = None # add configuration data to the gi if it is not already there dl_app_data = aim['Applications']['DL']['ApplicationData'] - if gi.get("GroundFailure", None) == None: - gi["GroundFailure"] = dl_app_data.get('ground_failure',None) - if gi.get("LifelineFacility", None) == None: - gi["LifelineFacility"] = dl_app_data.get('lifeline_facility', None) - if gi.get("StoryResolution", None) == None: - gi["StoryResolution"] = dl_app_data.get('story_resolution', None) - - if gi.get("StoryResolution"): - return auto_populate_story(aim) + if gi.get('GroundFailure', None) is None: + gi['GroundFailure'] = dl_app_data.get('ground_failure', None) + if gi.get('LifelineFacility', None) is None: + gi['LifelineFacility'] = dl_app_data.get('lifeline_facility', None) # load the schema assuming it is called "input_schema.json" and it is # stored next to the mapping script current_file_path = Path(__file__) current_directory = current_file_path.parent - with Path(current_directory / "input_schema.json").open(encoding="utf-8") as f: + with Path(current_directory / 'input_schema.json').open(encoding='utf-8') as f: input_schema = json.load(f) # validate the provided features against the required inputs @@ -102,115 +97,113 @@ def auto_populate(aim): validate(instance=gi, schema=input_schema) except jsonschema.exceptions.ValidationError as exc: # type: ignore msg = ( - "The provided building information does not conform to the input" - " requirements for the chosen damage and loss model." + 'The provided building information does not conform to the input' + ' requirements for the chosen damage and loss model.' ) raise ValueError(msg) from exc # prepare the labels for model IDs - structure_type = gi["StructureType"] + structure_type = gi['StructureType'] design_level_map = { - "Pre-Code": "PC", - "Low-Code": "LC", - "Moderate-Code": "MC", - "High-Code": "HC", - "Very High-Code": "VC", - "Severe-Code": "SC" + 'Pre-Code': 'PC', + 'Low-Code': 'LC', + 'Moderate-Code': 'MC', + 'High-Code': 'HC', + 'Very High-Code': 'VC', + 'Severe-Code': 'SC', } - design_level = design_level_map[gi["DesignLevel"]] + design_level = design_level_map[gi['DesignLevel']] - height_class_map = {"Low-Rise": "L", "Mid-Rise": "M", "High-Rise": "H"} - height_class_data = gi.get("HeightClass") + height_class_map = {'Low-Rise': 'L', 'Mid-Rise': 'M', 'High-Rise': 'H'} + height_class_data = gi.get('HeightClass') # some structural systems have no height class defined in Hazus if structure_type in ['W1', 'W2', 'S3', 'PC1', 'MH']: height_class_data = None - if gi.get("LifelineFacility"): - + if gi.get('LifelineFacility'): if height_class_data is not None: height_class = height_class_map[height_class_data] - model_id = f"LF.{structure_type}.{height_class}.{design_level}" + model_id = f'LF.{structure_type}.{height_class}.{design_level}' else: - model_id = f"LF.{structure_type}.{design_level}" + model_id = f'LF.{structure_type}.{design_level}' comp = pd.DataFrame( - {f"{model_id}": ["ea", 1, 1, 1, "N/A"]}, # noqa: E241 - index=["Units", "Location", "Direction", "Theta_0", "Family"], # noqa: E231, E251 + {f'{model_id}': ['ea', 1, 1, 1, 'N/A']}, + index=['Units', 'Location', 'Direction', 'Theta_0', 'Family'], ).T else: - if height_class_data is not None: height_class = height_class_map[height_class_data] - str_model_id = f"STR.{structure_type}.{height_class}.{design_level}" + str_model_id = f'STR.{structure_type}.{height_class}.{design_level}' else: - str_model_id = f"STR.{structure_type}.{design_level}" + str_model_id = f'STR.{structure_type}.{design_level}' nsd_model_id = 'NSD' nsa_model_id = f'NSA.{design_level}' comp = pd.DataFrame( { - f"{str_model_id}": ["ea", 1, 1, 1, "N/A"], - f"{nsd_model_id}": ["ea", 1, 0, 1, "N/A"], - f"{nsa_model_id}": ["ea", 1, 1, 1, "N/A"], - }, # noqa: E241 - index=["Units", "Location", "Direction", "Theta_0", "Family"], # noqa: E231, E251 + f'{str_model_id}': ['ea', 1, 1, 1, 'N/A'], + f'{nsd_model_id}': ['ea', 1, 0, 1, 'N/A'], + f'{nsa_model_id}': ['ea', 1, 1, 1, 'N/A'], + }, + index=['Units', 'Location', 'Direction', 'Theta_0', 'Family'], ).T # if needed, add components to simulate damage from ground failure - if gi.get("GroundFailure"): - foundation_type_map = {"Shallow": "S", "Deep": "D"} - foundation_type = foundation_type_map[gi["FoundationType"]] + if gi.get('GroundFailure'): + foundation_type_map = {'Shallow': 'S', 'Deep': 'D'} + foundation_type = foundation_type_map[gi['FoundationType']] - gf_model_id_h = f"GF.H.{foundation_type}" - gf_model_id_v = f"GF.V.{foundation_type}" + gf_model_id_h = f'GF.H.{foundation_type}' + gf_model_id_v = f'GF.V.{foundation_type}' comp_gf = pd.DataFrame( { - f"{gf_model_id_h}": ["ea", 1, 1, 1, "N/A"], # noqa: E201, E231, E241 - f"{gf_model_id_v}": ["ea", 1, 3, 1, "N/A"], - }, # noqa: E201, E231, E241 - index=["Units", "Location", "Direction", "Theta_0", "Family"], # noqa: E201, E231, E251 + f'{gf_model_id_h}': ['ea', 1, 1, 1, 'N/A'], + f'{gf_model_id_v}': ['ea', 1, 3, 1, 'N/A'], + }, + index=['Units', 'Location', 'Direction', 'Theta_0', 'Family'], ).T comp = pd.concat([comp, comp_gf], axis=0) # get the occupancy class - occupancy_type = gi["OccupancyClass"] + occupancy_type = gi['OccupancyClass'] dl_ap = { - "Asset": { - "ComponentAssignmentFile": "CMP_QNT.csv", - "ComponentDatabase": "Hazus Earthquake - Buildings", - "NumberOfStories": 1, - "OccupancyType": f"{occupancy_type}" + 'Asset': { + 'ComponentAssignmentFile': 'CMP_QNT.csv', + 'ComponentDatabase': 'Hazus Earthquake - Buildings', + 'NumberOfStories': 1, + 'OccupancyType': f'{occupancy_type}', }, - "Damage": {"DamageProcess": "Hazus Earthquake - Buildings"}, - "Demands": {}, - "Losses": { - "Repair": { - "ConsequenceDatabase": "Hazus Earthquake - Buildings", - "MapApproach": "Automatic", - "DecisionVariables": { - "Cost": True, - "Carbon": False, - "Energy": False, - "Time": True - } + 'Damage': {'DamageProcess': 'Hazus Earthquake - Buildings'}, + 'Demands': {}, + 'Losses': { + 'Repair': { + 'ConsequenceDatabase': 'Hazus Earthquake - Buildings', + 'MapApproach': 'Automatic', + 'DecisionVariables': { + 'Cost': True, + 'Carbon': False, + 'Energy': False, + 'Time': True, + }, } }, - "Options": { - "NonDirectionalMultipliers": {"ALL": 1.0}, + 'Options': { + 'NonDirectionalMultipliers': {'ALL': 1.0}, }, } - if gi.get("LifelineFacility"): - dl_ap['Damage'].update({ - 'DamageProcess': 'Hazus Earthquake - Lifeline Facilities' - }) + if gi.get('LifelineFacility'): + dl_ap['Damage'].update( + {'DamageProcess': 'Hazus Earthquake - Lifeline Facilities'} + ) - return gi, dl_ap, comp \ No newline at end of file + return gi, dl_ap, comp diff --git a/seismic/building/subassembly/Hazus v5.1/data_sources/generate_library_files.py b/seismic/building/subassembly/Hazus v5.1/data_sources/generate_library_files.py index cf85fb6a..47940881 100644 --- a/seismic/building/subassembly/Hazus v5.1/data_sources/generate_library_files.py +++ b/seismic/building/subassembly/Hazus v5.1/data_sources/generate_library_files.py @@ -124,7 +124,7 @@ def create_Hazus_EQ_fragility_db( # noqa: C901, N802 for dl in design_levels: if bt in S_data['EDP_limits'][dl]: # add a dot in bt between structure and height labels, if needed - if (len(bt) > 2) and (bt[-1] in {'L', 'M', 'H'}): # noqa: PLR2004 + if (len(bt) > 2) and (bt[-1] in {'L', 'M', 'H'}): bt_exp = f'{bt[:-1]}.{bt[-1]}' st = bt[:-1] hc = bt[-1] @@ -218,7 +218,7 @@ def create_Hazus_EQ_fragility_db( # noqa: C901, N802 'Fragility_beta' ][dl] - if LS_i == 4: # noqa: PLR2004 + if LS_i == 4: p_coll = S_data['P_collapse'][bt] df_db.loc[counter, f'LS{LS_i}-DamageStateWeights'] = ( f'{1.0 - p_coll} | {p_coll}' @@ -535,7 +535,7 @@ def create_Hazus_EQ_repair_db( # noqa: C901, N802 ) # DS4 and DS5 have identical repair consequences - if DS_i == 5: # noqa: PLR2004 + if DS_i == 5: ds_i = 4 else: ds_i = DS_i diff --git a/seismic/building/subassembly/Hazus v5.1/pelicun_config.py b/seismic/building/subassembly/Hazus v5.1/pelicun_config.py index 40077c60..e80a8f47 100644 --- a/seismic/building/subassembly/Hazus v5.1/pelicun_config.py +++ b/seismic/building/subassembly/Hazus v5.1/pelicun_config.py @@ -41,8 +41,8 @@ from pathlib import Path import jsonschema -from jsonschema import validate import pandas as pd +from jsonschema import validate ap_design_level = {1940: 'LC', 1975: 'MC', 2100: 'HC'} # ap_DesignLevel = {1940: 'PC', 1940: 'LC', 1975: 'MC', 2100: 'HC'} @@ -74,56 +74,74 @@ } -def story_scale(stories, comp_type): # noqa: C901 +def story_scale(stories, comp_type): # noqa: C901, PLR0911 + """ + Calculate story scaling factors for Hazus seismic components. + + This function returns scaling factors based on the number of stories + and component type. + + Parameters + ---------- + stories : int + Number of stories in the building. + comp_type : str + Component type ('NSA', 'S', or 'NSD'). + + Returns + ------- + float or None + Scaling factor for the given number of stories and component type. + Returns None if component type is not recognized. + """ if comp_type == 'NSA': if stories == 1: return 1.00 - elif stories == 2: + if stories == 2: return 1.22 - elif stories == 3: + if stories == 3: return 1.40 - elif stories == 4: + if stories == 4: return 1.45 - elif stories == 5: + if stories == 5: return 1.50 - elif stories == 6: + if stories == 6: return 1.90 - elif stories == 7: + if stories == 7: return 2.05 - elif stories == 8: + if stories == 8: return 2.15 - elif stories == 9: + if stories == 9: return 2.20 - elif (stories >= 10) and (stories < 30): + if (stories >= 10) and (stories < 30): return 2.30 + (stories - 10) * 0.04 - elif stories >= 30: + if stories >= 30: return 3.10 - else: - return 1.0 + return 1.0 - elif comp_type in ['S', 'NSD']: + if comp_type in ['S', 'NSD']: if stories == 1: return 1.45 - elif stories == 2: + if stories == 2: return 1.90 - elif stories == 3: + if stories == 3: return 2.50 - elif stories == 4: + if stories == 4: return 2.75 - elif stories == 5: + if stories == 5: return 3.00 - elif stories in (6, 7, 8): + if stories in (6, 7, 8): return 3.50 - elif stories == 9: + if stories == 9: return 4.50 - elif (stories >= 10) and (stories < 50): + if (stories >= 10) and (stories < 50): return 4.50 + (stories - 10) * 0.07 - elif stories >= 50: + if stories >= 50: return 7.30 - else: - return 1.0 + return 1.0 return None + def auto_populate(aim): """ Automatically creates a performance model for story EDP-based Hazus EQ analysis. @@ -154,13 +172,12 @@ def auto_populate(aim): gi = aim.get('GeneralInformation', None) if gi is None: - # TODO: show an error message + # TODO: show an error message # noqa: TD002 pass # initialize the auto-populated GI gi_ap = gi.copy() - asset_type = aim['assetType'] ground_failure = aim['Applications']['DL']['ApplicationData']['ground_failure'] # get the building parameters @@ -221,17 +238,17 @@ def auto_populate(aim): if ground_failure: foundation_type = 'S' - FG_GF_H = f'GF.H.{foundation_type}' - FG_GF_V = f'GF.V.{foundation_type}' - CMP_GF = pd.DataFrame( + horizontal_ground_failure_model_id = f'GF.H.{foundation_type}' + vertical_ground_failure_model_id = f'GF.V.{foundation_type}' + ground_failure_component = pd.DataFrame( { - f'{FG_GF_H}': ['ea', 1, 1, 1, 'N/A'], - f'{FG_GF_V}': ['ea', 1, 3, 1, 'N/A'], + f'{horizontal_ground_failure_model_id}': ['ea', 1, 1, 1, 'N/A'], + f'{vertical_ground_failure_model_id}': ['ea', 1, 3, 1, 'N/A'], }, index=['Units', 'Location', 'Direction', 'Theta_0', 'Family'], ).T - comp = pd.concat([comp, CMP_GF], axis=0) + comp = pd.concat([comp, ground_failure_component], axis=0) # get the occupancy class if gi['OccupancyClass'] in ap_occupancy: diff --git a/seismic/power_network/portfolio/Hazus v5.1/pelicun_config.py b/seismic/power_network/portfolio/Hazus v5.1/pelicun_config.py index a0e1dff8..ea28eb15 100644 --- a/seismic/power_network/portfolio/Hazus v5.1/pelicun_config.py +++ b/seismic/power_network/portfolio/Hazus v5.1/pelicun_config.py @@ -44,10 +44,10 @@ import numpy as np import pandas as pd - import pelicun -def auto_populate(aim): # noqa: C901 + +def auto_populate(aim): # noqa: C901, PLR0912, PLR0915 """ Automatically creates a performance model for PGA-based Hazus EQ analysis. @@ -72,21 +72,16 @@ def auto_populate(aim): # noqa: C901 Component assignment - Defines the components (in rows) and their location, direction, and quantity (in columns). """ - # extract the General Information gi = aim.get('GeneralInformation', None) if gi is None: - # TODO: show an error message + # TODO: show an error message # noqa: TD002 pass # initialize the auto-populated GI gi_ap = gi.copy() - asset_type = aim['assetType'] - dl_app_data = aim['Applications']['DL']['ApplicationData'] - ground_failure = dl_app_data['ground_failure'] - # initialize the auto-populated GI power_asset_type = gi_ap.get('type', 'MISSING') asset_name = gi_ap.get('AIM_id', None) @@ -101,7 +96,8 @@ def auto_populate(aim): # noqa: C901 f' substation "{asset_name}" assumed to be ' '" Low Voltage".' ) - print(msg) + # TODO (azs): implement a logging system instead of printing these messages + print(msg) # noqa: T201 substation_voltage = 'low' if isinstance(substation_voltage, str): @@ -169,7 +165,8 @@ def auto_populate(aim): # noqa: C901 substation_anchored = gi_ap.get('Anchored', None) if substation_anchored is None: - print( + # TODO (azs): implement a logging system instead of printing these messages + print( # noqa: T201 'Substation feature "Anchored" is missing. ' f' substation "{asset_name}" assumed to be ' '" Unanchored".' @@ -245,7 +242,8 @@ def auto_populate(aim): # noqa: C901 ep_c_anchored = None if circuit_anchored is None: - print( + # TODO (azs): implement a logging system instead of printing these messages + print( # noqa: T201 'Circuit feature "Anchored" is missing. ' f' Circuit "{asset_name}" assumed to be ' '" Unanchored".' @@ -324,7 +322,8 @@ def auto_populate(aim): # noqa: C901 f' Generation "{asset_name}" assumed to be ' '"Small".' ) - print(msg) + # TODO (azs): implement a logging system instead of printing these messages + print(msg) # noqa: T201 # if the power feature is missing, the generation is assumed # to be small ep_g_size = 'small' @@ -352,7 +351,8 @@ def auto_populate(aim): # noqa: C901 '"Output" value. The unit for Generation ' f'"{asset_name}" is assumed to be "MW".' ) - print(msg) + # TODO (azs): implement a logging system instead of printing these messages + print(msg) # noqa: T201 power_unit = 'mw' @@ -390,7 +390,8 @@ def auto_populate(aim): # noqa: C901 'value. So the size of the Generation is assumed ' 'to be "Small".' ) - print(msg) + # TODO (azs): implement a logging system instead of printing these messages + print(msg) # noqa: T201 ep_g_size = 'small' @@ -416,7 +417,8 @@ def auto_populate(aim): # noqa: C901 f' Circuit "{asset_name}" assumed to be ' '" Unanchored".' ) - print(msg) + # TODO (azs): implement a logging system instead of printing these messages + print(msg) # noqa: T201 generation_anchored = False diff --git a/seismic/transportation_network/portfolio/Hazus v5.1/pelicun_config.py b/seismic/transportation_network/portfolio/Hazus v5.1/pelicun_config.py index 2f3a0b5b..20e272d5 100644 --- a/seismic/transportation_network/portfolio/Hazus v5.1/pelicun_config.py +++ b/seismic/transportation_network/portfolio/Hazus v5.1/pelicun_config.py @@ -44,15 +44,13 @@ import numpy as np import pandas as pd - import pelicun from pelicun.assessment import DLCalculationAssessment + # Convert common length units -def convertUnits(value, unit_in, unit_out): - """ - Convert units. - """ +def convert_length_units(value, unit_in, unit_out): + """Convert units.""" aval_types = ['m', 'mm', 'cm', 'km', 'inch', 'ft', 'mile'] m = 1.0 mm = 0.001 * m @@ -71,7 +69,8 @@ def convertUnits(value, unit_in, unit_out): 'mile': mile, } if (unit_in not in aval_types) or (unit_out not in aval_types): - print( + # TODO (azs): implement a logging system instead of printing these messages + print( # noqa: T201 f'The unit {unit_in} or {unit_out} ' f'are used in auto_population but not supported' ) @@ -79,7 +78,28 @@ def convertUnits(value, unit_in, unit_out): return value * scale_map[unit_in] / scale_map[unit_out] -def getHAZUSBridgeK3DModifier(hazus_class, aim): +def get_hazus_bridge_k3d_modifier(hazus_class, aim): + """ + Calculate K_3D modifier for Hazus bridge seismic analysis. + + This function computes the K_3D factor based on the bridge class and + number of spans according to Hazus methodology. The K_3D factor modifies + the piers' 2-dimensional capacity to allow for the 3-dimensional arch + action in the deck. + + Parameters + ---------- + hazus_class : str + Hazus bridge classification (e.g., 'HWB1', 'HWB2', etc.). + aim : dict + Asset Information Model containing bridge characteristics, + specifically 'NumOfSpans'. + + Returns + ------- + float + K_3D modifier value for the given bridge class and span configuration. + """ # In HAZUS, the K_3D for HWB28 is undefined, so we return 1, i.e., no scaling # The K-3D factors for HWB3 and HWB4 are defined as EQ1, which leads to division by zero # This is an error in the HAZUS documentation, and we assume that the factors are 1 for these classes @@ -124,19 +144,38 @@ def getHAZUSBridgeK3DModifier(hazus_class, aim): } if hazus_class in ['HWB3', 'HWB4', 'HWB28']: return 1 - else: - n = aim['NumOfSpans'] - if n < 2: - return 1 - a = factors[mapping[hazus_class]][0] - b = factors[mapping[hazus_class]][1] - return 1 + a / ( - n - b - ) # This is the original form in Mander and Basoz (1999) - - -def convertBridgeToHAZUSclass(aim): # noqa: C901 - # TODO: replace labels in AIM with standard CamelCase versions + n = aim['NumOfSpans'] + if n < 2: + return 1 + a = factors[mapping[hazus_class]][0] + b = factors[mapping[hazus_class]][1] + return 1 + a / (n - b) # This is the original form in Mander and Basoz (1999) + + +def classify_bridge_for_hazus(aim): # noqa: C901 + """ + Classify bridge into Hazus bridge categories. + + This function determines the appropriate Hazus bridge classification + (HWB1-HWB28) based on bridge characteristics including structure type, + state code, year built, number of spans, and maximum span length. + + Parameters + ---------- + aim : dict + Asset Information Model containing bridge characteristics: + - BridgeClass: Structure type code + - StateCode: State where bridge is located + - YearBuilt: Year of construction + - NumOfSpans: Number of spans + - MaxSpanLength: Length of maximum span + - units: Dictionary containing length units + + Returns + ------- + str + Hazus bridge classification code (e.g., 'HWB1', 'HWB2', etc.). + """ structure_type = aim['BridgeClass'] # if ( # type(structure_type) == str @@ -151,7 +190,7 @@ def convertBridgeToHAZUSclass(aim): # noqa: C901 num_span = aim['NumOfSpans'] len_max_span = aim['MaxSpanLength'] len_unit = aim['units']['length'] - len_max_span = convertUnits(len_max_span, len_unit, 'm') + len_max_span = convert_length_units(len_max_span, len_unit, 'm') seismic = (int(state) == 6 and int(yr_built) >= 1975) or ( int(state) != 6 and int(yr_built) >= 1990 @@ -199,11 +238,10 @@ def convertBridgeToHAZUSclass(aim): # noqa: C901 bridge_class = 'HWB12' else: bridge_class = 'HWB13' + elif state != 6: + bridge_class = 'HWB24' else: - if state != 6: - bridge_class = 'HWB24' - else: - bridge_class = 'HWB25' + bridge_class = 'HWB25' else: bridge_class = 'HWB14' @@ -239,13 +277,38 @@ def convertBridgeToHAZUSclass(aim): # noqa: C901 else: bridge_class = 'HWB23' - # TODO: review and add HWB24-27 rules - # TODO: also double check rules for HWB10-11 and HWB22-23 + # TODO: review and add HWB24-27 rules # noqa: TD002 + # TODO: also double check rules for HWB10-11 and HWB22-23 # noqa: TD002 return bridge_class -def getHAZUSBridgePGDModifier(hazus_class, aim): +def get_hazus_bridge_pgd_modifier(hazus_class, aim): + """ + Calculate PGD modifier for Hazus bridge seismic analysis. + + This function computes the f1 and f2 factors that scale the Permanent + Ground Deformation (PGD) capacity of bridges in damage states 2-4. The + factors are based on bridge geometry and skew angle according to Hazus + methodology. + + Parameters + ---------- + hazus_class : str + Hazus bridge classification (e.g., 'HWB1', 'HWB2', etc.). + aim : dict + Asset Information Model containing bridge characteristics: + - DeckWidth: Width of bridge deck + - NumOfSpans: Number of spans + - Skew: Skew angle of bridge + - StructureLength: Total length of structure + + Returns + ------- + tuple of float + The f1 and f2 factors to modify PGD capacity for the given bridge class + and geometry. + """ # This is the original modifier in HAZUS, which gives inf if Skew is 0 # modifier1 = 0.5*AIM['StructureLength']/(AIM['DeckWidth']*AIM['NumOfSpans']*np.sin(AIM['Skew']/180.0*np.pi)) # Use the modifier that is corrected from HAZUS manual to achieve the asymptotic behavior @@ -290,28 +353,85 @@ def getHAZUSBridgePGDModifier(hazus_class, aim): return mapping[hazus_class][0], mapping[hazus_class][1] -def convertTunnelToHAZUSclass(aim) -> str: +def classify_tunnel_for_hazus(aim) -> str: + """ + Classify tunnel into Hazus tunnel categories. + + This function determines the appropriate Hazus tunnel classification + based on construction type. + + Parameters + ---------- + aim : dict + Asset Information Model containing tunnel characteristics, + specifically 'ConstructType'. + + Returns + ------- + str + Hazus tunnel classification code ('HTU1' or 'HTU2'). + """ if 'Bored' in aim['ConstructType'] or 'Drilled' in aim['ConstructType']: return 'HTU1' - elif 'Cut' in aim['ConstructType'] or 'Cover' in aim['ConstructType']: - return 'HTU2' - else: - # Select HTU2 for unclassified tunnels because it is more conservative. + if 'Cut' in aim['ConstructType'] or 'Cover' in aim['ConstructType']: return 'HTU2' + # Select HTU2 for unclassified tunnels because it is more conservative. + return 'HTU2' -def convertRoadToHAZUSclass(aim) -> str: +def classify_road_for_hazus(aim) -> str: + """ + Classify road into Hazus road categories. + + This function determines the appropriate Hazus road classification + based on road type. + + Parameters + ---------- + aim : dict + Asset Information Model containing road characteristics, + specifically 'RoadType'. + + Returns + ------- + str + Hazus road classification code ('HRD1' or 'HRD2'). + """ if aim['RoadType'] in ['Primary', 'Secondary']: return 'HRD1' - elif aim['RoadType'] == 'Residential': + if aim['RoadType'] == 'Residential': return 'HRD2' - else: - # many unclassified roads are urban roads - return 'HRD2' + # many unclassified roads are urban roads + return 'HRD2' + + +def get_hazus_bridge_slight_damage_modifier(hazus_class, aim): + """ + Calculate slight damage modifier for Hazus bridge seismic analysis. -def getHAZUSBridgeSlightDamageModifier(hazus_class, aim): + This function computes slight damage modifiers for specific Hazus bridge + classes based on spectral acceleration ratios. The modifier converts short + periods to an equivalent spectral amplitude at T=1.0 second. Since this + modifier is a function of the spectral shape of the ground motion demand, + its value is specific to each demand realization. This function returns + a specific factor for each realization in the form of an operation string + the Pelicun can apply to modify the demand sample during the analysis. + + Parameters + ---------- + hazus_class : str + Hazus bridge classification (e.g., 'HWB1', 'HWB2', etc.). + aim : dict + Asset Information Model containing demand and configuration data. + + Returns + ------- + list of str or None + List of operation strings for sample-wise modification, or None + if the bridge class doesn't require modification. + """ if hazus_class in [ 'HWB1', 'HWB2', @@ -379,7 +499,7 @@ def getHAZUSBridgeSlightDamageModifier(hazus_class, aim): return operation -def auto_populate(aim): # noqa: C901 +def auto_populate(aim): """ Automatically creates a performance model for PGA-based Hazus EQ analysis. @@ -404,18 +524,16 @@ def auto_populate(aim): # noqa: C901 Component assignment - Defines the components (in rows) and their location, direction, and quantity (in columns). """ - # extract the General Information gi = aim.get('GeneralInformation', None) if gi is None: - # TODO: show an error message + # TODO: show an error message # noqa: TD002 pass # initialize the auto-populated GI gi_ap = gi.copy() - asset_type = aim['assetType'] dl_app_data = aim['Applications']['DL']['ApplicationData'] ground_failure = dl_app_data['ground_failure'] @@ -428,20 +546,20 @@ def auto_populate(aim): # noqa: C901 gi['Skew'] = 45 # get the bridge class - bt = convertBridgeToHAZUSclass(gi) + bt = classify_bridge_for_hazus(gi) gi_ap['BridgeHazusClass'] = bt # fmt: off comp = pd.DataFrame( - {f'HWB.GS.{bt[3:]}': [ 'ea', 1, 1, 1, 'N/A']}, # noqa: E201, E241 - index = [ 'Units', 'Location', 'Direction', 'Theta_0', 'Family'] # noqa: E201, E251 + {f'HWB.GS.{bt[3:]}': [ 'ea', 1, 1, 1, 'N/A']}, + index = [ 'Units', 'Location', 'Direction', 'Theta_0', 'Family'] ).T # fmt: on # scaling_specification k_skew = np.sqrt(np.sin((90 - gi['Skew']) * np.pi / 180.0)) - k_3d = getHAZUSBridgeK3DModifier(bt, gi) - k_shape = getHAZUSBridgeSlightDamageModifier(bt, aim) + k_3d = get_hazus_bridge_k3d_modifier(bt, gi) + k_shape = get_hazus_bridge_slight_damage_modifier(bt, aim) scaling_specification = { f'HWB.GS.{bt[3:]}-1-1': { 'LS2': f'*{k_skew * k_3d}', @@ -455,14 +573,14 @@ def auto_populate(aim): # noqa: C901 if ground_failure: # fmt: off comp_gf = pd.DataFrame( - {f'HWB.GF': [ 'ea', 1, 1, 1, 'N/A']}, # noqa: E201, E241, F541 - index = [ 'Units', 'Location', 'Direction', 'Theta_0', 'Family'] # noqa: E201, E251 + {f'HWB.GF': [ 'ea', 1, 1, 1, 'N/A']}, # noqa: F541 + index = [ 'Units', 'Location', 'Direction', 'Theta_0', 'Family'] ).T # fmt: on comp = pd.concat([comp, comp_gf], axis=0) - f1, f2 = getHAZUSBridgePGDModifier(bt, gi) + f1, f2 = get_hazus_bridge_pgd_modifier(bt, gi) scaling_specification.update( { @@ -499,21 +617,21 @@ def auto_populate(aim): # noqa: C901 elif inf_type == 'HwyTunnel': # get the tunnel class - tt = convertTunnelToHAZUSclass(gi) + tt = classify_tunnel_for_hazus(gi) gi_ap['TunnelHazusClass'] = tt # fmt: off comp = pd.DataFrame( - {f'HTU.GS.{tt[3:]}': [ 'ea', 1, 1, 1, 'N/A']}, # noqa: E201, E241 - index = [ 'Units','Location','Direction','Theta_0','Family'] # noqa: E201, E231, E251 + {f'HTU.GS.{tt[3:]}': [ 'ea', 1, 1, 1, 'N/A']}, + index = [ 'Units','Location','Direction','Theta_0','Family'] ).T # fmt: on # if needed, add components to simulate damage from ground failure if ground_failure: # fmt: off comp_gf = pd.DataFrame( - {f'HTU.GF': [ 'ea', 1, 1, 1, 'N/A']}, # noqa: E201, E241, F541 - index = [ 'Units','Location','Direction','Theta_0','Family'] # noqa: E201, E231, E251 + {f'HTU.GF': [ 'ea', 1, 1, 1, 'N/A']}, # noqa: F541 + index = [ 'Units','Location','Direction','Theta_0','Family'] ).T # fmt: on @@ -540,21 +658,21 @@ def auto_populate(aim): # noqa: C901 } elif inf_type == 'Roadway': # get the road class - rt = convertRoadToHAZUSclass(gi) + rt = classify_road_for_hazus(gi) gi_ap['RoadHazusClass'] = rt # fmt: off comp = pd.DataFrame( {}, - index = [ 'Units','Location','Direction','Theta_0','Family'] # noqa: E201, E231, E251 + index = [ 'Units','Location','Direction','Theta_0','Family'] ).T # fmt: on if ground_failure: # fmt: off comp_gf = pd.DataFrame( - {f'HRD.GF.{rt[3:]}':[ 'ea', 1, 1, 1, 'N/A']}, # noqa: E201, E231, E241 - index = [ 'Units','Location','Direction','Theta_0','Family'] # noqa: E201, E231, E251 + {f'HRD.GF.{rt[3:]}':[ 'ea', 1, 1, 1, 'N/A']}, + index = [ 'Units','Location','Direction','Theta_0','Family'] ).T # fmt: on @@ -580,6 +698,7 @@ def auto_populate(aim): # noqa: C901 }, } else: - print('subtype not supported in HWY') + # TODO (azs): implement a logging system instead of printing these messages + print('subtype not supported in HWY') # noqa: T201 return gi_ap, dl_ap, comp diff --git a/seismic/water_network/portfolio/Hazus v6.1/pelicun_config.py b/seismic/water_network/portfolio/Hazus v6.1/pelicun_config.py index 60a3eb20..49e702f5 100644 --- a/seismic/water_network/portfolio/Hazus v6.1/pelicun_config.py +++ b/seismic/water_network/portfolio/Hazus v6.1/pelicun_config.py @@ -44,9 +44,9 @@ import numpy as np import pandas as pd - import pelicun + def auto_populate(aim): # noqa: C901 """ Automatically creates a performance model for PGA-based Hazus EQ analysis. @@ -72,20 +72,17 @@ def auto_populate(aim): # noqa: C901 Component assignment - Defines the components (in rows) and their location, direction, and quantity (in columns). """ - # extract the General Information gi = aim.get('GeneralInformation', None) if gi is None: - # TODO: show an error message + # TODO: show an error message # noqa: TD002 pass # initialize the auto-populated GI gi_ap = gi.copy() asset_type = aim['assetType'] - dl_app_data = aim['Applications']['DL']['ApplicationData'] - ground_failure = dl_app_data['ground_failure'] pipe_material_map = { 'CI': 'B', @@ -146,13 +143,15 @@ def auto_populate(aim): # noqa: C901 """ if pipe_material is None: if pipe_diameter > 20 * 0.0254: # 20 inches in meter - print( + # TODO (azs): implement a logging system instead of printing these messages + print( # noqa: T201 f'Asset {asset_name} is missing material. ' 'Material is assumed to be Cast Iron' ) pipe_material = 'CI' else: - print( + # TODO (azs): implement a logging system instead of printing these messages + print( # noqa: T201 f'Asset {asset_name} is missing material. Material is ' f'assumed to be Steel (ST)' ) @@ -167,7 +166,8 @@ def auto_populate(aim): # noqa: C901 'is assumed to be Ductile Steel.' ) - print(msg) + # TODO (azs): implement a logging system instead of printing these messages + print(msg) # noqa: T201 pipe_material = 'DS' else: @@ -176,7 +176,8 @@ def auto_populate(aim): # noqa: C901 'is assumed to be Brittle Steel.' ) - print(msg) + # TODO (azs): implement a logging system instead of printing these messages + print(msg) # noqa: T201 pipe_material = 'BS' pipe_flexibility = pipe_material_map.get(pipe_material, 'missing') @@ -233,7 +234,7 @@ def auto_populate(aim): # noqa: C901 ) demand_cloning_config = {} for edp in response_data.columns: - tag, location, direction = edp # noqa: F841 + tag, location, direction = edp demand_cloning_config['-'.join(edp)] = [ f'{tag}-{x}-{direction}' @@ -249,7 +250,7 @@ def auto_populate(aim): # noqa: C901 f'4_PWP.{pipe_flexibility}.GF-LOC': {'DS2': 'aggregate_DS2'}, } dmg_process_filename = 'dmg_process.json' - with open(dmg_process_filename, 'w', encoding='utf-8') as f: + with Path(dmg_process_filename).open('w', encoding='utf-8') as f: json.dump(dmg_process, f, indent=2) # Define the auto-populated config @@ -323,7 +324,8 @@ def auto_populate(aim): # noqa: C901 'The tank is assumed to be Steel ("S").' ) - print(msg) + # TODO (azs): implement a logging system instead of printing these messages + print(msg) # noqa: T201 tank_material = 'S' if tank_location == 'AG' and tank_material == 'W': @@ -334,7 +336,8 @@ def auto_populate(aim): # noqa: C901 'The tank is assumed to be Steel ("S").' ) - print(msg) + # TODO (azs): implement a logging system instead of printing these messages + print(msg) # noqa: T201 tank_material = 'S' if tank_location == 'B' and tank_material == 'S': @@ -345,7 +348,8 @@ def auto_populate(aim): # noqa: C901 'The tank is assumed to be Concrete ("C").' ) - print(msg) + # TODO (azs): implement a logging system instead of printing these messages + print(msg) # noqa: T201 tank_material = 'C' if tank_location == 'B' and tank_material == 'W': @@ -356,7 +360,8 @@ def auto_populate(aim): # noqa: C901 'to be Concrete ("C")' ) - print(msg) + # TODO (azs): implement a logging system instead of printing these messages + print(msg) # noqa: T201 tank_material = 'C' if tank_anchored == 1: @@ -388,12 +393,12 @@ def auto_populate(aim): # noqa: C901 } else: - print( + # TODO (azs): implement a logging system instead of printing these messages + print( # noqa: T201 f'Water Distribution network element type {wdn_element_type} ' f'is not supported in Hazus Earthquake - Potable Water' ) dl_ap = 'N/A' comp = None - return gi_ap, dl_ap, comp From 1406a6773142c5422528bec5fd3a0f449c3849c9 Mon Sep 17 00:00:00 2001 From: Adam Zsarnoczay <33822153+zsarnoczay@users.noreply.github.com> Date: Thu, 11 Sep 2025 22:56:40 -0700 Subject: [PATCH 3/4] docs: Prepare v2.1.0 release documentation This commit updates all documentation files in preparation for the v2.1.0 release: - Add comprehensive v2.1.0 release notes (doc/source/release_notes/v2.1.0.rst) detailing code quality improvements and enhanced Hazus assessment usability - Update README.md to reference v2.1.0 and highlight improvements over v2.0.0 - Add v2.1.0 section to CHANGELOG.md with detailed breakdown of changes and fixes All documentation follows established formatting conventions and maintains consistency with previous release documentation structure. --- CHANGELOG.md | 20 ++++++++++++++++++++ README.md | 2 +- doc/source/release_notes/index.rst | 3 ++- doc/source/release_notes/v1.0.0.rst | 4 ++-- doc/source/release_notes/v2.0.0.rst | 4 ++-- pyproject.toml | 2 +- 6 files changed, 28 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 886a5e87..0a493618 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 --- +## [2.1.0] - 2025-09-11 + +This release focuses on improving code quality and enhancing usability of Hazus assessments. The changes maintain backward compatibility while providing users with more flexibility in input specification and ensuring the codebase adheres to modern Python best practices. + +### Changed +- **Input Validation:** Relaxed validation constraints for seismic and flood assessments to improve usability: + - Allow HeightClass attribute for seismic structural systems (W1, W2, S3, PC1, MH) that don't require it in Hazus methodology + - Remove PlanArea field from auto-populated seismic configuration as it's no longer needed + - Allow RES1 occupancy buildings to have more than 3 stories in flood assessments, aligning with FEMA technical manual interpretation +- **Code Quality:** Comprehensive code formatting and linting improvements using Ruff across the entire codebase: + - Applied consistent code formatting across 15 Python files + - Fixed docstring formatting and missing docstring issues + - Cleaned up import statements and unused code + - Standardized quote usage and line spacing + +### Fixed +- Resolved spelling issues in comments + +--- + ## [2.0.0] - 2025-08-15 This release marks a major milestone for the Damage and Loss Model Library and the beginning of a more frequent and structured release schedule. After more than two years of continuous development, `v2.0.0` introduces a significantly improved data schema, a host of new models, and a documentation system for model discovery. diff --git a/README.md b/README.md index 63d2204a..0bc6e3bd 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ A curated, open-source repository of standardized model parameters and metadata The Damage and Loss Model Library (DLML) is a project from the NHERI SimCenter designed to address a critical gap in natural hazards engineering: the lack of a centralized, standardized, and easy-to-use repository for damage and loss models. This library provides the essential data—model parameters, descriptive metadata, and configuration files—that power natural hazard risk assessment simulations. -This `v2.0.0` release represents a significant evolution of the project, featuring an improved data schema, a host of new and updated models, and a documentation system. +This `v2.1.0` release continues the evolution of the project, building on the improved data schema, extensive model collection, and documentation system introduced in v2.0.0, with enhanced code quality and improved usability for Hazus assessments. **Key Features:** * **Standardized Data Schema:** A robust yet flexible schema for organizing models by hazard, asset type, and resolution, making it easy to use data and supporting new contributions. diff --git a/doc/source/release_notes/index.rst b/doc/source/release_notes/index.rst index 4c943767..e2cb75ce 100644 --- a/doc/source/release_notes/index.rst +++ b/doc/source/release_notes/index.rst @@ -7,11 +7,12 @@ Release Notes The following sections document the notable changes of each release. The sequence of all changes is available in the `commit logs `_. -Version 1 +Version 2 ----------- .. toctree:: :maxdepth: 2 + v2.1.0 v2.0.0 v1.0.0 diff --git a/doc/source/release_notes/v1.0.0.rst b/doc/source/release_notes/v1.0.0.rst index 355f1986..07195f3c 100644 --- a/doc/source/release_notes/v1.0.0.rst +++ b/doc/source/release_notes/v1.0.0.rst @@ -1,8 +1,8 @@ .. _changes_v1_0: -========================== +============================ Version 1.0.0 (May 19, 2023) -========================== +============================ Initial Release. diff --git a/doc/source/release_notes/v2.0.0.rst b/doc/source/release_notes/v2.0.0.rst index a157aa88..e5fcd5fc 100644 --- a/doc/source/release_notes/v2.0.0.rst +++ b/doc/source/release_notes/v2.0.0.rst @@ -1,8 +1,8 @@ .. _changes_v2_0: -============================= +=============================== Version 2.0.0 (August 15, 2025) -============================= +=============================== This release marks a major milestone for the Damage and Loss Model Library and the beginning of a more frequent and structured release schedule. After more than two years of continuous development, ``v2.0.0`` introduces a significantly improved data schema, a host of new models, and a documentation system for model discovery. diff --git a/pyproject.toml b/pyproject.toml index 1eced2f6..d3a1ec62 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "dlml" -version = "2.0.0" +version = "2.1.0" description = "Damage and Loss Model Library" readme = "README.md" license = {text = "BSD-3-Clause"} From 7fa531f0a1048f807a5425fec4a0f61ac3d65a7f Mon Sep 17 00:00:00 2001 From: Adam Zsarnoczay <33822153+zsarnoczay@users.noreply.github.com> Date: Thu, 11 Sep 2025 22:56:40 -0700 Subject: [PATCH 4/4] docs: Prepare v2.1.0 release documentation This commit updates all documentation files in preparation for the v2.1.0 release: - Add comprehensive v2.1.0 release notes (doc/source/release_notes/v2.1.0.rst) detailing code quality improvements and enhanced Hazus assessment usability - Update README.md to reference v2.1.0 and highlight improvements over v2.0.0 - Add v2.1.0 section to CHANGELOG.md with detailed breakdown of changes and fixes All documentation follows established formatting conventions and maintains consistency with previous release documentation structure. --- CHANGELOG.md | 20 ++++++++++++++++++++ README.md | 2 +- doc/source/release_notes/index.rst | 3 ++- doc/source/release_notes/v1.0.0.rst | 4 ++-- doc/source/release_notes/v2.0.0.rst | 4 ++-- doc/source/release_notes/v2.1.0.rst | 28 ++++++++++++++++++++++++++++ pyproject.toml | 2 +- 7 files changed, 56 insertions(+), 7 deletions(-) create mode 100644 doc/source/release_notes/v2.1.0.rst diff --git a/CHANGELOG.md b/CHANGELOG.md index 886a5e87..0a493618 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 --- +## [2.1.0] - 2025-09-11 + +This release focuses on improving code quality and enhancing usability of Hazus assessments. The changes maintain backward compatibility while providing users with more flexibility in input specification and ensuring the codebase adheres to modern Python best practices. + +### Changed +- **Input Validation:** Relaxed validation constraints for seismic and flood assessments to improve usability: + - Allow HeightClass attribute for seismic structural systems (W1, W2, S3, PC1, MH) that don't require it in Hazus methodology + - Remove PlanArea field from auto-populated seismic configuration as it's no longer needed + - Allow RES1 occupancy buildings to have more than 3 stories in flood assessments, aligning with FEMA technical manual interpretation +- **Code Quality:** Comprehensive code formatting and linting improvements using Ruff across the entire codebase: + - Applied consistent code formatting across 15 Python files + - Fixed docstring formatting and missing docstring issues + - Cleaned up import statements and unused code + - Standardized quote usage and line spacing + +### Fixed +- Resolved spelling issues in comments + +--- + ## [2.0.0] - 2025-08-15 This release marks a major milestone for the Damage and Loss Model Library and the beginning of a more frequent and structured release schedule. After more than two years of continuous development, `v2.0.0` introduces a significantly improved data schema, a host of new models, and a documentation system for model discovery. diff --git a/README.md b/README.md index 63d2204a..0bc6e3bd 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ A curated, open-source repository of standardized model parameters and metadata The Damage and Loss Model Library (DLML) is a project from the NHERI SimCenter designed to address a critical gap in natural hazards engineering: the lack of a centralized, standardized, and easy-to-use repository for damage and loss models. This library provides the essential data—model parameters, descriptive metadata, and configuration files—that power natural hazard risk assessment simulations. -This `v2.0.0` release represents a significant evolution of the project, featuring an improved data schema, a host of new and updated models, and a documentation system. +This `v2.1.0` release continues the evolution of the project, building on the improved data schema, extensive model collection, and documentation system introduced in v2.0.0, with enhanced code quality and improved usability for Hazus assessments. **Key Features:** * **Standardized Data Schema:** A robust yet flexible schema for organizing models by hazard, asset type, and resolution, making it easy to use data and supporting new contributions. diff --git a/doc/source/release_notes/index.rst b/doc/source/release_notes/index.rst index 4c943767..e2cb75ce 100644 --- a/doc/source/release_notes/index.rst +++ b/doc/source/release_notes/index.rst @@ -7,11 +7,12 @@ Release Notes The following sections document the notable changes of each release. The sequence of all changes is available in the `commit logs `_. -Version 1 +Version 2 ----------- .. toctree:: :maxdepth: 2 + v2.1.0 v2.0.0 v1.0.0 diff --git a/doc/source/release_notes/v1.0.0.rst b/doc/source/release_notes/v1.0.0.rst index 355f1986..07195f3c 100644 --- a/doc/source/release_notes/v1.0.0.rst +++ b/doc/source/release_notes/v1.0.0.rst @@ -1,8 +1,8 @@ .. _changes_v1_0: -========================== +============================ Version 1.0.0 (May 19, 2023) -========================== +============================ Initial Release. diff --git a/doc/source/release_notes/v2.0.0.rst b/doc/source/release_notes/v2.0.0.rst index a157aa88..e5fcd5fc 100644 --- a/doc/source/release_notes/v2.0.0.rst +++ b/doc/source/release_notes/v2.0.0.rst @@ -1,8 +1,8 @@ .. _changes_v2_0: -============================= +=============================== Version 2.0.0 (August 15, 2025) -============================= +=============================== This release marks a major milestone for the Damage and Loss Model Library and the beginning of a more frequent and structured release schedule. After more than two years of continuous development, ``v2.0.0`` introduces a significantly improved data schema, a host of new models, and a documentation system for model discovery. diff --git a/doc/source/release_notes/v2.1.0.rst b/doc/source/release_notes/v2.1.0.rst new file mode 100644 index 00000000..66beb25b --- /dev/null +++ b/doc/source/release_notes/v2.1.0.rst @@ -0,0 +1,28 @@ +.. _changes_v2_1: + +================================== +Version 2.1.0 (September 11, 2025) +================================== + +This release focuses on improving code quality and enhancing usability of Hazus assessments. The changes maintain backward compatibility while providing users with more flexibility in input specification and ensuring the codebase adheres to modern Python best practices. + +Changed +------- + +- **Input Validation:** Relaxed validation constraints for seismic and flood assessments to improve usability: + + - Allow HeightClass attribute for seismic structural systems (W1, W2, S3, PC1, MH) that don't require it in Hazus methodology + - Remove PlanArea field from auto-populated seismic configuration as it's no longer needed + - Allow RES1 occupancy buildings to have more than 3 stories in flood assessments, aligning with FEMA technical manual interpretation + +- **Code Quality:** Comprehensive code formatting and linting improvements using Ruff across the entire codebase: + + - Applied consistent code formatting across 15 Python files + - Fixed docstring formatting and missing docstring issues + - Cleaned up import statements and unused code + - Standardized quote usage and line spacing + +Fixed +----- + +- Resolved spelling issues in comments \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 1eced2f6..d3a1ec62 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "dlml" -version = "2.0.0" +version = "2.1.0" description = "Damage and Loss Model Library" readme = "README.md" license = {text = "BSD-3-Clause"}