Skip to main content

Tutorial - Integrate STAAD.Pro

note

Estimated time: 55 minutes
Difficulty level: Intermediate

  • Introduction

    STAAD.Pro is one of the most widely used software tools for structural engineering. It offers a range of features for modeling and design, along with a robust API called OpenSTAAD that enables you to automate a workflow. In this tutorial, you will learn how to create a VIKTOR web app that integrates STAAD.Pro behind the scenes.

    Here are the main sections we will cover:

    1. Build a VIKTOR app.
    2. Create a simple parametric model.
    3. Calculate the member end forces of your model.
    4. Display the member end forces in your VIKTOR app.

    Here is a sneak peek at how the app will look:

    model

    Prerequisites and downloads

    PREREQUISITES

    You have installed VIKTOR on your computer
    You have some experience with reading Python code
    You have some experience with STAAD.Pro

    Before using the OpenSTAAD API in Python, ensure you have pywin32 and comtypes installed in your main Python development environment. If they are not installed, you can add them using pip:

    pip install pywin32 comtypes openstaad 

    Create a new VIKTOR app

    Let's create, install and start an empty app. This will be the starting point for the rest of the tutorial. But before we start, make sure to shut down any app that is running (like the demo app) by closing the command-line shell (for example Powershell) or end the process using Ctrl + C.

    Follow these steps to create, install and start an empty app:

    1. Go to the App store in your VIKTOR environment to create a new app. After clicking 'Create app' choose the option 'Create blank app' and enter a name and description of your choice. Submit the form by clicking 'Create and setup'.

    Create app

    1. Select 'Editor' as app type and click 'Next'.

    2. Now follow the instructions to run the quickstart command to download the empty app template. After entering the command click 'I have run the command' to continue. The CLI will ask you to select your code editor of choice. Use the arrows and press enter to select a code editor. The app will now open in your code editor of choice.

    If all went well, your empty app is installed and connected to your development workspace. Do not close the terminal as this will break the connection with your app. The terminal in your code editor should show something like this:

    INFO    : Connecting to cloud.viktor.ai...
    INFO : Connection is established (use Ctrl+C to close)
    INFO :
    INFO : Navigate to the link below to see your app in the browser
    INFO : https://cloud.viktor.ai/workspaces/XXX/app
    INFO :
    INFO : App code loaded, waiting for jobs...
    RE-STARTING YOUR APP

    You only need to create an app template and install it once for each new app you want to make. as long as you don't close the terminal or your code editor, the app will update automatically once you save code in app.py. Did you close your code editor? Use viktor-cli start to start the app again. No need to install, clear, etc.

    Let's define a simple frame

    To create a basic steel structure, we use a parametric method by setting the frame's length and height as adjustable variables. The create_frame_data function defines the frame's topology through nodes and lines to represent its geometry. By modifying the length and height, we can produce various configurations of the frame. Each execution of the function also saves the geometry in a json file.

    In the following sections, we will demonstrate how this function integrates with the Controller class, activated when new inputs are provided by the Parametrization class. You can extend your app.py to look something like this:

    import viktor as vkt 
    import json

    def create_frame_data(length, height, cross_section="IPE400"):
    nodes = {
    1: {"node_id": 1, "x": 0, "z": 0, "y": 0},
    2: {"node_id": 2, "x": 0, "z": length, "y": 0},
    3: {"node_id": 3, "x": 0, "z": 0, "y": height},
    4: {"node_id": 4, "x": 0, "z": length, "y": height},
    5: {"node_id": 5, "x": length, "z": 0, "y": 0},
    6: {"node_id": 6, "x": length, "z": length, "y": 0},
    7: {"node_id": 7, "x": length, "z": 0, "y": height},
    8: {"node_id": 8, "x": length, "z": length, "y": height},
    }
    lines = {
    1: {"line_id": 1, "node_i": 1, "node_j": 3},
    2: {"line_id": 2, "node_i": 2, "node_j": 4},
    3: {"line_id": 3, "node_i": 3, "node_j": 4},
    4: {"line_id": 4, "node_i": 6, "node_j": 8},
    5: {"line_id": 5, "node_i": 5, "node_j": 7},
    6: {"line_id": 6, "node_i": 3, "node_j": 7},
    7: {"line_id": 7, "node_i": 4, "node_j": 8},
    8: {"line_id": 8, "node_i": 7, "node_j": 8},
    9: {"line_id": 9, "node_i": 1, "node_j": 4},
    10: {"line_id": 10, "node_i": 5, "node_j": 8},
    }

    with open("inputs.json", "w") as jsonfile:
    json.dump([nodes, lines, cross_section], jsonfile)

    return nodes, lines

    class Parametrization(vkt.Parametrization):
    pass

    class Controller(vkt.Controller):
    parametrization = Parametrization

    Let's create our 3D model

    Now that we have a function to create the structure's topology, the next step is to set up the inputs in the Parametrization class and add the logic to build a 3D model of the frame in a GeometryView. You can find more information about working with 3D geometry in VIKTOR in the documentation. As you will notice, adding input fields is easy thanks to the pre-built components that VIKTOR provides. You can find all the possiblities in the section on user inputs.

    import viktor as vkt 
    import json
    from io import BytesIO
    from pathlib import Path
    from viktor.geometry import Point, Line, RectangularExtrusion

    def create_frame_data(length, height, cross_section="IPE400"):
    ...

    class Parametrization(vkt.Parametrization):
    intro = vkt.Text("# STAAD - Member End Forces App")
    inputs_title = vkt.Text('''
    ## Frame Geometry
    Please fill in the following parameters to create the steel structure:
    ''')
    frame_length = vkt.NumberField("Frame Length", min=0.3, default=8, suffix="m")
    frame_height = vkt.NumberField("Frame Height", min=1, default=6, suffix="m")
    line_break = vkt.LineBreak()
    section_title = vkt.Text('''
    ## Frame Cross-Section
    Please select a cross section for the frame's elements:
    ''')
    cross_sect = vkt.OptionField("Cross-Section", options=["IPE400", "IPE200"], default="IPE400")

    class Controller(vkt.Controller):
    parametrization = Parametrization

    @vkt.GeometryView("3D Model", duration_guess=1, x_axis_to_right=True)
    def create_render(self, params, **kwargs):
    nodes, lines = create_frame_data(length=params.frame_length, height=params.frame_height)
    sections_group = []

    for line_id, dict_vals in lines.items():
    node_id_i = dict_vals["node_i"]
    node_id_j = dict_vals["node_j"]
    node_i = nodes[node_id_i]
    node_j = nodes[node_id_j]

    point_i = Point(node_i["x"], node_i["z"], node_i["y"])
    point_j = Point(node_j["x"], node_j["z"], node_j["y"])

    line_k = Line(point_i, point_j)
    cs_size = float(params.cross_sect[3:]) / 1000 # Convert from mm to meters
    section_k = RectangularExtrusion(cs_size, cs_size, line_k, identifier=str(line_id))
    sections_group.append(section_k)

    return vkt.GeometryResult(geometry=sections_group)

    As you can see we are able to use the values from the input fields from the Parametrization class in our Controller class by using the params.{parameter name} notation.

    Great! You can now modify the frame's dimensions and select a different cross-section, and the model will update to reflect the new geometry. Your browser should display an app that looks like this:

    model

    Since you're here to learn how to integrate STAAD.Pro, let's move on and start using the OpenSTAAD API!

    Working with OpenSTAAD in Python

    Let's create a new Python file called run_staad_model.py in the same directory and add the run_staad function. This function contains the logic to create our STAAD.Pro model using Python. The first step is to initialize the program with subprocess. Copy the snippet below, and make sure to update the staad_path to match your STAAD.Pro version. You can run this code in your Python environment, and it will launch STAAD.Pro and connect you to the OpenSTAAD API using comtypes.

    note

    If STAAD.Pro is running on a remote server, the staad_path needs to match the STAAD.Pro location on the server where the worker is running.

    import subprocess 
    import time
    import comtypes.client
    from pythoncom import CoInitialize, CoUninitialize
    from datetime import datetime
    from pathlib import Path
    from openstaad import Output

    def run_staad():
    CoInitialize()
    # Replace with your version and file path.
    staad_path = r"C:\Program Files\Bentley\Engineering\STAAD.Pro 2024\STAAD\Bentley.Staad.exe"
    # Launch STAAD.Pro
    staad_process = subprocess.Popen([staad_path])
    print("Launching STAAD.Pro...")
    time.sleep(15)

    # Connect to OpenSTAAD.
    openstaad = comtypes.client.GetActiveObject("StaadPro.OpenSTAAD")

    Create the structure in STAAD.Pro

    Next, let's extend the run_staad function to add nodes, beams, and supports. We'll load the nodes and lines geometry from the inputs.json file, then create the nodes and beams in STAAD.Pro, followed by defining the material properties and the cross-section for the frame. Here's how all this looks in code:

    def run_staad(): 
    CoInitialize()

    # Launch STAAD.Pro and connect to OpenSTAAD as before...

    # Create a new STAAD file.
    now = datetime.now()
    timestamp = now.strftime("%Y-%m-%d_%H-%M")
    std_file_path = Path.cwd() / f"Structure_{timestamp}.std"
    length_unit = 4 # Meter
    force_unit = 5 # Kilo Newton
    openstaad.NewSTAADFile(str(std_file_path), length_unit, force_unit)

    # Load nodes and lines
    input_json = Path.cwd() / "inputs.json"
    with open(input_json) as jsonfile:
    data = json.load(jsonfile)

    nodes, lines, section_name = data[:]

    # Wait to load interface
    time.sleep(10)

    # Set Material and Beam Section
    staad_property = openstaad.Property
    staad_property._FlagAsMethod("SetMaterialName")
    staad_property._FlagAsMethod("CreateBeamPropertyFromTable")
    material_name = "STEEL"
    staad_property.SetMaterialName(material_name)

    country_code = 7 # European database
    # section_name is retrieved from inputs.json
    type_spec = 0 # ST (Single Section from Table)
    add_spec_1 = 0.0 # Not used for single sections
    add_spec_2 = 0.0 # Must be 0.0

    # Create the beam property
    property_no = staad_property.CreateBeamPropertyFromTable(
    country_code, section_name, type_spec, add_spec_1, add_spec_2
    )

    # Create Members
    geometry = openstaad.Geometry
    geometry._FlagAsMethod("CreateNode")
    geometry._FlagAsMethod("CreateBeam")
    staad_property._FlagAsMethod("AssignBeamProperty")
    create_nodes = set()

    for line_id, vals in lines.items():
    node_i_id = str(vals["node_i"])
    node_i_coords = nodes[node_i_id]
    node_j_id = str(vals["node_j"])
    node_j_coords = nodes[node_j_id]

    if node_i_id not in create_nodes:
    geometry.CreateNode(
    int(node_i_id), node_i_coords["x"], node_i_coords["y"], node_i_coords["z"]
    )
    create_nodes.add(node_i_id)

    if node_j_id not in create_nodes:
    geometry.CreateNode(
    int(node_j_id), node_j_coords["x"], node_j_coords["y"], node_j_coords["z"]
    )
    create_nodes.add(node_j_id)

    geometry.CreateBeam(int(line_id), node_i_id, node_j_id)

    # Assign beam property to beam ids
    staad_property.AssignBeamProperty(int(line_id), property_no)

    # Create supports
    support = openstaad.Support
    support._FlagAsMethod("CreateSupportFixed")
    support._FlagAsMethod("AssignSupportToNode")

    var_support_no = support.CreateSupportFixed()
    nodes_with_support = [1, 2, 5, 6]

    for node in nodes_with_support:
    support.AssignSupportToNode(node, var_support_no)


    Run the analysis

    Now you can save the model and run the analysis. By default, we will create a load case that includes only the self-weight of the structure. After the analysis, we can extract the member end forces and save the results in a .json file to be used later in our VIKTOR app.

    def run_staad(): 

    # Previous code...

    # Create Load cases and add self-weight
    load = openstaad.Load
    load._FlagAsMethod("SetLoadActive")
    load._FlagAsMethod("CreateNewPrimaryLoad")
    load._FlagAsMethod("AddSelfWeightInXYZ")
    case_num = load.CreateNewPrimaryLoad("Self Weight")
    load.SetLoadActive(case_num) # Load Case 1
    load.AddSelfWeightInXYZ(2, -1.0) # Load factor

    # Run analysis in silent mode
    command = openstaad.Command
    command._FlagAsMethod("PerformAnalysis")
    openstaad._FlagAsMethod("SetSilentMode")
    openstaad._FlagAsMethod("Analyze")
    openstaad._FlagAsMethod("isAnalyzing")
    command.PerformAnalysis(6)
    openstaad.SaveModel(1)
    time.sleep(3)

    openstaad.SetSilentMode(1)

    openstaad.Analyze()

    while openstaad.isAnalyzing():
    print("...Analyzing")
    time.sleep(2)

    time.sleep(5)

    # Process Outputs

    output = Output()

    # GetMemberEndForces returns -> FX, FY, FZ, MX, MY and MZ (in order)
    end_forces = [list(output.GetMemberEndForces(beam=int(bid), start=False, lc=1)) for bid in lines]
    end_headers = [f"Beam:{lines[bid]['line_id']}/Node:{lines[bid]['node_j']}" for bid in lines]

    # Retrieve start forces and headers
    start_forces = [list(output.GetMemberEndForces(beam=int(bid), start=True, lc=1)) for bid in lines]
    start_headers = [f"Beam:{lines[bid]['line_id']}/Node:{lines[bid]['node_i']}" for bid in lines]

    # Combine forces and headers into lists of lists
    forces = end_forces + start_forces
    headers = end_headers + start_headers

    # Save to JSON file
    json_path = Path.cwd() / "output.json"
    with open(json_path, "w") as jsonfile:
    json.dump({"forces": forces, "headers": headers}, jsonfile)

    openstaad = None
    CoUninitialize()

    staad_process.terminate()

    return

    After running your run_staad_model.py in your Python environment, you should end up with a model like the following:

    model

    Let's Integrate the worker in our VIKTOR app

    A worker is simple program that lets you connect the app you have in the cloud to a server on which in this case STAAD.Pro and python can run to execute scripts and analyis' on models. Once we have a worker set up, we can connect our app! If you do not have access to a server with STAAD.Pro on it, do not worry, you can replicate this process on your local machine for the development of your applications.

    Don't have the worker installed yet?

    The connection between VIKTOR and STAAD.Pro is managed by the generic worker. You can install the worker on your local machine to test the integration by following this guide.

    In the Controller of our VIKTOR app, we need to set up the communication with the worker. To call the worker, we use the GenericAnalysis class, which allows us to send inputs (files and objects) to the worker. In this example, we will send the inputs.json and run_staad_model.py to be executed in the environment set up for the worker. This environment can be on your own computer or a remote server. When run_staad_model.py is executed, it will generate the output.json file, and the worker will handle transferring this file from the server environment to our VIKTOR app. Here's how it looks in the code:

    from viktor.external.generic import GenericAnalysis 
    from viktor.core import File
    from pathlib import Path
    from io import BytesIO

    class Controller(vkt.Controller):

    # Previous code...

    @vkt.TableView("Member End Forces", duration_guess=10, update_label="Run STAAD Analysis")
    def run_staad(self, params, **kwargs):
    nodes, lines = create_frame_data(length=params.frame_length, height=params.frame_height)
    cross_section = params.cross_sect
    input_json = json.dumps([nodes, lines, cross_section])
    script_path = Path(__file__).parent / "run_staad_model.py"
    files = [
    ("inputs.json", BytesIO(bytes(input_json, 'utf8'))),
    ("run_staad_model.py", File.from_path(script_path))
    ]
    generic_analysis = GenericAnalysis(
    files=files,
    executable_key="run_staad",
    output_filenames=["output.json"]
    )

    generic_analysis.execute(timeout=300)

    output_file = generic_analysis.get_output_file("output.json", as_file=True)
    output_data = json.loads(output_file.getvalue())
    forces = [[round(force, 2) for force in row] for row in output_data['forces']]

    return vkt.TableResult(
    forces,
    row_headers=output_data["headers"],
    column_headers=["FX [kN]", "FY [kN]", "FZ [kN]", "MX [kN⋅m]", "MY [kN⋅m]", "MZ [kN⋅m]"],
    )

    Now you're ready to test your app. Make sure your worker is running and connected to your VIKTOR workspace, then try generating the model. After the analysis, your app should look like this:

    model

    Your turn to build

    You now have everything you need to start building applications that integrate STAAD.Pro into your VIKTOR apps. You can also explore other integration tutorials to learn how to connect more structural or geotechnical software to your apps!