Stability Methods#

../../../../../_images/steel_design_stability_methods.png

This example demonstrates the two stability design methods of the Steel Design add-on side by side according to EN 1993, and how to switch between their sub-options through the ULS configuration.

  • Model two identical beam-columns under a compressive normal force and bending about both principal axes.

  • Assign effective lengths to the first member to put it on the equivalent member method acc. to 6.3.1 to 6.3.3.

  • Assign boundary conditions to the second member to put it on the 2D general method acc. to 6.3.4.

  • Perform the calculation and confirm that both members are designed for the same internal forces.

  • Determine the interaction factors kyy, kyz, kzy, kzz acc. to Method 1 (Annex A) and acc. to Method 2 (Annex B), and read them from the design check details.

  • Enable the adapted method that extends the general method to double bending, without which the second member is rejected because of Mz.

  • Compare the design checks of both methods in a single design run.

Note

The calculation method is not a setting of the ULS configuration. It follows from the stability object assigned to the member:

Assigned object

Calculation method

SteelEffectiveLengths

Equivalent member method, EN 1993-1-1, 6.3.1 to 6.3.3

SteelBoundaryConditions

2D general method, EN 1993-1-1, 6.3.4 (4 degrees of freedom)

Assigning both objects to the same member is rejected by the add-on with the message More than one stability method assigned. This is why the model contains two identical members, one per method, instead of reassigning the stability object of a single member between design runs.

from dlubal.api import rfem, common
from math import inf
import pandas

# -------------------------------------------------------
# This example demonstrates the two stability design methods of the
# Steel Design add-on side by side, in accordance with EN 1993-1-1 and
# the French National Annex NF:2016-02.
#
# The calculation method is not a setting of the ULS configuration. It
# follows from the stability object assigned to the member:
#
#   SteelEffectiveLengths    -> equivalent member method, 6.3.1 to 6.3.3
#   SteelBoundaryConditions  -> 2D general method, 6.3.4 (4 DOF)
#
# Assigning both objects to the same member is rejected by the add-on
# with the message "More than one stability method assigned". The model
# therefore contains two identical members, one for each method, so that
# a single design run covers both of them.
#
# It includes:
#   - two identical beam-columns under an axial compressive force and
#     bending about both principal axes
#   - the interaction factors kyy, kyz, kzy and kzz of 6.3.3(4)
#     determined acc. to Method 1 (Annex A) and acc. to Method 2
#     (Annex B), and their effect on the design ratio
#   - the adapted method that extends the general method of 6.3.4 to
#     double bending, without which a member carrying M_z is rejected
#   - extraction of the interaction factors from the design check details
# -------------------------------------------------------

# The two members are geometrically and structurally identical. They differ
# only in the stability object assigned to them.
MEMBER_EQUIVALENT_MEMBER_METHOD = 1     # carries SteelEffectiveLengths
MEMBER_GENERAL_METHOD = 2               # carries SteelBoundaryConditions

# What to report for each member: the method its stability object selects, the
# design check types to look up, and the values to read from the design check
# details. A "ST" check type carries a design ratio, an "ER" type carries no
# ratio and states why a check could not be performed. Both outcomes of the
# general method are expected results of the two design runs below: without the
# adapted method the member is rejected because of M_z, with it it is designed.
MEMBERS = {
    MEMBER_EQUIVALENT_MEMBER_METHOD: {
        'method': "Equivalent member method acc. to EN 1993-1-1, 6.3.1 to 6.3.3",
        'check_types': (
            'ST3100.00',    # Bending and buckling about principal axes acc. to EN 1993-1-1, 6.3.3
        ),
        'detail_keys': ('k_yy', 'k_yz', 'k_zy', 'k_zz'),  # Interaction factors of 6.3.3(4)
    },
    MEMBER_GENERAL_METHOD: {
        'method': "2D general method acc. to EN 1993-1-1, 6.3.4",
        'check_types': (
            'ST4100.03',    # Compression and/or bending acc. to EN 1993-1-1, 6.3.4 | General method
            'ER3100.00',    # General method acc. to EN 1993-1-1, 6.3.4 is not applicable for bending about z-axis
        ),
        'detail_keys': (),  # The interaction factors of 6.3.3(4) do not apply here
    },
}

# The two design runs. Each one is a set of keys of the tree table
# "settings_ec3" of the Steel Design ULS configuration.
DESIGN_VARIANTS = [
    (
        "=== Method 1 acc. to Annex A | adapted method OFF ===",
        {
            # Interaction factors of 6.3.3(4) acc. to Method 1, Annex A
            'param_k_annex_a': True,
            # General method without the adapted method, so it covers in-plane
            # bending and lateral-torsional buckling only
            'extensional_methods': False,
            'european_lateral_torsional_buckling_curves': False,
            'adapted_method': False,
        },
    ),
    (
        "=== Method 2 acc. to Annex B | adapted method ON (double bending) ===",
        {
            # Interaction factors of 6.3.3(4) acc. to Method 2, Annex B
            'param_k_annex_b': True,
            # "Adapted method (enable double bending)" extends the general
            # method of 6.3.4 to biaxial bending. The checkbox is nested under
            # two parents, and a leaf under an inactive parent is stored but
            # has no effect, so the whole branch is written, not the leaf alone.
            'extensional_methods': True,
            'european_lateral_torsional_buckling_curves': True,
            'interpolation_acc_to_eq_666': False,
            'adapted_method': True,
        },
    ),
]


def define_structure_objects() -> list:

    # Both members are single-span beam-columns, 6.0 m long, IPE 300 in S235.
    # Each of them is held in position and against torsion at both ends and is
    # free to rotate about both principal axes, which is the fork support case
    # and the k = 1.0 reference for lateral-torsional buckling.
    #
    # Member 1 lies at y = 0.0 m, member 2 at y = 3.0 m. Their supports and
    # loads are identical, so any difference in the design check comes from
    # the stability method alone.

    return [

        # Materials
        rfem.structure_core.Material(
            no=1,
            name="S235",
        ),

        # Cross-Sections
        rfem.structure_core.CrossSection(
            no=1,
            name="IPE 300",
            material=1,
        ),

        # Nodes | Member 1
        rfem.structure_core.Node(
            no=1,
        ),
        rfem.structure_core.Node(
            no=2,
            coordinate_1=6.0,
        ),

        # Nodes | Member 2
        rfem.structure_core.Node(
            no=3,
            coordinate_2=3.0,
        ),
        rfem.structure_core.Node(
            no=4,
            coordinate_1=6.0,
            coordinate_2=3.0,
        ),

        # Lines
        rfem.structure_core.Line(
            no=1,
            definition_nodes=[1, 2],
        ),
        rfem.structure_core.Line(
            no=2,
            definition_nodes=[3, 4],
        ),

        # Members
        rfem.structure_core.Member(
            no=MEMBER_EQUIVALENT_MEMBER_METHOD,
            line=1,
            cross_section_start=1,
        ),
        rfem.structure_core.Member(
            no=MEMBER_GENERAL_METHOD,
            line=2,
            cross_section_start=1,
        ),

        # Nodal Supports
        rfem.types_for_nodes.NodalSupport(
            no=1,
            user_defined_name_enabled=True,
            name="Fork support | held in position",
            nodes=[1, 3],
            spring=common.Vector3d(x=inf, y=inf, z=inf),
            rotational_restraint=common.Vector3d(x=inf, y=0, z=0),
        ),
        rfem.types_for_nodes.NodalSupport(
            no=2,
            user_defined_name_enabled=True,
            name="Fork support | free along the member axis",
            nodes=[2, 4],
            spring=common.Vector3d(x=0, y=inf, z=inf),
            rotational_restraint=common.Vector3d(x=inf, y=0, z=0),
        ),
    ]


def define_loading_objects() -> list:

    # Both members carry the same three actions: a compressive normal force at
    # the free end, a uniform load about the major y-axis and a uniform load
    # about the minor z-axis.
    #
    # The normal force is what makes the interaction factors kyy, kyz, kzy and
    # kzz relevant at all, and the load about the minor axis is what makes the
    # member a case of double bending. Both are needed to tell the two
    # stability methods apart.

    return [

        # Static Analysis Settings
        rfem.loading.StaticAnalysisSettings(
            no=1,
            analysis_type=rfem.loading.StaticAnalysisSettings.ANALYSIS_TYPE_GEOMETRICALLY_LINEAR,
        ),

        # Load Cases
        rfem.loading.LoadCase(
            no=1,
            name="Permanent",
            action_category=rfem.loading.LoadCase.ACTION_CATEGORY_PERMANENT_G,
            static_analysis_settings=1,
            self_weight_active=False,
        ),
        rfem.loading.LoadCase(
            no=2,
            name="Wind",
            action_category=rfem.loading.LoadCase.ACTION_CATEGORY_WIND_QW,
            static_analysis_settings=1,
            self_weight_active=False,
        ),

        # Nodal Loads | LC1
        rfem.loads.NodalLoad(
            no=1,
            nodes=[2, 4],
            load_case=1,
            load_type=rfem.loads.NodalLoad.LOAD_TYPE_COMPONENTS,
            components_force_x=-80000,  # Compressive normal force (N)
        ),

        # Member Loads | LC1
        rfem.loads.MemberLoad(
            no=1,
            members=[MEMBER_EQUIVALENT_MEMBER_METHOD, MEMBER_GENERAL_METHOD],
            load_case=1,
            load_type=rfem.loads.MemberLoad.LOAD_TYPE_FORCE,
            load_distribution=rfem.loads.MemberLoad.LOAD_DISTRIBUTION_UNIFORM,
            load_direction=rfem.loads.MemberLoad.LOAD_DIRECTION_GLOBAL_Z_OR_USER_DEFINED_W_TRUE_LENGTH,
            magnitude=-2500,  # Bending about the major y-axis (N/m)
        ),

        # Member Loads | LC2
        rfem.loads.MemberLoad(
            no=2,
            members=[MEMBER_EQUIVALENT_MEMBER_METHOD, MEMBER_GENERAL_METHOD],
            load_case=2,
            load_type=rfem.loads.MemberLoad.LOAD_TYPE_FORCE,
            load_distribution=rfem.loads.MemberLoad.LOAD_DISTRIBUTION_UNIFORM,
            load_direction=rfem.loads.MemberLoad.LOAD_DIRECTION_GLOBAL_Y_OR_USER_DEFINED_V_TRUE_LENGTH,
            magnitude=1000,  # Bending about the minor z-axis (N/m)
        ),

        # Design Situations
        rfem.loading.DesignSituation(
            no=1,
            user_defined_name_enabled=True,
            name="ULS - Permanent and transient",
            design_situation_type=rfem.loading.DesignSituation.DESIGN_SITUATION_TYPE_STR_PERMANENT_AND_TRANSIENT_6_10,
            consider_inclusive_exclusive_load_cases=False,
        ),

        # Load Combinations
        rfem.loading.LoadCombination(
            no=1,
            design_situation=1,
            name="CO1",
            static_analysis_settings=1,
            to_solve=True,
            items=rfem.loading.LoadCombination.ItemsTable(
                rows=[
                    rfem.loading.LoadCombination.ItemsRow(
                        load_case=1,
                        factor=1.35,
                    ),
                    rfem.loading.LoadCombination.ItemsRow(
                        load_case=2,
                        factor=1.5,
                    ),
                ]
            ),
        ),
    ]


def define_steel_design_objects() -> list:

    # The configuration applies to both members. The stability object is what
    # differs: effective lengths select the equivalent member method, boundary
    # conditions select the 2D general method.

    return [
        # Steel Design Configurations
        rfem.steel_design_objects.SteelDesignUlsConfiguration(
            no=1,
            user_defined_name_enabled=True,
            name="ULS_configuration",
            assigned_to_all_members=True,
        ),

        # Steel Effective Lengths | Member 1
        rfem.steel_design.SteelEffectiveLengths(
            no=1,
            user_defined_name_enabled=True,
            name="Equivalent member method",
            members=[MEMBER_EQUIVALENT_MEMBER_METHOD],
            flexural_buckling_about_y=True,
            flexural_buckling_about_z=True,
            torsional_buckling=True,
            lateral_torsional_buckling=True,
            intermediate_nodes=False,
            different_properties=False,
            determination_mcr_europe=rfem.steel_design.SteelEffectiveLengths.DeterminationMcrEurope.DETERMINATION_MCR_EUROPE_EIGENVALUE,
        ),

        # Steel Boundary Conditions | Member 2
        # The default definition type is 2D, which gives four degrees of
        # freedom and fork supports at both member ends.
        rfem.steel_design.SteelBoundaryConditions(
            no=1,
            user_defined_name_enabled=True,
            name="2D general method",
            members=[MEMBER_GENERAL_METHOD],
            definition_type=rfem.steel_design.SteelBoundaryConditions.DEFINITION_TYPE_2D,
        ),
    ]


def get_member_checks(design_checks_df: pandas.DataFrame, member_no: int,
                      check_types: tuple) -> pandas.DataFrame:
    """
    This function returns the rows of the given check types for the given
    member. The results are aggregated per member, so there is at most one row
    per check type.
    """
    return design_checks_df.loc[
        (design_checks_df['member_no'] == member_no)
        & design_checks_df['design_check_type'].isin(check_types)
    ]


def get_design_check_value_by_key(design_check_details_df, key) -> str:
    """
    This function extracts the 'value', 'unit', and 'caption' from the design check details
    based on the specified 'key'.
    """
    row = design_check_details_df.loc[design_check_details_df['key'] == key]

    if row.empty:
        raise ValueError(f"Key '{key}' not found in design check details.")

    caption = row['caption'].values[0]
    key = row['key'].values[0]
    value = row['value'].values[0]
    unit = row['unit'].values[0]

    return f"{key} = {value} [{unit}]\t| {caption}"


# Connect to the RFEM application
with rfem.Application() as rfem_app:

    # Initialize model
    rfem_app.close_all_models(save_changes=False)
    rfem_app.create_model(name="steel_design_stability_methods")

    # Edit base data
    base_data = rfem_app.get_base_data()
    base_data.addons.steel_design_active = True
    base_data.standards.steel_design_standard = rfem.BaseData.Standards.STEEL_DESIGN_NATIONAL_ANNEX_AND_EDITION_EN_1993_NF_2016_02_STANDARD
    rfem_app.set_base_data(base_data=base_data)
    rfem_app.delete_all_objects()

    # Create model objects
    rfem_app.create_object_list(
        objs=
            define_structure_objects()+
            define_loading_objects()+
            define_steel_design_objects()
    )

    # Calculate the model
    rfem_app.calculate_all(skip_warnings=True)

    # Retrieve the internal forces both members are designed for. They are
    # identical, which is the point of the comparison.
    internal_forces_df = rfem_app.get_results(
        results_type=rfem.results.STATIC_ANALYSIS_MEMBERS_INTERNAL_FORCES,
        filters=[
            rfem.results.ResultsFilter(
                column_id="loading",
                filter_expression="CO1"
            )
        ],
    ).data

    print("\nDesign internal forces | CO1")
    print(f"  {'Member':<8}{'N [kN]':>12}{'M_y [kNm]':>12}{'M_z [kNm]':>12}")
    for member_no, member_forces_df in internal_forces_df.groupby('member_no'):
        print(
            f"  {member_no:<8}"
            f"{member_forces_df['n'].abs().max() / 1000:>12.2f}"
            f"{member_forces_df['m_y'].abs().max() / 1000:>12.2f}"
            f"{member_forces_df['m_z'].abs().max() / 1000:>12.2f}"
        )

    # Retrieve the design check ratios of both members, to show which columns
    # the design results carry. to_string() is used instead of printing the
    # DataFrame directly, because the default representation truncates itself
    # to the terminal width and fails on the metadata that the API attaches to
    # a results DataFrame.
    design_checks_df = rfem_app.get_results(
        results_type=rfem.results.STEEL_DESIGN_MEMBERS_DESIGN_RATIOS_BY_MEMBER,
    ).data
    print(f"\nDesign Check Ratios:\n{design_checks_df.to_string()}")

    # Design both members once per variant of the stability sub-options
    for label, settings in DESIGN_VARIANTS:

        # Retrieve the design configuration and write the settings of the
        # variant into its tree table
        steel_uls_config = rfem_app.get_object(
            obj=rfem.steel_design_objects.SteelDesignUlsConfiguration(no=1)
        )
        settings_ec3_uls_tree = steel_uls_config.settings_ec3

        for key, value in settings.items():
            common.tree_table.set_values_by_key(
                tree=settings_ec3_uls_tree,
                key=key,
                values=[value]
            )

        # Apply the updated configuration to the model
        rfem_app.update_object(
            obj=rfem.steel_design_objects.SteelDesignUlsConfiguration(
                no=1,
                settings_ec3=settings_ec3_uls_tree
            )
        )

        # Calculate the model and retrieve the design check ratios
        rfem_app.calculate_all(skip_warnings=True)
        design_checks_df = rfem_app.get_results(
            results_type=rfem.results.STEEL_DESIGN_MEMBERS_DESIGN_RATIOS_BY_MEMBER,
        ).data

        print(f"\n{label}")

        for member_no, member in MEMBERS.items():

            print(f"\n  Member {member_no} | {member['method']}")

            for _, check in get_member_checks(design_checks_df, member_no, member['check_types']).iterrows():

                print(f"    {check['design_check_type']} | {check['design_check_description']}")

                # An "ER" row states why no check was performed and carries no
                # design ratio and no details
                if pandas.isna(check['design_ratio']):
                    continue

                print(f"    Design ratio: {check['design_ratio']:.3f}")

                # Retrieve the design check details and read the values of
                # interest from them
                if member['detail_keys']:
                    design_check_details_df = rfem_app.get_results(
                        results_type=rfem.results.STEEL_DESIGN_DESIGN_CHECK_DETAILS,
                        filters=[
                            rfem.results.ResultsFilter(
                                column_id="design_check_details_id",
                                filter_expression=str(check['design_check_details_id'])
                            )
                        ],
                    ).data

                    for key in member['detail_keys']:
                        print(f"    {get_design_check_value_by_key(design_check_details_df, key=key)}")

    print(
        "\nBoth members carry the same internal forces, so the difference in the\n"
        "design ratios comes from the stability method alone:\n"
        "\n"
        f"  - Member {MEMBER_EQUIVALENT_MEMBER_METHOD} is verified with the interaction formulae 6.61 and 6.62.\n"
        "    Method 1 and Method 2 are two ways of determining the interaction\n"
        "    factors of the same formulae, so the ratio changes while the design\n"
        "    check stays the same.\n"
        "\n"
        f"  - Member {MEMBER_GENERAL_METHOD} is verified with the general method of 6.3.4, which starts\n"
        "    from the elastic critical moment of the member instead of from\n"
        "    tabulated interaction factors. It is a different mechanical model,\n"
        "    so its ratio is not expected to match the one of the equivalent\n"
        "    member method, and it needs the adapted method to accept M_z at all."
    )