Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions CITATION.cff
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
cff-version: 1.0.0
message: "If you use this software, please cite it as below."
authors:
- family-names: "Zaidi"
given-names: "Faraz"
- family-names: "Pham"
given-names: "Huy G."
orcid: "https://orcid.org/0000-0002-8043-7784"
- family-names: "Wang"
given-names: "Ziyi"
- family-names: "Cook"
given-names: "Dustin T."
orcid: "https://orcid.org/0000-0002-4841-4515"
- family-names: "Zsarnoczay"
given-names: "Adam"
orcid: "https://orcid.org/0000-0001-6895-1417"
- family-names: "Molina Hutt"
given-names: "Carlos"
orcid: "https://orcid.org/0000-0003-2116-1201"
- family-names: "Hulsey"
given-names: "Anne M."
orcid: "https://orcid.org/0000-0002-4265-055X"
title: "OpenPBEE/Functional-Recovery-Python"
version: 1.3.0
doi: https://doi.org/10.5281/zenodo.20045328
date-released: 2026-03-31
url: "https://github.com/OpenPBEE/Functional-Recovery-Python"
preferred-citation:
type: article
authors:
- family-names: "Cook"
given-names: "Dustin T."
orcid: "https://orcid.org/0000-0002-4841-4515"
- family-names: "Liel"
given-names: "Abbie B."
orcid: "https://orcid.org/0000-0002-9241-5144"
- family-names: "Haselton"
given-names: "Curt B."
- family-names: "Koliou"
given-names: "Maria"
orcid: "https://orcid.org/0000-0002-0686-493X"
doi: https://doi.org/10.1177/87552930221081538
journal: "Earthquake Spectra"
start: 1972 # First page number
end: 2007 # Last page number
title: "A framework for operationalizing the assessment of post-earthquake functional recovery of buildings"
issue: 3
volume: 38
year: 2022
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
SOFTWARE.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -258,4 +258,4 @@ python tests/compare_runs.py <reference_dir> <python_output_dir>

### Validation Testing Against MATLAB

For detailed guidance on validating the Python implementation against the original MATLAB framework, see [tests/detailed_inspection_guide.md](tests/detailed_inspection_guide.md).
For detailed guidance on validating the Python implementation against the original MATLAB framework, see [tests/README.md](tests/README.md).
12 changes: 6 additions & 6 deletions plotters/other_plot_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,9 +249,9 @@ def plt_recovery_trajectory(recovery, full_repair_time, plot_dir):
# Plot Recovery Trajectory
plt.rcParams["font.family"] = "Times New Roman"
plt.figure(figsize=(6,4))
plt.plot(reoc, level_of_repair,'r-', linewidth = 1.5, label = 'Re-Occupancy')
plt.plot(func, level_of_repair,'b-', linewidth = 1.5, label= 'Functional')
plt.plot([full, full], [0, 1],'k-', linewidth = 1.5, label= 'Fully Repaired')
plt.plot(reoc, level_of_repair,'r:', linewidth = 1.5, label = 'Re-Occupancy', zorder=2)
plt.plot(func, level_of_repair,'b--', linewidth = 1.5, label= 'Functional',zorder=1)
plt.plot([full, full], [0, 1],'k-', linewidth = 1.5, label= 'Fully Repaired', zorder=0)
plt.xlim([0,np.ceil((full+1)/10)*10])
plt.xlabel('Days After Earthquake')
plt.ylabel('Fraction of Floor Area')
Expand Down Expand Up @@ -414,9 +414,9 @@ def plt_gantt_chart(p_idx, recovery, full_repair_time, workers, schedule, impede
G3.grid(which = 'major', axis = 'x', alpha=0.5)

# Plot Recovery Trajectory
G4.plot(recovery_trajectory['reoc'], recovery_trajectory['level_of_repair'],'r-', linewidth = 1.5, label = 'Re-Occupancy')
G4.plot(recovery_trajectory['func'], recovery_trajectory['level_of_repair'],'b-', linewidth = 1.5, label = 'Functional')
G4.plot(recovery_trajectory['ful_rep'], recovery_trajectory['level_of_repair'],'k-', linewidth = 1.5, label = 'Fully Repaired')
G4.plot(recovery_trajectory['func'], recovery_trajectory['level_of_repair'],'b--', linewidth = 1.5, label = 'Functional',zorder=1)
G4.plot(recovery_trajectory['reoc'], recovery_trajectory['level_of_repair'],'r:', linewidth = 1.5, label = 'Re-Occupancy',zorder=2)
G4.plot(recovery_trajectory['ful_rep'], recovery_trajectory['level_of_repair'],'k-', linewidth = 1.5, label = 'Fully Repaired', zorder=0)
G4.set_ylim([0,1])
G4.set_xlabel('Days After Earthquake')
G4.set_ylabel('Fraction of Floor Area')
Expand Down
65 changes: 35 additions & 30 deletions src/atc138/driver.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import numpy as np
def run_analysis(input_dir, output_dir,
output_file='recovery_outputs.json',
seed=None, force_rebuild=False):
Expand Down Expand Up @@ -163,36 +164,11 @@ def run_analysis(input_dir, output_dir,
os.makedirs(output_dir)

# Covert arrays to list for writing to json file
fnc_keys_1 = list(functionality.keys())
for k_1 in fnc_keys_1:
if type(functionality[k_1]) == np.ndarray:
functionality[k_1] = functionality[k_1].tolist()
if type(functionality[k_1]) == dict:
fnc_keys_2 = list(functionality[k_1].keys())

for k_2 in fnc_keys_2:
if type(functionality[k_1][k_2]) == np.ndarray:
functionality[k_1][k_2] = functionality[k_1][k_2].tolist()
if type(functionality[k_1][k_2]) == dict:
fnc_keys_3 = list(functionality[k_1][k_2].keys())

for k_3 in fnc_keys_3:
if type(functionality[k_1][k_2][k_3]) == np.ndarray:
functionality[k_1][k_2][k_3] = functionality[k_1][k_2][k_3].tolist()
if type(functionality[k_1][k_2][k_3]) == dict:
fnc_keys_4 = list(functionality[k_1][k_2][k_3].keys())

for k_4 in fnc_keys_4:
if type(functionality[k_1][k_2][k_3][k_4]) == np.ndarray:
functionality[k_1][k_2][k_3][k_4] = functionality[k_1][k_2][k_3][k_4].tolist()
if type(functionality[k_1][k_2][k_3][k_4]) == dict:
fnc_keys_5 = list(functionality[k_1][k_2][k_3][k_4].keys())

for k_5 in fnc_keys_5:
if type(functionality[k_1][k_2][k_3][k_4][k_5]) == np.ndarray:
functionality[k_1][k_2][k_3][k_4][k_5] = functionality[k_1][k_2][k_3][k_4][k_5].tolist()
if type(functionality[k_1][k_2][k_3][k_4][k_5]) == dict:
fnc_keys_6 = list(functionality[k_1][k_2][k_3][k_4][k_5].keys())
functionality = recursive_convert_array(functionality)

# join consequences
consequences = recursive_convert_array(damage_consequences)
functionality = functionality | consequences

output_json_object = json.dumps(functionality)

Expand All @@ -205,3 +181,32 @@ def run_analysis(input_dir, output_dir,
print('Recovery assessment complete')
print('time to run '+str(round(end_time - start_time,2))+'s')

def recursive_convert_array(obj):
"""
Recursively convert numpy arrays to lists,
traversing nested dicts/lists/tuples.
"""

if isinstance(obj, np.ndarray):
return obj.tolist()

elif isinstance(obj, dict):
return {
key: recursive_convert_array(value)
for key, value in obj.items()
}

elif isinstance(obj, list):
return [recursive_convert_array(item) for item in obj]

elif isinstance(obj, tuple):
return tuple(recursive_convert_array(item) for item in obj)

elif isinstance(obj, np.integer):
return int(obj)

elif isinstance(obj, np.floating):
return float(obj)

else:
return obj
3 changes: 2 additions & 1 deletion src/atc138/input_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,8 @@ def build_simulated_inputs(model_dir):
comp_header = list(comp_population.columns)

# component IDs from comp_population
comp_list = np.array(comp_header[2:])
comp_meta = ['story', 'dir']
comp_list = [cmp_label for cmp_label in comp_header if cmp_label not in comp_meta]
comp_list = np.char.replace(comp_list, '_', '.')
comp_list = comp_list.tolist()

Expand Down
24 changes: 16 additions & 8 deletions tests/detailed_inspection_guide.md → tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,23 +56,31 @@ Threshold: ```≤ 4 %```

**Metric 3 — Worst system mean absolute error (MAE)**

System-level wise, the maximum MAE of the fraction of unresolved damage over time, across all functional-recovery and reoccupancy systems.
System-level wise, the highest MAE of the fraction of unresolved damage over time, across all functional-recovery and reoccupancy systems.

Threshold: ```≤ 0.02``` (maximum mean difference of 2% unresolved fraction)
Threshold: ```≤ 0.02``` (no more than 2% unresolved fraction across all systems)

</br>

**Metric 4 — Worst system 95th percentile absolute error**

System-level wise, the maximum 95th percentile aboslute error of the fraction of unresolved damage over time, across all functional-recovery and reoccupancy systems.
System-level wise, the maximum 95th percentile absolute error of the fraction of unresolved damage over time, across all functional-recovery and reoccupancy systems.

Threshold: ```≤ 0.04``` (maximum mean difference of 4% unresolved fraction)

Both absolute and relative differences are reported to provide a more complete assessment —— absolute difference reflects the magnitude of discrepancies, while relative difference normalizes them across models with different recovery scales. Using both avoids misleading interpretations —— for example, a small absolute difference may appear large in relative terms for short recovery durations, while a small relative difference may still correspond to a large absolute difference for long recovery durations.
Threshold: ```≤ 0.04``` (no more than of 4% unresolved fraction across all systems)

</br>

#### 5. Recovery Trajectory and System-Level Recovery Comparison
#### 5. Testing thresholds

Both absolute and relative differences are reported to provide a more complete assessment: absolute difference reflects the magnitude of discrepancies, while relative difference normalizes them across models with different recovery scales. Using both avoids misleading interpretations. For example, a small absolute difference may appear large in relative terms for short recovery durations, while a small relative difference may still correspond to a large absolute difference for long recovery durations.

In the porting process from MATLAB to Python, the output Python results were compared against the reference MATLAB values. The test case is ideally averaged across multiple random seeds, to minimize variance between each realization. The test case is considered to adequately match if:

1) At the high level, either Metric 1 _or_ Metric 2 pass their thresholds. That is, either an absolute difference in mean recovery time $\leq$ 2 days _or_ a relative difference in mean recovery time of $\leq$ 4% is considered passing.
2) At the system level, both Metric 3 _and_ Metric 4 pass their thresholds. That is, for all systems at each assessed point in time, the average difference in unresolved fraction is no more than 2%, and the 95th-percentile worst difference in unresolved fraction is no more than 4%.


#### 6. Recovery Trajectory and System-Level Recovery Comparison

For each model, plots of recovery trajectories and system-level recovery curves are saved:

Expand All @@ -81,7 +89,7 @@ For each model, plots of recovery trajectories and system-level recovery curves
- `system_breakdowns_reoccupancy.jpg`


#### 6. Validation Results
#### 7. Validation Results

The comparison results for the two example models are summarized below. Twenty batch runs were carried out for each model, and the mean values are compared against the defined metrics.

Expand Down
6 changes: 4 additions & 2 deletions tests/compare_runs.py
Original file line number Diff line number Diff line change
Expand Up @@ -359,11 +359,13 @@ def evaluate_tolerances(reference_dir, python_dir):

if ref_stats['mean'] != 0:
diff = 100.0 * abs(py_stats['mean'] - ref_stats['mean']) / ref_stats['mean']
abs_diff = abs(py_stats['mean'] - ref_stats['mean'])
else:
diff = float('nan')
abs_diff = float('nan')

# Tolerance: mean recovery days within 3% of reference
passed = diff <= 4.0 or (np.isnan(diff) and py_stats['mean'] == 0)
# Tolerance: mean recovery days within 4% of reference OR absolute difference within 2 days of reference
passed = diff <= 4.0 or abs_diff <= 2.0 or (np.isnan(diff) and py_stats['mean'] == 0)
if not passed: results["all_passed"] = False

results["high_level"][metric] = {
Expand Down

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Loading
Loading