Wind Simulation from Wizard#

../../../../../_images/wind_simulation_user_defined.png

Letting the ‘Wind Simulation’ load wizard generate the wind load cases from a range of wind directions:

  • Activate the wind simulation add-on and orient the global Z-axis upwards

  • Model a closed box building of four walls and a flat roof on a fixed base

  • Define a horizontal terrain plane as the floor of the CFD wind tunnel

  • Create the shrink wrapping, surface roughness, wind profile and analysis settings

  • Create the wind simulation load wizard over a range of wind directions

  • Calculate an unrelated load case, which makes the wizard generate the wind load cases

  • Read the generated load cases from the wizard and calculate them

  • Read the wind force resultants and the deformations per wind direction

Keywords:
wind simulation load wizard CFD wind direction generated load case wind tunnel
from math import inf, radians

from dlubal.api import rfem, common

# -------------------------------------------------------
# This example runs the wind simulation add-on on a simple closed box building,
# but the wind load cases are not written by hand - they are generated by the
# 'Wind Simulation' load wizard from a range of wind directions.
#
# The wizard generates during the preparation of a calculation. There is no
# separate generation call on the API, so the workflow has two calculation
# steps:
#
#   1. calculate an unrelated, already existing load case. The preparation
#      processes every active wizard in the model regardless of the selection,
#      so the wind load cases are created without running the CFD solver.
#   2. read the generated load cases from the wizard and calculate them.
#
# 'WindSimulation.generate_into_load_cases' is the read-only result of step 1 -
# one row per wind direction, holding the direction, the generated load case
# and the wind profile. Writing into it has no effect.
#
# Every wind direction is a separate CFD run. With the settings below one
# direction takes roughly 3-4 minutes on a regular workstation, so the whole
# script runs for several minutes.
#
# See 'wind_simulation_user_defined.py' for the same model with the wind load
# cases defined explicitly.
# -------------------------------------------------------

# Building dimensions [m]
LENGTH = 10.0     # along global X
WIDTH = 8.0       # along global Y
HEIGHT = 6.0      # along global Z
THICKNESS = 0.2   # wall and roof thickness

# Range of wind directions around the global Z-axis, clockwise [deg]
WIND_DIRECTION_START = 0.0
WIND_DIRECTION_END = 90.0
WIND_DIRECTION_STEP = 90.0


def define_structure() -> list:
    """Define a closed box building - four walls and a flat roof."""

    return [

        # Material
        rfem.structure_core.Material(
            no=1,
            name='C30/37 | EN 1992-1-1:2004/A1:2014',
        ),

        # Nodes - base
        rfem.structure_core.Node(no=1, coordinate_1=0.0, coordinate_2=0.0, coordinate_3=0.0),
        rfem.structure_core.Node(no=2, coordinate_1=LENGTH, coordinate_2=0.0, coordinate_3=0.0),
        rfem.structure_core.Node(no=3, coordinate_1=LENGTH, coordinate_2=WIDTH, coordinate_3=0.0),
        rfem.structure_core.Node(no=4, coordinate_1=0.0, coordinate_2=WIDTH, coordinate_3=0.0),

        # Nodes - top
        rfem.structure_core.Node(no=5, coordinate_1=0.0, coordinate_2=0.0, coordinate_3=HEIGHT),
        rfem.structure_core.Node(no=6, coordinate_1=LENGTH, coordinate_2=0.0, coordinate_3=HEIGHT),
        rfem.structure_core.Node(no=7, coordinate_1=LENGTH, coordinate_2=WIDTH, coordinate_3=HEIGHT),
        rfem.structure_core.Node(no=8, coordinate_1=0.0, coordinate_2=WIDTH, coordinate_3=HEIGHT),

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

        # Lines - vertical
        rfem.structure_core.Line(no=5, definition_nodes=[1, 5]),
        rfem.structure_core.Line(no=6, definition_nodes=[2, 6]),
        rfem.structure_core.Line(no=7, definition_nodes=[3, 7]),
        rfem.structure_core.Line(no=8, definition_nodes=[4, 8]),

        # Lines - top
        rfem.structure_core.Line(no=9, definition_nodes=[5, 6]),
        rfem.structure_core.Line(no=10, definition_nodes=[6, 7]),
        rfem.structure_core.Line(no=11, definition_nodes=[7, 8]),
        rfem.structure_core.Line(no=12, definition_nodes=[8, 5]),

        # Surfaces - four walls and the roof
        rfem.structure_core.Surface(no=1, boundary_lines=[1, 6, 9, 5]),
        rfem.structure_core.Surface(no=2, boundary_lines=[2, 7, 10, 6]),
        rfem.structure_core.Surface(no=3, boundary_lines=[3, 8, 11, 7]),
        rfem.structure_core.Surface(no=4, boundary_lines=[4, 5, 12, 8]),
        rfem.structure_core.Surface(no=5, boundary_lines=[9, 10, 11, 12]),

        # Thickness
        rfem.structure_core.Thickness(
            no=1,
            material=1,
            uniform_thickness=THICKNESS,
            assigned_to_surfaces=[1, 2, 3, 4, 5],
        ),

        # Fixed line support along the base
        rfem.types_for_lines.LineSupport(
            no=1,
            lines=[1, 2, 3, 4],
            spring=common.Vector3d(x=inf, y=inf, z=inf),
        ),

        # Static analysis settings of the load cases
        rfem.loading.StaticAnalysisSettings(
            no=1,
            analysis_type=rfem.loading.StaticAnalysisSettings.ANALYSIS_TYPE_GEOMETRICALLY_LINEAR,
        ),
    ]


def define_terrain() -> rfem.base_data_objects.Terrain:
    """Define a horizontal terrain plane - the floor of the CFD wind tunnel."""

    return rfem.base_data_objects.Terrain(
        no=1,
        type=rfem.base_data_objects.Terrain.TYPE_HORIZONTAL_PLANE,
        center_of_terrain_z=0.0,
        bounding_box_offset_x=30.0,
        bounding_box_offset_y=30.0,
    )


def define_wind_simulation() -> list:
    """Define the wind simulation objects.

    The CFD model is built from a shrink-wrapped envelope of the structure, so
    a shrink wrapping is required for the main structure and one for the
    surrounding objects. Both are referenced by the mesh settings further down.
    The wind profile describes the atmospheric boundary layer at the tunnel
    inlet, and the load wizard turns the range of wind directions into one wind
    simulation load case per direction.

    Load case no. 1 is not a wind case - it only gives the wizard something to
    calculate, because the generation runs in the preparation of a calculation.
    """

    return [

        # Shrink wrapping - main structure
        rfem.types_for_wind_simulation.ShrinkWrapping(
            no=1,
            simplification_defined_by=rfem.types_for_wind_simulation.ShrinkWrapping.SIMPLIFICATION_DEFINED_BY_LEVEL_OF_DETAILS,
            level_of_detail=2,
            small_openings_closure_type=rfem.types_for_wind_simulation.ShrinkWrapping.SMALL_OPENINGS_CLOSURE_TYPE_PERCENT_OF_MODEL_DIAMETER,
            closure_relative_to_model_parameter=0.05,
            deactivate_shrink_wrapping=False,
        ),

        # Shrink wrapping - surrounding objects
        rfem.types_for_wind_simulation.ShrinkWrapping(
            no=2,
            simplification_defined_by=rfem.types_for_wind_simulation.ShrinkWrapping.SIMPLIFICATION_DEFINED_BY_LEVEL_OF_DETAILS,
            level_of_detail=0,
            small_openings_closure_type=rfem.types_for_wind_simulation.ShrinkWrapping.SMALL_OPENINGS_CLOSURE_TYPE_PERCENT_OF_MODEL_DIAMETER,
            closure_relative_to_model_parameter=0.2,
            deactivate_shrink_wrapping=False,
        ),

        # Surface roughness of the model in the wind tunnel
        rfem.types_for_wind_simulation.RoughnessAndPermeability(
            no=1,
            type_of_surface=rfem.types_for_wind_simulation.RoughnessAndPermeability.TYPE_OF_SURFACE_SMOOTH,
        ),

        # Wind profile at the tunnel inlet
        rfem.load_wizards.WindProfile(
            no=1,
            type=rfem.load_wizards.WindProfile.TYPE_USER_DEFINED,
            user_defined_input_type=rfem.load_wizards.WindProfile.USER_DEFINED_INPUT_TYPE_CONSTANT,
            user_defined_inlet_variables=rfem.load_wizards.WindProfile.USER_DEFINED_INLET_VARIABLES_I_TUL,
            user_defined_wind_profile=rfem.load_wizards.WindProfile.UserDefinedWindProfileTable(
                rows=[
                    # height [m], velocity [m/s], turbulence intensity [-]
                    rfem.load_wizards.WindProfile.UserDefinedWindProfileRow(
                        no=1, height=0.0, velocity=15.0, turbulence_intensity=0.20),
                    rfem.load_wizards.WindProfile.UserDefinedWindProfileRow(
                        no=2, height=5.0, velocity=20.0, turbulence_intensity=0.18),
                    rfem.load_wizards.WindProfile.UserDefinedWindProfileRow(
                        no=3, height=10.0, velocity=23.0, turbulence_intensity=0.16),
                    rfem.load_wizards.WindProfile.UserDefinedWindProfileRow(
                        no=4, height=20.0, velocity=26.0, turbulence_intensity=0.14),
                    rfem.load_wizards.WindProfile.UserDefinedWindProfileRow(
                        no=5, height=40.0, velocity=29.0, turbulence_intensity=0.12),
                ]
            ),
        ),

        # Steady-flow CFD simulation with a reduced iteration count
        rfem.loading.WindSimulationAnalysisSettings(
            no=1,
            simulation_type=rfem.loading.WindSimulationAnalysisSettings.SIMULATION_TYPE_STEADY_FLOW,
            turbulence_model_type=rfem.loading.WindSimulationAnalysisSettings.TURBULENCE_MODEL_TYPE_EPSILON,
            finite_volume_mesh_density=0.2,
            minimum_number_of_iterations=60,
            maximum_number_of_iterations=300,
            residual_type=rfem.loading.WindSimulationAnalysisSettings.RESIDUAL_TYPE_PRESSURE,
            residual_pressure=0.01,
            consider_turbulence=True,
            snap_to_model_edges=True,
            use_potential_flow_solver_for_initial_condition=True,
            member_load_distribution=rfem.loading.WindSimulationAnalysisSettings.MEMBER_LOAD_DISTRIBUTION_CONCENTRATED,
        ),

        # Load case that triggers the generation of the wind load cases
        rfem.loading.LoadCase(
            no=1,
            name='Self-weight',
            analysis_type=rfem.loading.LoadCase.ANALYSIS_TYPE_STATIC_ANALYSIS,
            action_category=rfem.loading.LoadCase.ACTION_CATEGORY_PERMANENT_G,
            static_analysis_settings=1,
            self_weight_active=True,
            self_weight_factor_z=-1.0,
            to_solve=True,
        ),

        # Wind simulation load wizard over the range of wind directions. It has
        # to be active, otherwise the preparation of the calculation skips it
        # and no load cases are generated.
        rfem.load_wizards.WindSimulation(
            no=1,
            active=True,
            wind_definition_type=rfem.load_wizards.WindSimulation.WIND_DEFINITION_TYPE_UNIFORM_WIND_PROFILE,
            wind_profile=1,
            wind_simulation_analysis_settings=1,
            wind_direction_type=rfem.load_wizards.WindSimulation.WIND_DIRECTION_TYPE_UNIFORM,
            uniform_wind_direction_range_start=radians(WIND_DIRECTION_START),
            uniform_wind_direction_range_end=radians(WIND_DIRECTION_END),
            uniform_wind_direction_step=radians(WIND_DIRECTION_STEP),
        ),
    ]


with rfem.Application() as rfem_app:

    # Create an empty model
    rfem_app.close_all_models(save_changes=False)
    rfem_app.create_model(name='wind_simulation_from_wizard')

    # Activate the wind simulation add-on and orient the global Z-axis upwards
    base_data: rfem.BaseData = rfem_app.get_base_data()
    base_data.addons.wind_simulation_active = True
    base_data.general_settings.global_axes_orientation = \
        rfem.BaseData.GeneralSettings.GLOBAL_AXES_ORIENTATION_ZUP
    rfem_app.set_base_data(base_data=base_data)

    # Clean up the objects pre-created by the wind simulation add-on
    rfem_app.delete_all_objects()

    # Create the structure and the wind simulation objects
    rfem_app.create_object_list(
        objs=define_structure() + define_wind_simulation()
    )

    # Terrain no. 1 always exists in the model and is not removed by
    # 'delete_all_objects', so it is updated instead of created
    rfem_app.update_object(
        obj=define_terrain()
    )

    # Mesh settings of the wind simulation - shrink wrapping, terrain, solver run
    mesh_settings = rfem_app.get_mesh_settings()
    mesh_settings.wind_simulation.consider_terrain_enabled = True
    mesh_settings.wind_simulation.shrink_wrapping_main_structure = 1
    mesh_settings.wind_simulation.shrink_wrapping_surrounding_objects = 2
    mesh_settings.wind_simulation.consider_surface_thickness_above_enabled = True
    mesh_settings.wind_simulation.consider_surface_thickness_above_value = 0.005
    mesh_settings.wind_simulation.run_rwind_in_background_enabled = True
    rfem_app.set_mesh_settings(mesh_settings=mesh_settings)

    # Right after creation the wizard has generated nothing yet
    wizard = rfem_app.get_object(rfem.load_wizards.WindSimulation(no=1))
    print(f'Wizard: {wizard.name}')
    print(f'Generated load cases before the calculation: '
          f'{len(wizard.generate_into_load_cases.rows)}')

    # Step 1 | Generate the wind load cases. Calculating the self-weight load
    # case no. 1 is enough: the preparation of the calculation processes every
    # active wizard in the model, and since load case no. 1 is not a wind
    # simulation case the CFD solver is not started.
    rfem_app.calculate_specific(
        loadings=[rfem.ObjectId(no=1, object_type=rfem.ObjectType.OBJECT_TYPE_LOAD_CASE)],
        skip_warnings=True,
    )

    # Read the generated wind load cases from the wizard
    wizard = rfem_app.get_object(rfem.load_wizards.WindSimulation(no=1))
    wind_load_cases = [
        rfem.ObjectId(no=row.load_case, object_type=rfem.ObjectType.OBJECT_TYPE_LOAD_CASE)
        for row in wizard.generate_into_load_cases.rows
    ]

    print(f'\nGenerated load cases after the calculation: '
          f'{len(wizard.generate_into_load_cases.rows)}')
    for row in wizard.generate_into_load_cases.rows:
        load_case = rfem_app.get_object(rfem.loading.LoadCase(no=row.load_case))
        print(f"  LC{row.load_case} | {row.direction * 180.0 / 3.141592653589793:5.1f} deg | "
              f"wind profile {row.wind_profile} | '{load_case.name}' | "
              f"generated: {load_case.is_generated} | wind tunnel "
              f"{load_case.wind_simulation_wind_tunnel_depth:.1f} x "
              f"{load_case.wind_simulation_wind_tunnel_width:.1f} x "
              f"{load_case.wind_simulation_wind_tunnel_height:.1f} m")

    # Step 2 | Calculate the generated wind load cases
    print('\nCalculating - this takes a few minutes per wind direction ...')
    print(rfem_app.calculate_specific(
        loadings=wind_load_cases,
        skip_warnings=True,
    ).message)

    # ----------------------------------------------------------------------
    # Results
    # ----------------------------------------------------------------------

    # Retrieve results per wind direction. The result tables are wider than the
    # terminal, so they are printed with 'to_string()' to keep them untruncated.
    for loading in wind_load_cases:

        load_case = rfem_app.get_object(rfem.loading.LoadCase(no=loading.no))
        print(f'\n=== LC{loading.no} | {load_case.name} ===')

        # The wind simulation results of a load case can be missing even though
        # the calculation reported success, so this is checked explicitly.
        if not rfem_app.has_results(loading=loading).value:
            print('No results - the wind simulation of this load case did not finish.')
            continue

        # Solver messages, e.g. on the convergence of the residual pressure
        errors_and_warnings = rfem_app.get_result_table(
            table=rfem.results.ResultTable.ERRORS_AND_WARNINGS_TABLE,
            loading=loading,
        )
        print(f'\nErrors and Warnings:\n{errors_and_warnings.data.to_string(index=False)}')

        # Summary of the static analysis - the wind force resultants handed over
        # by the CFD solver, checked against the support forces. The 'value'
        # column holds SI base units (N, Nm, m).
        summary = rfem_app.get_result_table(
            table=rfem.results.ResultTable.STATIC_ANALYSIS_SUMMARY_TABLE,
            loading=loading,
        )
        totals = summary.data[summary.data['description'].str.contains(
            'sum of loads|sum of support forces', na=False)]
        print(f'\nWind Force Resultants and Support Forces [N]:\n'
              f'{totals[["description", "value"]].to_string(index=False)}')

    # Deformations of the walls and the roof in the mesh nodes [m, rad].
    # 'get_results' returns every calculated loading, so the self-weight load
    # case is filtered out and only the wind directions are kept.
    deformations = rfem_app.get_results(
        results_type=rfem.results.ResultsType.STATIC_ANALYSIS_SURFACES_GLOBAL_DEFORMATIONS_MESH_NODES
    )
    wind_deformations = deformations.data[deformations.data['loading'].isin(
        [f'LC{loading.no}' for loading in wind_load_cases])]
    print(f'\nSurfaces Global Deformations:\n{wind_deformations.head(10).to_string(index=False)}')

    # Governing deformation per wind direction
    governing = wind_deformations.loc[
        wind_deformations.groupby('loading')['u_abs'].idxmax(),
        ['loading', 'surface_no', 'mesh_node_no', 'u_abs', 'u_x', 'u_y', 'u_z'],
    ]
    print(f'\nMaximum Absolute Deformation per Load Case [m]:\n{governing.to_string(index=False)}')