@@ -503,6 +503,36 @@ def save_json(data, path):
503503 with open (path , "w" ) as f :
504504 json .dump (data , f , indent = 2 )
505505
506+ # SI conversion factors for units relevant to FEMA P-58 components.
507+ # To convert from unit_a to unit_b: value_b = value_a * (factor_a / factor_b)
508+ _UNIT_TO_SI = {
509+ # Area (to m²)
510+ 'sf' : 0.09290304 , 'SF' : 0.09290304 , 'ft2' : 0.09290304 , 'm2' : 1.0 ,
511+ # Length (to m)
512+ 'lf' : 0.3048 , 'LF' : 0.3048 , 'ft' : 0.3048 , 'm' : 1.0 ,
513+ # Count (unitless)
514+ 'ea' : 1.0 , 'EA' : 1.0 , 'each' : 1.0 ,
515+ }
516+
517+
518+ def _pelicun_to_p58_factor (pelicun_unit , atc138_unit , unit_qty ):
519+ """Compute factor to convert a Pelicun physical quantity to P-58 unit count.
520+
521+ value_in_p58_units = value_from_pelicun * factor
522+
523+ When both units belong to the same physical dimension (area, length, or
524+ count), the factor converts between unit systems and then divides by the
525+ P-58 block size (unit_qty). When the dimensions are incompatible (e.g.
526+ Pelicun reports EA but ATC-138 expects amp/ton/cfm), the Pelicun value
527+ is assumed to already represent a P-58 unit count and the factor is 1.0.
528+ """
529+ p_si = _UNIT_TO_SI .get (pelicun_unit )
530+ a_si = _UNIT_TO_SI .get (atc138_unit )
531+ if p_si is not None and a_si is not None :
532+ return (p_si / a_si ) / unit_qty
533+ return 1.0
534+
535+
506536def clean_frag_id (frag_id : str ) -> str :
507537 """
508538 Splits by ".",
@@ -565,6 +595,10 @@ def story_mask(location, num_stories):
565595 start , end = map (int , location .split ("--" ))
566596 return (stories >= start ) & (stories <= end )
567597
598+ if ',' in location :
599+ loc_vec = np .array (location .split (',' ), dtype = int )
600+ return np .isin (stories , loc_vec )
601+
568602 loc_vec = np .array (location .split (), dtype = int )
569603 return np .isin (stories , loc_vec )
570604
@@ -620,6 +654,8 @@ def convert_pelicun(model_dir):
620654 # ds attributes
621655 static_data_dir = os .path .join (os .path .dirname (__file__ ), 'data' )
622656 ds_attributes = load_custom_static_tables (model_dir , static_data_dir , 'damage_state_attribute_mapping.csv' )
657+ component_attributes = load_custom_static_tables (
658+ model_dir , static_data_dir , 'component_attributes.csv' )
623659
624660 # general inputs
625661 with open (os .path .join (model_dir , 'general_inputs.json' )) as file :
@@ -710,18 +746,39 @@ def convert_pelicun(model_dir):
710746 else :
711747 eng_cost_ratio = general_inputs ['engineering_cost_ratio' ]
712748
749+ # Convert Pelicun worker-days to calendar days for replacement time.
750+ # Use the same max-workers formula as the repair scheduler, but
751+ # cap the effective area at a limited number of parallel floors to consider
752+ # that construction proceeds upward — a 40-story building will not have all
753+ # floors staffed simultaneously.
754+ _MAX_PARALLEL_FLOORS = 6
755+ _default_rto = json .load (
756+ open (os .path .join (static_data_dir , 'default_inputs.json' ))
757+ )['repair_time_options' ]
758+ _wkr_per_sf = _default_rto ['max_workers_per_sqft_building' ]
759+ _wkr_min = _default_rto ['max_workers_building_min' ]
760+ _wkr_max = _default_rto ['max_workers_building_max' ]
761+
762+ _effective_floors = min (num_stories , _MAX_PARALLEL_FLOORS )
763+ _effective_area = _effective_floors * plan_area
764+ _max_workers = min (
765+ max (int (_effective_area * _wkr_per_sf + 10 ), _wkr_min ),
766+ _wkr_max ,
767+ )
768+ _replacement_time_days = np .where (
769+ replacement_mask ,
770+ DV_summary ["repair_time-parallel" ] / _max_workers ,
771+ np .nan ,
772+ )
773+
713774 damage_consequences = dict (
714775 repair_cost_ratio_total = (
715776 DV_summary ["repair_cost-" ] / total_cost
716777 ).tolist (),
717778 repair_cost_ratio_engineering = (
718779 DV_summary ["repair_cost-" ] / total_cost * eng_cost_ratio
719780 ).tolist (),
720- simulated_replacement_time = np .where (
721- replacement_mask ,
722- DV_summary ["repair_time-parallel" ],
723- np .nan ,
724- ).tolist (),
781+ simulated_replacement_time = _replacement_time_days .tolist (),
725782 )
726783
727784 save_json (
@@ -809,6 +866,22 @@ def convert_pelicun(model_dir):
809866 comps = comps [comps ['tmp_clean_id' ].isin (list (comp_ds_list ['comp_id' ]))]
810867 comps = comps .drop ('tmp_clean_id' , axis = 1 )
811868
869+ # Build per-component conversion factor: Pelicun physical qty -> P-58 units
870+ comp_conversion = {}
871+ for _ , _comp in comps .iterrows ():
872+ _fid = clean_frag_id (_comp ["ID" ])
873+ _pelicun_unit = str (_comp ["Units" ]).strip ()
874+ _attr_match = component_attributes [
875+ component_attributes ['fragility_id' ] == _fid
876+ ]
877+ if _attr_match .empty :
878+ comp_conversion [_fid ] = 1.0
879+ continue
880+ _attr = _attr_match .iloc [0 ]
881+ comp_conversion [_fid ] = _pelicun_to_p58_factor (
882+ _pelicun_unit , str (_attr ['unit' ]).strip (), float (_attr ['unit_qty' ])
883+ )
884+
812885 for _ , comp in comps .iterrows ():
813886
814887 # clean periods from ID, then replace with underscore to match convention
@@ -831,7 +904,8 @@ def convert_pelicun(model_dir):
831904 & np .isin (comp_population .index .get_level_values ("dir" ), dir_vec )
832905 )
833906
834- comp_population .loc [mask , frag_id ] += float (comp ["Theta_0" ])
907+ _cfid = clean_frag_id (comp ['ID' ])
908+ comp_population .loc [mask , frag_id ] += float (comp ["Theta_0" ]) * comp_conversion .get (_cfid , 1.0 )
835909
836910 comp_population .reset_index ().to_csv (
837911 os .path .join (model_dir , "comp_population.csv" ),
@@ -928,7 +1002,8 @@ def convert_pelicun(model_dir):
9281002 idx = lookup_index [key ]
9291003
9301004 dmg_data = np .array (damage [col ].fillna (0 ), dtype = float )
931- simulated_damage ["story" ][story ]["qnt_damaged" ][:, idx ] += dmg_data
1005+ qnt_factor = comp_conversion .get (frag_id , 1.0 )
1006+ simulated_damage ["story" ][story ]["qnt_damaged" ][:, idx ] += dmg_data * qnt_factor
9321007
9331008 if col in DV_time .columns :
9341009 repair_time_data = DV_time [col ].fillna (0 ).astype (float )
@@ -947,8 +1022,7 @@ def convert_pelicun(model_dir):
9471022 # Direction-specific
9481023 simulated_damage ["story" ][story ][
9491024 f"qnt_damaged_dir_{ dir_id } "
950- ][:, idx ] += dmg_data
951-
1025+ ][:, idx ] += dmg_data * qnt_factor
9521026
9531027 # add component quantity to simulated_damage
9541028 for _ , comp in comps .iterrows ():
@@ -964,7 +1038,7 @@ def convert_pelicun(model_dir):
9641038 if not is_story :
9651039 continue
9661040
967- simulated_damage ["story" ][s_idx ]["num_comps" ][frag_matches ] += theta
1041+ simulated_damage ["story" ][s_idx ]["num_comps" ][frag_matches ] += theta * comp_conversion . get ( frag_id , 1.0 )
9681042
9691043 ############# convert to json-serializable and save
9701044 # Convert numpy arrays to lists
0 commit comments