Tutorial - Integrate STAAD.Pro
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:
- Build a VIKTOR app.
- Create a simple parametric model.
- Calculate the member end forces of your model.
- Display the member end forces in your VIKTOR app.
Here is a sneak peek at how the app will look:
Prerequisites and downloads
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:
- 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'.
-
Select 'Editor' as app type and click 'Next'.
-
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...
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:
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
.
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:
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.
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:
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!