NSPA

This example demonstrates the full step-by-step pipeline — from portfolio generation to pushover analysis — using the BCIM, BDIM, and BNSM classes directly rather than through the high-level simdesign.rcmrf.generate() convenience function. It also shows an adaptive convergence strategy that retries the pushover with progressively finer displacement increments when the default increment fails.

import matplotlib.pyplot as plt
from pathlib import Path
from simdesign import rcmrf
from simdesign.utils.misc import make_dir

model = "CP03"
scheme = "EQL"
max_drift = 0.1
include_infills = True

design_classes = [
    "eu_cdn", "eu_cdl", "eu_cdm", "eu_cdh",
    "tr_7599", "tr_0018_dcm", "tr_0018_dch",
    "tr_post18_dcm", "tr_post18_dch",
]

outdir_main = Path("Outputs-NSPA-Batch")
make_dir(outdir_main)

for design_class in design_classes:
    outdir_class = outdir_main / design_class
    make_dir(outdir_class)

    # Generate the building portfolio
    bcim = rcmrf.BCIM()
    bcim.generate(
        sample_size=1,
        design_class=design_class,
        num_storeys=4,
        beta=0.1,
    )
    bcim.to_csv(outdir_class / "BCIM_initial.csv")

    for i, taxonomy in enumerate(bcim.taxonomy):
        print(f"Designing {design_class.upper()} building "
              f"{i + 1}/{len(bcim.taxonomy)}")
        outdir_building = outdir_class / f"Building_{i + 1}"

        # Simulated design
        bdim = rcmrf.BDIM(taxonomy)
        bdim.set_seed_for_quality_adjustments(bcim.inputs.seed)
        bdim.run_iterative_design_algorithm()

        if not bdim.ok:
            continue

        # Update BCIM attributes that may be adjusted during design
        bcim.column_section[i] = bdim.column_section
        bcim.beam_type[i]      = bdim.beam_type
        bcim.concrete_grade[i] = bdim.concrete_grade
        bcim.steel_grade[i]    = bdim.steel_grade
        bdim.to_csv(outdir_building / "BDIM-Data")

        # Modal analysis and model visualisation
        bnsm = rcmrf.BNSM(
            design=bdim, scheme=scheme, dincr=1e-3,
            max_drift=max_drift, model=model,
            include_infills=include_infills,
        )
        bnsm.do_modal(num_modes=6, out_dir=outdir_building / "Modal-Results")
        bnsm.plot_model(directory=outdir_building, show=False)
        bnsm.plot_mode_shape(
            mode_number=1, contour="x", show=False, directory=outdir_building
        )
        bnsm.plot_mode_shape(
            mode_number=2, contour="y", show=False, directory=outdir_building
        )

        # Pushover in X — retry with finer increments on convergence failure
        dincr_x = 1e-3
        dx, vx, ok_x = bnsm.do_nspa(
            ctrl_dof=1, out_dir=outdir_building / "NSPA-Results-X"
        )
        for dincr_try in [1e-4, 1e-5]:
            if ok_x != 0:
                dincr_x = dincr_try
                bnsm = rcmrf.BNSM(
                    design=bdim, scheme=scheme, dincr=dincr_x,
                    max_drift=max_drift, model=model,
                    include_infills=include_infills,
                )
                dx, vx, ok_x = bnsm.do_nspa(
                    ctrl_dof=1, out_dir=outdir_building / "NSPA-Results-X"
                )

        # Pushover in Y — same adaptive strategy
        dincr_y = 1e-3
        dy, vy, ok_y = bnsm.do_nspa(
            ctrl_dof=2, out_dir=outdir_building / "NSPA-Results-Y"
        )
        for dincr_try in [1e-4, 1e-5]:
            if ok_y != 0:
                dincr_y = dincr_try
                bnsm = rcmrf.BNSM(
                    design=bdim, scheme=scheme, dincr=dincr_y,
                    max_drift=max_drift, model=model,
                    include_infills=include_infills,
                )
                dy, vy, ok_y = bnsm.do_nspa(
                    ctrl_dof=2, out_dir=outdir_building / "NSPA-Results-Y"
                )

        # Rebuild BNSM with the finest increment that worked for both directions
        bnsm = rcmrf.BNSM(
            design=bdim, scheme=scheme,
            dincr=min(dincr_x, dincr_y),
            max_drift=max_drift, model=model,
            include_infills=include_infills,
        )
        bnsm.to_py(outdir_building / "OpsPy-Model")
        bnsm.to_tcl(outdir_building / "OpsTcl-Model")

        # Plot pushover curves
        plt.plot(dx, vx, label="X-dir")
        plt.plot(dy, vy, label="Y-dir")
        plt.xlabel("Control Node Displacement [m]")
        plt.ylabel("Base Shear [kN]")
        plt.legend()
        plt.savefig(outdir_building / "nspa.png", dpi=300)
        plt.close()

    bcim.to_csv(outdir_class / "BCIM_final.csv")

Key points

Adaptive convergence

do_nspa() returns a status flag as its third return value. A non-zero value indicates that the analysis did not converge. The example retries with successively finer increments (1e-31e-41e-5) before proceeding. A new BNSM instance must be constructed for each retry because OpenSees state is not reset between runs.

BCIM update after design

Attributes such as column_section and concrete_grade may be adjusted by the design algorithm. Writing them back to the BCIM object ensures that the final CSV reflects the as-designed properties rather than the initially sampled values.

Final model export

The OpenSees model is exported only once, using the smallest displacement increment that successfully converged in both directions.