diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..efd28f9 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,80 @@ +name: Publish to PyPI + +# ┌───────────────────────────────────────────────────────────────────────────┐ +# │ RELEASE POLICY (see README, "Releases"). Publishing a GitHub Release │ +# │ uploads that version to PyPI. Write the release notes from the CHANGELOG │ +# │ entry, publish, and the upload follows. │ +# │ │ +# │ PyPI uploads are immutable, so an upload cannot be repeated for the same │ +# │ version. Only the `published` event is subscribed to, which fires once; │ +# │ editing a release afterward does not re-trigger it. If an upload fails │ +# │ after PyPI has accepted the files, the fix is a new version, not a re-run.│ +# └───────────────────────────────────────────────────────────────────────────┘ + +on: + release: + types: [published] + # Manual runs build + validate only; the publish job is gated on the release + # event, so a dispatch is a safe no-upload dry run (inspect the artifacts + # afterward). + workflow_dispatch: + +permissions: + contents: read + +jobs: + build: + name: Build + validate sdist and wheel + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 + with: + python-version: "3.12" + - name: Install build tooling + run: pip install build twine + - name: Build + run: python -m build + # Refuse to proceed unless the release tag matches the built version + # exactly (tag v1.4 <-> version 1.4, tag v1.4.1 <-> version 1.4.1). + # Catches a release cut against a forgotten version bump in + # pyproject.toml, which would otherwise upload the wrong version. + - name: Verify release tag matches the built version + if: github.event_name == 'release' + env: + TAG: ${{ github.event.release.tag_name }} + run: | + version=$(ls dist/*.whl | sed -E 's|.*/atc138-([^-]+)-py3.*|\1|') + echo "Built version: $version Release tag: $TAG" + if [ "$TAG" != "v$version" ]; then + echo "::error::Release tag '$TAG' != built version 'v$version'. Refusing to publish." + exit 1 + fi + - name: Check package metadata + README rendering + run: twine check dist/* + - name: Upload the distributions + uses: actions/upload-artifact@v4 + with: + name: dist + path: dist/ + + publish: + name: Publish to PyPI + needs: build + # Only a published release uploads; workflow_dispatch stops after `build`. + if: github.event_name == 'release' + runs-on: ubuntu-latest + # The `pypi` environment carries a required-reviewer protection rule, so an + # upload waits for a human approval in the Actions UI even on a correct tag. + environment: pypi + permissions: + contents: read + id-token: write # OIDC for PyPI Trusted Publishing -- no stored token. + steps: + - name: Download the distributions + uses: actions/download-artifact@v4 + with: + name: dist + path: dist/ + - name: Publish to PyPI (Trusted Publishing) + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.gitignore b/.gitignore index d601d32..660f66e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,13 @@ # Compiled python modules *.pyc +__pycache__/ + +# macOS metadata +.DS_Store + +# Build artifacts +/dist/ +/build/ # VSCode folders /.vscode @@ -30,7 +38,7 @@ outputs/ Comparison_scripts/ # All new files coming out of buildInputs -inputs/example_inputs/*/optional_inputs.json -inputs/example_inputs/*/optional_inputs.py -inputs/example_inputs/*/simulated_inputs.json -inputs/example_inputs/*/build_input.py +examples/*/optional_inputs.json +examples/*/optional_inputs.py +examples/*/simulated_inputs.json +examples/*/build_input.py diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..ac4a11f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,109 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). + +The version number tracks the ATC-138 methodology this codebase implements. It is not an API contract, and it continues the lineage that began in the MATLAB [PBEE-Recovery](https://github.com/OpenPBEE/PBEE-Recovery) repository. Released versions carry two segments, `1.3` and `1.4`, with a third added for a patch release such as `1.4.1`. + +A change to the methodology means a change to the assessment logic, the fragility library or the damage-state attributes. Any of those makes the next release a minor one, and every such change is listed in that release's Methodology section, because they change simulated recovery times. A patch release carries corrections, documentation, packaging and tooling, and no methodology change. A correction can still change results, and where it does, the Fixed entry states which models are affected. + +## [Unreleased] + +--- + +## [1.4] - 2026-09-10 + +This release brings the component library up to the current ATC-138 fragility set, raises the safety class assigned to the third damage state of gravity shear tabs, and corrects defects in the red-tag and fire-suppression logic. The applied methodology changes alter simulated recovery times; the Methodology entries below state the scope of each. The packaging configuration is committed, so the published package can be rebuilt from the repository, and releases now go to PyPI automatically. + +### Methodology + +- **Gravity shear tab damage state 3 raised to Safety Class 3.** The third damage state of the bolted shear tab gravity connections, matched by the pattern `B1031\.00\w+` and covering `B1031.001` in the packaged component table, now maps to safety class 3 instead of safety class 2 in `damage_state_attribute_mapping.csv`. + + *Why:* the damage state is described as "complete separation of shear tab, close to complete loss of vertical load resistance", which matches the safety class 3 definition of severe damage that compromises both the lateral and the vertical load carrying capacity of the component. The proposal for the change cites the experimental work behind the FEMA P-58 fragility, which consistently reports complete fracture of the tab at the median drift assigned to this damage state, which implies a significant loss of vertical load carrying capacity (Deierlein and Victorsson 2008, FEMA P-58/BD-3.8.3; Liu and Astaneh-Asl 2000, *Journal of Structural Engineering* 126(1), 32-39). The same mapping was applied during development of the 2026 NEHRP Seismic Provisions Appendix A, where the component was used in its updated state to prequalify several lateral force-resisting systems (Pham et al. 2026, Functional Recovery Design Parameters for Five Additional Seismic Force-Resisting Systems in the 2026 NEHRP Recommended Provisions, *Earthquake Spectra*, in review). + + *Scope:* safety class filters are cumulative, since `red_tag.py` selects every component at or above the class being evaluated, so this damage state now counts toward the safety class 3 tally as well as the classes below it. The share of the shear tab population that has to be damaged before the system trips a red tag therefore falls from 25% to 10%, and the inspection trigger, set at half the red tag thresholds, falls from 12.5% to 5%. The change author's assessment is that the upgrade occurs at a drift demand that would generally already trigger red-tagging through other global consequences such as residual drift, so a large shift in outcomes is not expected. + +- **Component library updated to the current ATC-138 fragility set.** 326 entries were retired from `component_attributes.csv` and 42 were added, with 16 new rows in `damage_state_attribute_mapping.csv` covering the added components. + + *Why:* The retired entries are product-specific component attributes for proprietary systems that are not included in the official ATC-138 methodology. The added component and damage state attributes reflect new functional recovery damage state attributes added by the ATC-138 project to extend the fault trees to better cover generic steel buckling-restrained braces and load bearing vs non-load bearing RC shear walls. + + *Scope:* a model that references a retired fragility id now prints `Warning: skipping components with missing component attributes: ` and leaves those components out of the assessment. The run completes and does not fail, so recovery times will be shorter than they were at v1.3. Check the run log if you have v1.3 results to compare. Anyone who holds the supporting data for a retired component can restore it by placing an edited `component_attributes.csv` in the model directory, which overrides the packaged table; see "Customizing Static Tables" in the README. **NOTE - This project only performs the functional recovery assessment on damage simulated by an external engine. Therefore, no fragility information (e.g., damage state fragility medians) is represented in the `component_attributes.csv` and `damage_state_attribute_mapping.csv`. These data sets only contains the component-specific functional consequence and repair information needed to carry out the functional performance and repair scheduling performed as part of the ATC-138 methodology.** + +### Added + +- **Damage consequences in the recovery output.** The recovery output file now carries the damage consequences alongside the functionality results, as additional top-level keys: `red_tag`, `red_tag_impact`, `inspection_trigger`, `repair_cost_ratio_total`, `repair_cost_ratio_engineering`, `simulated_replacement_time`, and the door-racking counts. At v1.3 these were computed and then discarded. Existing keys are unchanged, so a consumer that reads the file today keeps working. +- **Custom output file name.** `run_analysis()` takes an `output_file` argument and the CLI exposes it as `--output_file`. The default remains `recovery_outputs.json`. +- **Duplicate-input validation on the Pelicun path.** `convert_pelicun()` now stops with an explicit message when the damage or loss tables contain duplicate location-direction-damage-state rows distinguished by a `uid` suffix. Previously the duplicates were carried through silently. +- **Two more reference models in the test suite.** The suite grew from two archetypes to four, adding a one-story reinforced concrete shear wall model and a twelve-story reinforced concrete moment frame, each with a 20-run reference set. The test itself runs each model once with a fixed seed and compares it against that reference; the pass criteria are in `tests/compare_runs.py`. +- **Automated PyPI publishing.** Publishing a GitHub Release builds the wheel and sdist, verifies that the release tag and the version in `pyproject.toml` agree, checks the package metadata, and uploads to PyPI through Trusted Publishing after a maintainer approves the deployment. No API token is stored in the repository. The README's "Releases" section describes the flow. +- **An MIT `LICENSE` file and a `CITATION.cff`.** The citation file declares `cff-version` 1.2.0 and `type: software`, and both DOI fields carry a bare DOI as the schema requires. The software DOI is now the Zenodo concept DOI, `10.5281/zenodo.20045327`, which resolves to the newest release. A citation generated from this file therefore points at the current release; the v1.3 file named that one version specifically. + +### Changed + +- **Output serialization.** Recovery outputs are converted from NumPy types with a recursive routine that handles arbitrary nesting depth and descends into lists and tuples, replacing hand-written loops that stopped at five dictionary levels and descended only into dictionaries. All four reference models still match their references. The new routine also converts NumPy scalar types that the old loops passed through untouched. +- **Reference comparison for the two new models.** The one-story shear wall and twelve-story moment frame models are compared against seeded Python runs; their MATLAB reference output was removed. The two original archetypes keep their MATLAB comparison, so parity with the MATLAB implementation is still covered by the suite, on those two models. +- **High-level comparison tolerance.** The high-level metric now passes when the relative difference is within 4% or the absolute difference is within 2 days; before, only the relative check applied. `tests/detailed_inspection_guide.md` was renamed to `tests/README.md`. +- **README.** Import examples updated for installed-package usage, a stale testing link corrected, and new pointers to the changelog and to the release process. +- **Packaging.** The build backend is now hatchling, and the wheel and sdist exclude cache and Finder metadata files. License metadata is declared as the SPDX expression `MIT` together with `license-files`, matching the `LICENSE` file the repository carries; the published 1.3.0 package declared no license. The project URLs now properly point at the OpenPBEE repository. +- **Version numbering.** The version in `pyproject.toml` and `CITATION.cff` now uses two segments, `1.4`, so it matches the `v1.4` tag exactly. + +### Fixed + +- **Red-tag evaluation failed on components with an alternate structural system.** A component's primary and alternate structural system identifiers were added together instead of being collected into one set, so a component assigned to structural system 5 with 6 as its alternate contributed a phantom system 11 to the list of systems to evaluate. No component matches system 11, so the tool raised `ValueError: zero-size array to reduction operation fmax which has no identity`. The next entry's empty-series guard is the other half of this defect. + + *Scope:* models containing any of the six components that carry a non-zero alternate structural system, namely the steel column base plates (`B1031.011a-c`) and the welded column splices (`B1031.021a-c`), each assigned to structural system 5 with 6 as the alternate, and with red-tag calculation enabled, which is the default. Such a model produced no results at all at v1.3, so there are no wrong numbers to re-check, only runs that could not complete. Components with no alternate system were unaffected. + +- **Non-numeric replacement times could fail the red-tag and recovery-metric calculations.** `simulated_replacement_time` is now coerced to a numeric type before it reaches the red-tag logic, the recovery-metric extraction and the repair-schedule helpers. The red-tag path also gained guards for a structural series that matched no components and for division by a zero component count; that case is now treated as not tagged. + + *Scope:* runs whose replacement time reached these functions as a non-numeric value, and red-tag runs with an empty structural series. Both previously raised or produced `NaN`. + +- **Fire suppression recovery read the wrong quantity.** The building-wide fire suppression branch read a dictionary key that does not exist, and a second call passed an array where NumPy expected an axis, which raises a `TypeError`. Neither path produced a wrong number; both stopped the run. + + *Scope:* runs with the fire watch option disabled and fire sprinkler components present. The fire watch option is enabled by default, so a default run never reached this code. + +- **Component list built from named columns instead of positions.** The input builder identified the metadata columns of `comp_population.csv` by position, so a file whose `story` and `dir` columns sat elsewhere produced a corrupted component list. The component list is now built by matching those names. The column-filtering step further down still assumes the metadata columns come first, so such a file is not yet handled end to end. +- **`np.row_stack` replaced with `np.vstack`.** The two are aliases, so results are unchanged; the former is deprecated in current NumPy. +- **Recovery trajectory plots.** Line styles and draw order were adjusted so overlapping trajectories stay legible. + +### Removed + +- **`requirements.txt`.** Every dependency it listed is declared in `pyproject.toml`, which has been the single source of truth since v1.3. Install with `pip install -e .` or `pip install atc138`. + +--- + +## [1.3] - 2026-03-31 + +The first release of the Python codebase. It converts the MATLAB [PBEE-Recovery v1.2](https://github.com/OpenPBEE/PBEE-Recovery/releases/tag/v1.2) implementation into an installable Python package and reproduces its results. Development continues on the Python codebase alone; the MATLAB codebase is deprecated. + +### Methodology + +- **Aligned with the ATC-138-6 / FEMA P-58-8 report.** The assessment method implemented here is the one documented in the ATC-138-6/FEMA P-58-8 report. The methodology changes relative to the previous MATLAB release, v1.2, were made in the MATLAB codebase and are inherited here: temporary repair times were developed, the impedance factor models were updated, component safety class mapping was updated, and existing fragility models were revised. The [PBEE-Recovery v1.2 release notes](https://github.com/OpenPBEE/PBEE-Recovery/releases/tag/v1.2) document them, and the full fragility model details are released with the report. + +- **No methodology change originates in the Python port.** The port was written to reproduce MATLAB PBEE-Recovery v1.2: establish a Python implementation that matches the MATLAB one, and defer method changes to later versions. Outputs from the two codebases were compared, and the differences in calculated recovery times fall within the expected range of variation from probabilistic sampling, so for practical purposes the two produce the same results. + +### Added + +- **Installable Python package.** The codebase is installable with `pip install -e .` and importable as `atc138`, with the static data tables bundled inside the package. +- **Command-line interface.** `atc138 `, also reachable as `python -m atc138.cli`, replaces the practice of editing a variable at the top of a driver script. It takes explicit input and output directories, a `--seed` for reproducible runs, and a `--force_rebuild` flag that regenerates `simulated_inputs.json` even when one is present. +- **Built-in input builder.** Input generation moved into the package as `atc138.input_builder` and runs automatically when `simulated_inputs.json` is missing, so the earlier step of copying a script out of the repository and hand-editing it is no longer necessary. A user's `optional_inputs.json` is deep-merged over the packaged `default_inputs.json`, so a partial override no longer has to restate every default. +- **Pelicun integration.** `convert_pelicun()` builds the native input set from Pelicun output (`DMG_sample.csv`, `DV_repair_sample.csv`, `CMP_QNT.csv`, `DL_summary.csv`, `general_inputs.json`). Two conversions in it are worth knowing about, because both are easy to get wrong by hand. Damage quantities are converted from Pelicun's physical units into the P-58 component unit counts the engine expects, using `unit_qty` from `component_attributes.csv`, with metric-to-imperial conversion where needed. Pelicun does not track which side of a building a cladding component sits on, so side assignment is drawn at random, optionally weighted by a `side_damage_ratio.csv`. +- **Overridable static tables.** Placing a copy of any of the seven packaged data tables in a model directory overrides the packaged version for that run, which is how a project supplies its own component attributes, damage-state mapping, systems, subsystems, tenant function requirements, impeding factor medians or temporary repair classes. +- **Integration test harness and continuous integration.** A pytest suite compares full model runs against MATLAB reference output. It reports high-level, area-under-curve and pointwise metrics, and gates the test on the high-level ones; the other two are printed for inspection behind flags that are off by default. GitHub Actions runs the suite across Python 3.9 through 3.14 on Linux, macOS and Windows. + +### Changed + +- **Source layout and module names.** The code moved into a `src/atc138/` package, and the MATLAB-derived file names were mapped to new ones: `main_PBEE_recovery.py` is now `engine.py`, `driver_PBEE_recovery.py` is `driver.py`, `fn_red_tag.py` is `red_tag.py`, and `static_tables/` is `atc138/data/`. Code that imported the old module paths needs updating. +- **Example models** moved from `inputs/example_inputs//` to `examples//`. + +### Removed + +- **The `inputs/` directory and the copy-and-edit input workflow**, including `inputs/Inputs2Copy/build_input.py`, superseded by the built-in input builder. +- **`plotModel_PBEErecovery.py`**, whose plotting logic moved into `plotters/main_plot_functionality.py` as `plot_results()`. +- **`demo.ipynb`**, which described the pre-package layout and is superseded by the README. + +--- + +## Earlier releases + +Versions 1.0.0 through 1.2 were released from the MATLAB [PBEE-Recovery](https://github.com/OpenPBEE/PBEE-Recovery) repository, and its [release notes](https://github.com/OpenPBEE/PBEE-Recovery/releases) document them. Version 1.3 is the first release of this Python codebase and continues that numbering. diff --git a/CITATION.cff b/CITATION.cff index 250bdf9..efd00ec 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -1,4 +1,5 @@ -cff-version: 1.0.0 +cff-version: 1.2.0 +type: software message: "If you use this software, please cite it as below." authors: - family-names: "Zaidi" @@ -21,9 +22,9 @@ authors: 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 +version: 1.4 +doi: 10.5281/zenodo.20045327 +date-released: 2026-09-10 url: "https://github.com/OpenPBEE/Functional-Recovery-Python" preferred-citation: type: article @@ -39,7 +40,7 @@ preferred-citation: - family-names: "Koliou" given-names: "Maria" orcid: "https://orcid.org/0000-0002-0686-493X" - doi: https://doi.org/10.1177/87552930221081538 + doi: 10.1177/87552930221081538 journal: "Earthquake Spectra" start: 1972 # First page number end: 2007 # Last page number diff --git a/README.md b/README.md index aaaaa8a..f96b779 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # Functional-Recovery---Python This is translation of Matlab codebase into Python for quantifying building-specific functional recovery and reoccupancy based on a probabilistic performance-based earthquake engineering framework. +Changes to the tool and to the assessment methodology are documented in [CHANGELOG.md](CHANGELOG.md). + ## Requirements - **Python Version**: 3.9 or later (recommend 3.9) @@ -259,3 +261,9 @@ python tests/compare_runs.py ### Validation Testing Against MATLAB For detailed guidance on validating the Python implementation against the original MATLAB framework, see [tests/README.md](tests/README.md). + +## Releases + +Releases are published to PyPI as [`atc138`](https://pypi.org/project/atc138/) and tagged in this repository, with a matching entry in [CHANGELOG.md](CHANGELOG.md). Version numbers carry no trailing zeros, so `1.3` and `1.4`, with a third segment only for a patch release such as `1.4.1`. They track the ATC-138 methodology, not an API contract; the top of the changelog explains what that means for a minor release. + +Publishing a GitHub Release starts the publish workflow. It builds the wheel and sdist, refuses to continue if the release tag and the version in `pyproject.toml` disagree, checks the package metadata, and uploads to PyPI through Trusted Publishing once a maintainer approves the deployment. Write the release notes from the matching changelog entry before publishing. diff --git a/pyproject.toml b/pyproject.toml index e218c1f..db74e45 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,20 +1,20 @@ [build-system] -requires = ["setuptools>=61.0", "wheel"] -build-backend = "setuptools.build_meta" +requires = ["hatchling>=1.27"] +build-backend = "hatchling.build" [project] name = "atc138" -version = "1.3.0" +version = "1.4" description = "Functional Recovery Assessment (ATC-138)" readme = "README.md" requires-python = ">=3.9" -license = {file = "LICENSE"} +license = "MIT" +license-files = ["LICENSE"] authors = [ {name = "Dustin Cook", email = "dustin.cook@nist.gov"}, ] classifiers = [ "Programming Language :: Python :: 3", - "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", ] dependencies = [ @@ -32,14 +32,31 @@ atc138 = "atc138.cli:main" test = ["pytest>=7.0"] [project.urls] -"Homepage" = "https://github.com/NHERI-SimCenter/Functional-Recovery-Python" -"Bug Tracker" = "https://github.com/NHERI-SimCenter/Functional-Recovery-Python/issues" +"Homepage" = "https://github.com/OpenPBEE/Functional-Recovery-Python" +"Bug Tracker" = "https://github.com/OpenPBEE/Functional-Recovery-Python/issues" +"Changelog" = "https://github.com/OpenPBEE/Functional-Recovery-Python/blob/main/CHANGELOG.md" -[tool.setuptools.packages.find] -where = ["src"] +[tool.hatch.build.targets.wheel] +packages = ["src/atc138"] +exclude = [ + "**/__pycache__", + "**/*.pyc", + "**/.DS_Store", +] -[tool.setuptools.package-data] -atc138 = ["data/*"] +[tool.hatch.build.targets.sdist] +include = [ + "/src/atc138", + "/README.md", + "/CHANGELOG.md", + "/CITATION.cff", + "/pyproject.toml", +] +exclude = [ + "**/__pycache__", + "**/*.pyc", + "**/.DS_Store", +] [tool.pytest.ini_options] pythonpath = ["tests"] diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index af3fc8b..0000000 --- a/requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -numpy -pandas -scipy -matplotlib -seaborn \ No newline at end of file