Skip to main content

Tutorial - Integrate ETABS and SAP2000

note

Estimated time: 55 minutes
Difficulty level: Intermediate

  • Introduction

    ETABS and SAP2000 are two of the most commonly used software tools for structural engineering. They offer a range of features for modeling and design, along with a robust API that automates various workflows. In this tutorial, you will learn how to create a VIKTOR web app that integrates these tools 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 reaction loads of your model.
    4. Display the reaction loads in your VIKTOR app.

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

    Prerequisites and downloads

    Prerequisites

    You have installed VIKTOR
    You have some experience with reading Python code
    You have some experience with ETABS/SAP2000

    Before using the ETABS 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

    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'.

    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 platform...
    INFO : Connection is established: https://cloud.viktor.ai <---- here you can see your app
    INFO : The connection can be closed using Ctrl+C
    INFO : App is ready
    Re-starting your app
    • You only need create an app template and install it once for each new app you want to make.
    • The app will update automatically once you start adding code in app.py, as long as you don't close the terminal or your code editor.
    • 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 structure

    To generate a simple moment frame, we use a parametric approach by defining the frame's length and height as variables. The function create_frame_data creates the frame’s topology using nodes and lines to represent its geometry. This approach allows us to generate different configurations by adjusting the length and height values. Additionally, the geometry is saved to a json file each time the function is executed. In the next sections, we will show how this function is used within the Controller class, triggered when new inputs are defined in the Parametrization class. Take a moment to review the following code snippet before continuing with the tutorial.

    import viktor as vkt
    import json

    def create_frame_data(length, height):
    nodes = {
    1:{"node_id": 1, "x": 0, "y": 0, "z": 0},
    2:{"node_id": 2, "x": 0, "y": length, "z": 0},
    3:{"node_id": 3, "x": 0, "y": 0, "z": height},
    4:{"node_id": 4, "x": 0, "y": length, "z": height},
    5:{"node_id": 5, "x": length, "y": 0, "z": 0},
    6:{"node_id": 6, "x": length, "y": length, "z": 0},
    7:{"node_id": 7, "x": length, "y": 0, "z": height},
    8:{"node_id": 8, "x": length, "y": length, "z": 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},
    }

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

    return nodes, lines


    class Parametrization(vkt.Parametrization):
    pass


    class Controller(vkt.Controller):
    parametrization = Parametrization

    Let's create the 3D model

    Now that we have a function to generate the topology of our structure, it's time to define the inputs in our Parametrization class and implement the logic to create a 3D model of the frame in a GeometryView. You can refer to the documentation for more details on how to work with 3D geometry in VIKTOR.

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

    def create_frame_data(length, height):
    ...

    class Parametrization(vkt.Parametrization):
    intro = vkt.Text(
    "# ETABS Base Reaction App"
    "\n\n Please fill in the following parameters:"
    )
    str_length = vkt.NumberField("Frame Length [mm]", min=100, default=4000)
    str_height = vkt.NumberField("Frame Height [mm]", min=100, default=4000)


    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.str_length, height=params.str_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["y"], node_i["z"])

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

    line_k = Line(point_i, point_j)
    section_k = RectangularExtrusion(300, 300, line_k, identifier=str(line_id))
    sections_group.append(section_k)

    return vkt.GeometryResult(geometry=sections_group)

    Great! Now, you can change the dimensions of your frame, and the model will render with the updated geometry. You should see an app like this in your browser:

    model

    However, since you're here to learn how to integrate ETABS and SAP2000, let's move on and start using the CSI API!

    Hands on the ETABS API

    Let's create a new Python file called run_etabs_model.py in the same directory and add the create_etabs_model function. This function contains the logic to create our ETABS model using Python. The first step is to initialize the program with comtypes. Copy the snippet below, and make sure to update the program_path to match your ETABS version. You can run this code in your Python environment, and it will open a blank ETABS model.

    note

    If ETABS is running on a remote server, the program_path needs to match the ETABS location on the server where the worker is running.

    import comtypes.client
    import pythoncom
    import json
    from pathlib import Path

    def create_etabs_model():
    # Initialize ETABS model
    program_path=r"C:\Program Files\Computers and Structures\ETABS 22\ETABS.exe"
    pythoncom.CoInitialize()
    try:
    helper = comtypes.client.CreateObject("ETABSv1.Helper")
    helper = helper.QueryInterface(comtypes.gen.ETABSv1.cHelper)
    EtabsEngine = helper.CreateObject(program_path)
    EtabsEngine.ApplicationStart()
    EtabsObject = EtabsEngine.SapModel
    EtabsObject.InitializeNewModel(9) # Set units to mm
    EtabsObject.File.NewBlank()
    finally:
    pythoncom.CoUninitialize()

    if __name__ == "__main__":
    create_etabs_model()

    Create the structure in ETABS

    Next, let's extend the create_etabs_model function to add joints, members, and rigid supports. We'll load the nodes and lines geometry from the inputs.json file, then create the nodes and members in ETABS, followed by defining the material properties for the frame. Here's how all this looks in code:

    def create_etabs_model():
    ...

    # Create joints
    input_json = Path.cwd() / "inputs.json"
    with open(input_json) as jsonfile:
    data = json.load(jsonfile)
    nodes, lines = data[:]

    # Create nodes
    for id, node in nodes.items():
    ret, _ = EtabsObject.PointObj.AddCartesian(
    node["x"], node["y"], node["z"], " ", str(id)
    )

    # Create members
    MATERIAL_CONCRETE = 2
    ret = EtabsObject.PropMaterial.SetMaterial("CONC", MATERIAL_CONCRETE)
    ret = EtabsObject.PropMaterial.SetMPIsotropic("CONC", 30000, 0.2, 0.0000055)
    section_name = "300x300 RC"
    ret = EtabsObject.PropFrame.SetRectangle(section_name, "CONC", 300, 300)
    for id, line in lines.items():
    point_i = line["node_i"]
    point_j = line["node_j"]
    ret, _ = EtabsObject.FrameObj.AddByPoint(
    str(point_i), str(point_j), str(id), section_name, "Global"
    )

    # Add rigid supports
    list_nodes = [1, 2, 5, 6]
    for node_id in list_nodes:
    ret = EtabsObject.PointObj.SetRestraint(str(node_id), [1, 1, 1, 1, 1, 1])

    EtabsObject.View.RefreshView(0, False)

    Run the model

    Now you can save the model and run the analysis. By default, ETABS creates a load case called Dead, which includes only the self-weight of the structure. After the analysis, we can extract the reaction loads and save the results in a .json file to be used later in our VIKTOR app.

    def create_etabs_model():
    ...
    # Create the model and run the analysis
    file_path = Path.cwd() / "etabsmodel.edb"
    EtabsObject.File.Save(str(file_path))
    EtabsObject.Analyze.RunAnalysis()

    # Get the reaction loads
    load_case = "Dead"
    ret = EtabsObject.Results.Setup.DeselectAllCasesAndCombosForOutput()
    reactions_list = list()
    ret = EtabsObject.Results.Setup.SetCaseSelectedForOutput(load_case)
    for node in list_nodes:
    *_, U1, U2, U3, R1, R2, R3, ret = EtabsObject.Results.JointReact(
    str(node), 0, 0
    )
    reaction = {
    "Node": str(node),
    "LoadCase": load_case,
    "U1": U1[0],
    "U2": U2[0],
    "U3": U3[0],
    "R1": R1[0],
    "R2": R2[0],
    "R3": R3[0],
    }
    reactions_list.append(reaction)

    # Save the output in a JSON
    output = Path.cwd() / "output.json"
    with open(output, "w") as jsonfile:
    json.dump(reactions_list, jsonfile)

    ret = EtabsEngine.ApplicationExit(False)

    return ret

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

    model

    How to connect your VIKTOR app with the CSI API?

    The connection between VIKTOR and SAP2000 or ETABS is managed by a “worker.” A worker is a program that connects the VIKTOR platform to third-party software running outside the platform. You can install the worker on your local machine to test the integration by following this tutorial.

    Let's Integrate the worker in our VIKTOR app

    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_etabs_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_etabs_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

    class Controller(vkt.Controller):
    ...
    def run_etabs(self, params, **kwargs):
    nodes, lines = create_frame_data(length=params.str_length, height= params.str_height)
    input_json = json.dumps([nodes, lines])
    script_path = Path(__file__).parent / "run_etabs_model.py"

    files = [
    ("inputs.json", BytesIO(bytes(input_json, 'utf8'))),
    ("run_etabs_model.py", File.from_path(script_path))
    ]
    generic_analysis = GenericAnalysis(
    files=files,
    executable_key="run_etabs",
    output_filenames=["output.json"]
    )
    generic_analysis.execute(timeout=300)
    output_file = generic_analysis.get_output_file("output.json", as_file=True)

    And of course, we would like to see the results in a nice table, so we will decorate the previous function with a tableview and post-process the results from the output.json file, as shown below:

    # Previous imports omitted

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

    def create_frame_data(length, height):
    ...
    class Parametrization(vkt.Parametrization):
    ...

    class Controller(vkt.Controller):
    parametrization = Parametrization

    @vkt.GeometryView("3D model", duration_guess=1, x_axis_to_right=True)
    def create_render(self, params, **kwargs):
    ...

    @vkt.TableView("Base Reactions",duration_guess=10, update_label="Run ETABS analysis")
    def run_etabs(self, params, **kwargs):

    nodes, lines = create_frame_data(length=params.str_length, height= params.str_height)
    input_json = json.dumps([nodes, lines])
    script_path = Path(__file__).parent / "run_etabs_model.py"

    files = [
    ("inputs.json", BytesIO(bytes(input_json, 'utf8'))),
    ("run_etabs_model.py", File.from_path(script_path))
    ]
    generic_analysis = GenericAnalysis(
    files=files,
    executable_key="run_etabs",
    output_filenames=["output.json"]
    )
    generic_analysis.execute(timeout=300)
    output_file = generic_analysis.get_output_file("output.json", as_file=True)
    reactions = json.loads(output_file.getvalue())
    data = []
    for reaction in reactions:
    data.append([
    reaction.get("Node", 0),
    reaction.get("LoadCase", "Dead"),
    round(reaction.get("U1", 0),0),
    round(reaction.get("U2", 0),0),
    round(reaction.get("U3", 0),0)
    ])
    return vkt.TableResult(
    data,
    row_headers=["Sup 1","Sup 2","Sup 3","Sup 4"],
    column_headers=["Node", "Load Case","U1[N]", "U2[N]", "U3[N]"],
    )

    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

    What about SAP2000?

    You can integrate SAP2000 in the same way we did for ETABS. All you need to do is change the program path and the helper, as shown below.

    import pythoncom
    import comtypes.client

    def initialize_sap2000(
    program_path=r"C:\Program Files\Computers and Structures\SAP2000 25\SAP2000.exe",
    ):
    pythoncom.CoInitialize()
    try:
    helper = comtypes.client.CreateObject("SAP2000v1.Helper")
    helper = helper.QueryInterface(comtypes.gen.SAP2000v1.cHelper)
    SapEngine = helper.CreateObject(program_path)
    SapEngine.ApplicationStart()
    SapObject = SapEngine.SapModel
    SapObject.InitializeNewModel()
    SapObject.File.NewBlank()
    return SapObject, SapEngine
    finally:
    pythoncom.CoUninitialize()

    Your turn to build

    You now have everything you need to start building powerful applications that integrate ETABS and SAP2000 into your VIKTOR apps. But the journey doesn’t end here, you can explore our other tutorials or visit the next section to learn more about VIKTOR.