Examples¶
The examples/ directory contains runnable scripts that demonstrate the library
from simple to advanced use cases.
Note: All examples require Autodesk Robot Structural Analysis Professional to be running with an open model before execution.
01 — Initialize¶
Connects to Robot and verifies the connection by adding a single node. The simplest possible starting point.
"""
This example shows how to initialize connection with Autodesk Robot. To verify connection, one node is added.
NOTE: You need to open a blank Robot model before execution of this script.
"""
import pyrobotstructural # 'import pyrobotstructural as pyrbt' or similar abbreviation can be optionally used
# Path can differ, update it according to your needs. It usually requires version number update.
dll_path = r"C:\Program Files\Autodesk\Robot Structural Analysis Professional 2026\Exe\Interop.RobotOM.dll"
pyrobotstructural.initialize(dll_path)
app = pyrobotstructural.RobotApp()
# --- MVerify ---
# To verify if connection works, lets add a node to the model.
app.model.geometry.add_node(1, 0, 0, 0)
02 — Build geometry¶
Demonstrates adding nodes, bars (members), shell panels, and cladding surfaces.
Shows how to use begin_edit() to batch many geometry operations into a single
COM transaction for large performance gains.
"""
This example shows how to define simple geometry objects such as nodes, bars (members), shell and cladding.
It also shows basic support assignment.
Performance tip: wrapping bulk geometry and support operations in ``app.model.begin_edit()``
batches all COM calls into a single multi-operation flush. For models with many nodes/members
this can be 10-100x faster than individual calls.
NOTE: You need to open a blank Robot model, go for Shell Design or Frame 3D Design structure type.
"Pinned" support label must exist in the model (Robot built-in default).
"""
import pyrobotstructural
# you can optionally use 'import pyrobotstructural as pyrbt' or similar abbreviation
# --- Initialize ---
dll_path = r"C:\Program Files\Autodesk\Robot Structural Analysis Professional 2026\Exe\Interop.RobotOM.dll"
pyrobotstructural.initialize(dll_path)
app = pyrobotstructural.RobotApp()
# --- Clear previous model ---
app.model.management.clear()
# --- Define geometry ---
node_data = [
[1, 0.0, 0.0, 0.0],
[2, 3.0, 0.0, 0.0],
[3, 0, 3.0, 0.0],
[4, 3, 3.0, 0.0],
[5, 0, 5, 0],
[6, 3, 5, 0],
[7, 1.5, 3, 0],
[8, 1.5, 5, 0],
]
bars = [
[1, 1, 2],
[2, 1, 3],
[3, 2, 4],
[4, 3, 4],
[5, 5, 6],
[6, 7, 8],
[7, 3, 5],
[8, 4, 6],
]
# --- Add nodes, bars, and supports in a single batched multi-operation ---
# begin_edit() calls BeginMultiOperation / EndMultiOperation around the block,
# flushing all COM writes in one go instead of round-tripping for each object.
with app.model.begin_edit():
app.model.geometry.add_node(node_data)
app.model.geometry.add_member(bars, material_name="S235", section_name="IPE 100")
app.model.supports.apply_node_support(
node_number=[1, 2, 3, 4, 5, 6], support_name="Pinned"
)
# --- Geometry for contour ---
contour_for_shell = [
[1, 0.0, 0.0, 0.0],
[2, 3.0, 0.0, 0.0],
[3, 3, 3.0, 0.0],
[4, 0, 3.0, 0.0],
]
# --- Add slab ---
app.model.geometry.add_shell_by_contour(
points=contour_for_shell,
thickness=0.1,
thickness_name="10cm",
material_name="C20/25",
)
# --- Add cladding ---
contour_for_cladding = [
[1, 0, 3.0, 0.0],
[2, 3, 3.0, 0.0],
[3, 3, 5, 0],
[4, 0, 5, 0],
]
app.model.geometry.add_cladding(
points=contour_for_cladding, load_distribution="Two-way"
)
03 — Loads¶
Creates load cases (permanent and exploitation natures), applies several load types (self-weight, uniform member loads, panel loads), and creates ULS and SLS combinations with load factors.
"""
This example shows how to define load cases, loads and load combinations.
Performance note: load cases are resolved by name via an internal cache.
The first ``add_*`` call for a given case name scans the COM collection once;
all subsequent calls for the same case cost only a dict lookup.
"""
import pyrobotstructural
from pyrobotstructural.enums import CaseNature, CaseAnalizeType, CombinationType
# you can optionally use 'import pyrobotstructural as pyrbt' or similar abbreviation
# --- Initialize ---
dll_path = r"C:\Program Files\Autodesk\Robot Structural Analysis Professional 2026\Exe\Interop.RobotOM.dll"
pyrobotstructural.initialize(dll_path)
app = pyrobotstructural.RobotApp()
# --- Clear previous model ---
app.loads.cases.clear()
# --- Define loadcases ---
app.loads.cases.add_loadcase(
number=1,
name="Self-weight",
label="SW",
nature=CaseNature.PERMANENT,
analize_type=CaseAnalizeType.STATIC_LINEAR,
)
app.loads.cases.add_loadcase(
number=2,
name="Live load",
label="LL",
nature=CaseNature.EXPLOITATION,
analize_type=CaseAnalizeType.STATIC_LINEAR,
)
app.loads.cases.add_loadcase(
number=3,
name="Live load 2",
label="LL2",
nature=CaseNature.EXPLOITATION,
analize_type=CaseAnalizeType.STATIC_LINEAR,
)
# --- Define loads ---
app.loads.load.add_self_weight(case_name="Self-weight", objects="all")
app.loads.load.add_uniform_panel_load(
case_name="Live load", objects="all", loads=[0, 0, -2000]
)
app.loads.load.add_node_load(
case_name="Live load", objects="1, 6", loads=[0, 0, -3000, 0, 0, 0]
)
app.loads.load.add_uniform_load(
case_name="Live load 2", objects="6", loads=[1000, 0, -1000, 0, 0, 0]
)
# --- Define combinations ---
app.loads.combinations.add_combination(
comb_number=100,
comb_name="ULS 1",
label="ULS_1",
comb_type=CombinationType.ULS,
case_analize_type=CaseAnalizeType.COMB_LINEAR,
case_nature=CaseNature.EXPLOITATION, # Is this needed?
factors=[(1, 1.35), (2, 1.5), (3, 1.15)],
)
app.loads.combinations.add_combination(
comb_number=100,
comb_name="SLS 1",
label="SLS_1",
comb_type=CombinationType.SLS,
case_analize_type=CaseAnalizeType.COMB_LINEAR,
case_nature=CaseNature.EXPLOITATION, # Is this needed?
factors=[(1, 1), (2, 1), (3, 0.6)],
)
04 — View control¶
Interactive demonstration of the viewport API: zoom, pan, rotate, toggling display annotations (node numbers, section shapes, local axes, supports), and switching between results visualisations (forces, displacements, reactions, stresses, utilisations).
"""
View control interactive example — manipulate and display methods.
Demonstrates how to control the Robot Structural Analysis view using
pyrobotstructural. The script is organised in sections; each section
calls input() so you can observe the result in Robot before moving on.
NOTE: Works with any open model — no specific geometry is assumed.
"""
import pyrobotstructural
dll_path = r"C:\Program Files\Autodesk\Robot Structural Analysis Professional 2026\Exe\Interop.RobotOM.dll"
pyrobotstructural.initialize(dll_path)
app = pyrobotstructural.RobotApp()
view = app.view.view # ViewFacade.view → ViewManager
view.restore_default()
# =============================================================================
# 1. MANIPULATE — zoom, pan and rotate the view
# =============================================================================
# Zoom in to 2× the current zoom level.
view.manipulate(zoom_factor=2.0)
input("Zoomed in 2×. Press Enter to zoom back out …")
# Zoom out (factor < 1 moves the camera further away).
view.manipulate(zoom_factor=0.5)
input("Zoomed out. Press Enter to pan the view …")
# Pan: positive pan_up moves the viewport upward, pan_right moves it right.
view.manipulate(zoom_factor=1.0, pan_up=0.5, pan_right=0.5)
input("Panned up and right. Press Enter to rotate …")
# Rotate around X axis by 30 °. Note: rotation only works in 3-D views.
view.manipulate(rotation_x=30.0)
input("Rotated 30 ° around X. Press Enter to rotate back …")
view.manipulate(rotation_x=-30.0)
input("Rotation restored. Press Enter to go to display settings …")
# =============================================================================
# 2. DISPLAY — toggle structural element annotations
# =============================================================================
# Show node and bar numbers.
view.display(node_numbers=True, bar_numbers=True)
input("Node and bar numbers ON. Press Enter to add section shapes …")
# Add section shape rendering and support symbols with labels.
view.display(section_shapes=True, supports=True, with_codes=True)
input("Section shapes and supports with codes ON. Press Enter …")
# Show member local coordinate systems.
view.display(member_lcs=True)
input("Member LCS ON. Press Enter to turn off annotations …")
# Turn everything off to get a clean view.
view.display(
node_numbers=False,
bar_numbers=False,
section_shapes=False,
supports=False,
releases=False,
member_lcs=False,
panel_lcs=False,
)
input("All annotations OFF. Press Enter to continue to loads display …")
# =============================================================================
# 3. LOADS DISPLAY — show load symbols for different case selections
# =============================================================================
# Show load symbols and values for all simple load cases (default).
view.loads_display(symbols=True, values=True, symbol_size=5)
input("Loads displayed for Simple Cases (default). Press Enter …")
# Show loads for a specific case number.
view.loads_display(symbols=True, values=True, cases="1")
input("Loads displayed for case 1 only. Press Enter …")
# Show loads for all cases (simple + combinations).
view.loads_display(symbols=True, values=True, cases="all")
input("Loads displayed for all cases. Press Enter …")
# Hide load display by disabling symbols.
view.loads_display(symbols=False, values=False)
input("Loads hidden. Press Enter to display member forces …")
# =============================================================================
# 4. RESULTS — member forces, displacements, reactions
# Requires the model to be analysed before these have an effect.
# =============================================================================
# --- Member forces ---
# Show Mz (bending about local Z) with filled diagram and colour-differentiated
# positive/negative zones. Displayed for all simple load cases.
view.display_member_forces(
Fx=False,
Fy=False,
Fz=True,
Mx=False,
My=True,
Mz=True,
filling=True,
pos_neg=True,
values_type="all",
labels=True,
cases="Simple Cases",
)
input("Fz / My / Mz diagrams ON for Simple Cases. Press Enter …")
# Switch to combinations only.
view.display_member_forces(
Fx=False,
Fy=False,
Fz=True,
Mx=False,
My=True,
Mz=True,
filling=True,
pos_neg=True,
values_type="global extremes",
cases="Combinations",
)
input(
"Same forces for Combinations, showing global extremes. Press Enter to clear the results…"
)
# --- Reset ---
view.clear_results()
# --- Displacements ---
view.display_displacements(display=True, labels=True, cases="Simple Cases")
input("Deflections shown for Simple Cases. Press Enter to hide …")
view.display_displacements(display=False)
input("Deflections hidden. Press Enter to show reactions …")
# --- Reactions ---
view.display_reactions(Rx=True, Ry=True, Rz=True, cases="Simple Cases")
input("Reactions (Rx, Ry, Rz) shown for Simple Cases. Press Enter to hide …")
view.display_reactions(Rx=False, Ry=False, Rz=False)
input("Reactions hidden. Press Enter to show stresses …")
# --- Stresses ---
view.display_stresses(display=True, s_max=True, labels=True, cases="Simple Cases")
input("Max stress map ON. Press Enter to hide …")
view.display_stresses(display=False)
input("Stresses hidden. Press Enter to show s_max member stresses …")
view.display_member_stresses(display=True, s_max=True, labels=True)
input("Max stress diagram ON. Press Enter to hide …")
view.display_member_stresses(display=False)
input("Stresses hidden. Press Enter to show utilisations …")
# --- Utilisations (requires steel/timber design results) ---
view.display_utilisations(
display=True, labels=True, thickness_coeff=5, cases="Combinations"
)
input("Utilisation map ON for Combinations. Press Enter to finish …")
view.display_utilisations(display=False)
print("View controlling example complete.")
05 — Query model data¶
Read-only access to model objects: lists all nodes, bars, load cases, and combinations from a calculated Robot model.
"""
Query geometry — read model file path, nodes, bars, load cases, and combinations.
Works with any open model (analysis not required).
All values are in Robot's internal SI units (N, m, Pa, rad).
"""
import pyrobotstructural
dll_path = r"C:\Program Files\Autodesk\Robot Structural Analysis Professional 2026\Exe\Interop.RobotOM.dll"
pyrobotstructural.initialize(dll_path)
app = pyrobotstructural.RobotApp()
# =============================================================================
# 1. MODEL — file path
# =============================================================================
print("=" * 60)
print("MODEL")
print("=" * 60)
model_path = app.query.model.get_model_path()
print(f" Model directory: {model_path}")
# =============================================================================
# 2. NODES — coordinates
# =============================================================================
print()
print("=" * 60)
print("NODES")
print("=" * 60)
# All nodes as a plain list — single COM collection fetch, no per-node round-trips
node_list = app.query.nodes.get_all_in_list()
print(f" Total nodes: {len(node_list)}")
print("Printing first 5 nodes data:")
for nr, x, y, z in node_list[:5]:
print(f" {nr:>5} {x:>10.4f} {y:>10.4f} {z:>10.4f}")
# Individual node access — get node 1 by index
node = app.query.nodes.get_node(1)
print(f"\n Node 1 (by index): number={node.Number} IsCalc={node.IsCalc}")
# Convenience wrapper for coordinates
coords = app.query.nodes.get_node_coords(node)
print(f" Node 1 coords: X={coords[0]:.4f} Y={coords[1]:.4f} Z={coords[2]:.4f}")
# Exclude calculation nodes (auto-generated by FE mesh or analysis).
# Returns a plain list[IRobotNode] — supports len(), iteration, and slicing.
real_nodes = app.query.nodes.get_all(exclude_calc_nodes=True)
print(f"\n User-defined nodes (exclude calc): {len(real_nodes)}")
# =============================================================================
# 3. BARS — topology and section properties
# =============================================================================
print()
print("=" * 60)
print("BARS")
print("=" * 60)
# Topology: bar number + start/end node numbers
bar_topo = app.query.bars.get_all_bars_node_numbers()
print(f" Total bars: {len(bar_topo)}")
print(f" {'Bar':>5} {'Start node':>12} {'End node':>10}")
for bar_nr, start, end in bar_topo:
print(f" {bar_nr:>5} {start:>12} {end:>10}")
# Individual bar — section name and material
if bar_topo:
first_bar_nr = bar_topo[0][0]
bar = app.query.bars.get_bar(first_bar_nr)
if bar is not None:
print(f"\n Bar {first_bar_nr}:")
print(f" Start node : {bar.StartNode}")
print(f" End node : {bar.EndNode}")
print(f" IsSuperBar : {bar.IsSuperBar}")
section = app.query.bars.get_bar_section_data(bar)
print(f" Section : {section.Name}")
# =============================================================================
# 4. LOAD CASES — names, numbers, natures
# =============================================================================
print()
print("=" * 60)
print("LOAD CASES")
print("=" * 60)
all_cases = app.query.loadcases.get_all_load_cases()
print(f" Total cases (simple + combinations): {all_cases.Count}")
# Walk through and print simple load cases
print(f"\n {'Nr':>5} {'Name':<30} {'Nature':>6} {'Type':>6}")
for i in range(1, all_cases.Count + 1):
lcase = app.query.loadcases.get_simple_loadcase(case_index=i)
# lcase.Type: 0 = simple, 1 = combination
if int(lcase.Type) == 0:
print(
f" {lcase.Number:>5} {lcase.Name:<30} {int(lcase.Nature):>6} {int(lcase.AnalizeType):>6}"
)
# Access a specific case by its user-assigned number (e.g. case 2)
case1 = app.query.loadcases.get_simple_loadcase(number=2)
print(f"\n Case number=2: name='{case1.Name}'")
# =============================================================================
# 5. COMBINATIONS — names, numbers, types
# =============================================================================
print()
print("=" * 60)
print("COMBINATIONS")
print("=" * 60)
# Return as plain list of [name, number, comb_type_string]
comb_list = app.query.combinations.get_all(return_objects=False)
print(f" Total combinations: {len(comb_list)}")
if comb_list:
print(f" {'Nr':>5} {'Name':<30} {'Type':>6}")
for name, number, comb_type in comb_list:
print(f" {number:>5} {name:<30} {comb_type:>6}")
print()
print("Geometry query complete.")
05a — Query bar results¶
Queries bar member analysis results: internal forces at a point, forces at multiple points along the bar, maximum deflection, cross-section stresses, and stress envelopes.
"""
Query member results — bar forces, deflections, and stresses.
Requires an open and analysed model with bars.
All values are in Robot's internal SI units (N, m, Pa, rad).
"""
import pyrobotstructural
dll_path = r"C:\Program Files\Autodesk\Robot Structural Analysis Professional 2026\Exe\Interop.RobotOM.dll"
pyrobotstructural.initialize(dll_path)
app = pyrobotstructural.RobotApp()
bar_topo = app.query.bars.get_all_bars_node_numbers()
bar_nr = bar_topo[0][0] # first bar
case_nr = 1 # adjust to a valid case number in your model
# =============================================================================
# BAR RESULTS
# =============================================================================
print("=" * 60)
print("BAR RESULTS")
print("=" * 60)
# --- Forces at midspan ---
forces = app.query.results.get_forces(bar_nr, case_nr, position=0.5)
print(f"\n Forces on bar {bar_nr}, case {case_nr}, pos=0.5:")
print(f" Fx={forces.fx:>12.2f} N (axial)")
print(f" Fy={forces.fy:>12.2f} N (shear local Y)")
print(f" Fz={forces.fz:>12.2f} N (shear local Z)")
print(f" Mx={forces.mx:>12.2f} Nm (torsion)")
print(f" My={forces.my:>12.2f} Nm (bending local Y)")
print(f" Mz={forces.mz:>12.2f} Nm (bending local Z)")
# --- Forces at 5 evenly-spaced points ---
force_pts = app.query.results.get_forces_at_points(bar_nr, case_nr, n_points=5)
print(f"\n Mz envelope on bar {bar_nr}, case {case_nr} (5 points):")
print(f" {'Point':>7} {'My [Nm]':>14}")
for idx, f in enumerate(force_pts):
print(f" {idx + 1:>7} {f.my:>14.2f}")
# --- Deflections at midspan ---
defl = app.query.results.get_deflections(bar_nr, case_nr, position=0.5)
print(f"\n Deflections on bar {bar_nr}, case {case_nr}, pos=0.5:")
print(f" UX={defl.ux * 1000:>10.4f} mm")
print(f" UY={defl.uy * 1000:>10.4f} mm")
print(f" UZ={defl.uz * 1000:>10.4f} mm")
print(f" RX={defl.rx:>10.6f} rad")
print(f" RY={defl.ry:>10.6f} rad")
print(f" RZ={defl.rz:>10.6f} rad")
# --- Maximum deflection at given bar ---
max_defl = app.query.results.get_max_deflection(bar_nr, case_nr)
print(f"\n Maximum Uz on a bar {bar_nr}, case {case_nr}:")
print(f" {max_defl.uz * 1000:>12.4f}")
# --- Stresses at midspan ---
stress = app.query.results.get_stresses(bar_nr, case_nr, pos=0.5)
print(f"\n Stresses on bar {bar_nr}, case {case_nr}, pos=0.5:")
print(f" Smax ={stress.smax / 1e6:>10.3f} MPa")
print(f" Smin ={stress.smin / 1e6:>10.3f} MPa")
print(f" Shear Y={stress.shear_y / 1e6:>10.3f} MPa")
print(f" Shear Z={stress.shear_z / 1e6:>10.3f} MPa")
print(f" Torsion={stress.torsion / 1e6:>10.3f} MPa")
# --- Stress envelope at 5 points ---
stress_pts = app.query.results.get_stresses_at_points(bar_nr, case_nr, n_points=5)
print(f"\n Smax envelope on bar {bar_nr}, case {case_nr} (5 points):")
print(f" {'Point':>7} {'Smax [MPa]':>12}")
for idx, s in enumerate(stress_pts):
print(f" {idx + 1:>7} {s.smax / 1e6:>12.3f}")
print()
print("Member results query complete.")
05b — Query shell results¶
Queries shell finite element results: in-plane forces (Nxx, Nyy, Mxx, Myy, Qxx, Qyy) and stresses (Sxx, Syy, von Mises) at the mid, top, and bottom through-thickness layers.
"""
Query shell results — FE forces and stresses for panel elements.
Requires an open and analysed model with panels (shell elements).
All values are in Robot's internal SI units (N, m, Pa, rad).
"""
import pyrobotstructural
from pyrobotstructural.enums import ShellLayer
dll_path = r"C:\Program Files\Autodesk\Robot Structural Analysis Professional 2026\Exe\Interop.RobotOM.dll"
pyrobotstructural.initialize(dll_path)
app = pyrobotstructural.RobotApp()
# Change element_nr to a valid finite element number from your model.
element_nr = 500
case_nr = 1
# =============================================================================
# SHELL (FE) RESULTS
# =============================================================================
print("=" * 60)
print("SHELL FE RESULTS")
print("=" * 60)
for layer_name, layer in [
("MID", ShellLayer.MID),
("TOP", ShellLayer.TOP),
("BOTTOM", ShellLayer.BOTTOM),
]:
sf = app.query.shell_results.get_forces(
element_nr,
case_nr,
layer=layer,
)
print(
f"\n Shell forces — element {element_nr}, case {case_nr}, layer={layer_name}:"
)
print(
f" Nxx={sf.nxx / 1e3:>10.3f} kN/m Nyy={sf.nyy / 1e3:>10.3f} kN/m Nxy={sf.nxy / 1e3:>10.3f} kN/m"
)
print(
f" Mxx={sf.mxx / 1e3:>10.3f} kNm/m Myy={sf.myy / 1e3:>10.3f} kNm/m Mxy={sf.mxy / 1e3:>10.3f} kNm/m"
)
print(f" Qxx={sf.qxx / 1e3:>10.3f} kN/m Qyy={sf.qyy / 1e3:>10.3f} kN/m")
ss = app.query.shell_results.get_stresses(element_nr, case_nr, layer=layer)
print(
f" Shell stresses — element {element_nr}, case {case_nr}, layer={layer_name}:"
)
print(
f" Sxx={ss.sxx / 1e6:>10.3f} MPa Syy={ss.syy / 1e6:>10.3f} MPa Sxy={ss.sxy / 1e6:>10.3f} MPa"
)
print(f" Txx={ss.txx / 1e6:>10.3f} MPa Tyy={ss.tyy / 1e6:>10.3f} MPa")
print(f" von Mises={ss.mises / 1e6:>10.3f} MPa")
print()
print("Shell results query complete.")
06 — Full beam example¶
Complete end-to-end workflow on a simple 4-node beam: pinned and roller supports, self-weight and live load cases, a ULS combination, calculation, and visualisation of the My bending moment diagram with a screenshot.
"""
Simple beam example using the pyrobotstructural wrapper.
The workflow:
- Creates geometry: 4 nodes, 3 bars (IPE 100)
- Pinned support at node 1, Roller support at nodes 2-4
- Self-weight (permanent) + live uniform load (exploitation)
- ULS 1 combination (1.35 SW + 1.5 LL)
- Calculate and display bending moment My
Performance notes:
- Geometry and support creation are wrapped in ``app.model.begin_edit()`` to
batch all COM writes into a single multi-operation flush.
- Load records share a case object via the internal case cache, so each
``add_*`` load call costs a dict lookup instead of a full COM scan.
NOTE: You need to open a blank Robot model, go for Frame 2D Design or Frame 3D Design structure type.
"Pinned" support label must exist in the model (Robot built-in default).
"""
import pyrobotstructural
from pyrobotstructural.enums import CaseNature, CaseAnalizeType, CombinationType
dll_path = r"C:\Program Files\Autodesk\Robot Structural Analysis Professional 2026\Exe\Interop.RobotOM.dll"
pyrobotstructural.initialize(dll_path)
app = pyrobotstructural.RobotApp()
# Clear any previous model content
app.model.management.clear()
# Custom roller support must be defined before the multi-operation block
# because label creation is not batched.
app.model.supports.define_nodal_support(
name="Roller", ux=0, uy=1, uz=1, rx=0, ry=0, rz=0
)
# --- Geometry + supports — batched into a single multi-operation flush ---
with app.model.begin_edit():
app.model.geometry.add_node(
[
[1, 0, 0, 0],
[2, 3, 0, 0],
[3, 6, 0, 0],
[4, 9, 0, 0],
]
)
app.model.geometry.add_member(
[[1, 1, 2], [2, 2, 3], [3, 3, 4]],
section_name="IPE 100",
)
# Pinned at node 1 (predefined label assumed to exist)
app.model.supports.apply_node_support(node_number=1, support_name="Pinned")
app.model.supports.apply_node_support(node_number=[2, 3, 4], support_name="Roller")
# --- Load cases ---
app.loads.cases.add_loadcase(
name="Self-weight",
nature=CaseNature.PERMANENT,
analize_type=CaseAnalizeType.STATIC_LINEAR,
number=1,
)
app.loads.cases.add_loadcase(
name="Live load",
nature=CaseNature.EXPLOITATION,
analize_type=CaseAnalizeType.STATIC_LINEAR,
number=2,
)
# --- Loads ---
# Self-weight acting in -Z direction
app.loads.load.add_self_weight(
case_name="Self-weight",
objects="all",
factors=[0, 0, -1],
)
# Uniform live load: -500 N/m in Z direction
app.loads.load.add_uniform_load(
case_name="Live load",
objects="all",
loads=[0, 0, -500, 0, 0, 0],
)
# --- Combination ---
app.loads.combinations.add_combination(
comb_number=3,
comb_name="ULS 1",
label="3",
comb_type=CombinationType.ULS,
case_analize_type=CaseAnalizeType.COMB_LINEAR,
case_nature=CaseNature.PERMANENT,
factors=[(1, 1.35), (2, 1.5)],
)
# --- Calculate ---
app.calculate(ignore_warnings=True)
# --- Display bending moment My ---
app.view.view.display_member_forces(
Fx=False, Fy=False, Fz=False, Mx=False, My=True, Mz=False, labels=True, scale=1
)
# --- Save screenshot ---
model_path = app.query.model.get_model_path()
app.view.screenshots.save_screenshot(name="Moments-My", dir_path=model_path)
print("Done.")
07 — Support types¶
Advanced support definitions covering all available types: rigid (pinned, fixed, roller), elastic translational and rotational springs, one-directional (compression-only or tension-only), combined elastic + unilateral, and skewed supports via alpha/beta/gamma orientation angles.
"""
Example: advanced nodal support definitions.
Demonstrates the full range of nodal support types available through
``define_nodal_support``:
1. Rigid supports (pinned, fixed, roller).
2. Elastic spring supports (translational and rotational springs).
3. One-directional (unilateral) supports — resist load in one direction only,
e.g. compression-only foundation, tension-only anchor.
4. Combined elastic + one-directional support.
5. Local-axis (skewed) support.
NOTE: Open a blank Robot model (Frame 3D or Shell Design) before running.
"""
import pyrobotstructural
DLL_PATH = r"C:\Program Files\Autodesk\Robot Structural Analysis Professional 2026\Exe\Interop.RobotOM.dll"
pyrobotstructural.initialize(DLL_PATH)
app = pyrobotstructural.RobotApp()
app.model.management.clear()
# ---------------------------------------------------------------------------
# 1. Rigid supports
# ---------------------------------------------------------------------------
# Pinned: all translations fixed, all rotations free
app.model.supports.define_nodal_support("Pinned", ux=1, uy=1, uz=1)
# Fixed (encastre): all six DOFs restrained
app.model.supports.define_nodal_support("Fixed", ux=1, uy=1, uz=1, rx=1, ry=1, rz=1)
# Roller in Z: only vertical translation fixed
app.model.supports.define_nodal_support("RollerZ", uz=1)
# ---------------------------------------------------------------------------
# 2. Elastic spring supports
# ---------------------------------------------------------------------------
# Translational spring in X only (kN/m)
app.model.supports.define_nodal_support("SpringX_1000", kx=1000.0)
# Winkler-type elastic foundation: springs in all three translations
app.model.supports.define_nodal_support(
"ElasticFoundation",
kx=5000.0, # kN/m
ky=5000.0,
kz=10000.0,
)
# Rotational spring about Z (kNm/rad) — semi-rigid column base
app.model.supports.define_nodal_support("SemiRigidBase", uz=1, hz=15000.0)
# ---------------------------------------------------------------------------
# 3. One-directional (unilateral) supports
# ---------------------------------------------------------------------------
# Compression-only vertical support: resists downward movement (negative Z)
# Use "-" to block in the negative direction only.
app.model.supports.define_nodal_support(
"CompressionOnlyZ",
one_dir={"uz": "-"},
)
# Tension-only anchor: resists upward pull (positive Z)
app.model.supports.define_nodal_support(
"TensionOnlyZ",
one_dir={"uz": "+"},
)
# Multi-DOF unilateral: compression-only Z, friction-like constraint on X+
app.model.supports.define_nodal_support(
"UnilateralXZ",
one_dir={"uz": "-", "ux": "+"},
)
# ---------------------------------------------------------------------------
# 4. Combined elastic + one-directional
# ---------------------------------------------------------------------------
# Elastic compression-only spring in Z: spring acts only under compression
app.model.supports.define_nodal_support(
"ElasticCompressionZ",
kz=8000.0,
one_dir={"uz": "-"},
)
# ---------------------------------------------------------------------------
# 5. Skewed (local-axis) support — rotated 45° about global Z
# ---------------------------------------------------------------------------
import math
app.model.supports.define_nodal_support(
"SkewedPinned45",
ux=1,
uy=1,
uz=1,
alpha=math.radians(45), # rotate support axes 45° about Z
)
# ---------------------------------------------------------------------------
# Build a simple four-node frame and apply supports
# ---------------------------------------------------------------------------
with app.model.begin_edit():
app.model.geometry.add_node([
[1, 0.0, 0.0, 0.0],
[2, 5.0, 0.0, 0.0],
[3, 0.0, 0.0, 3.0],
[4, 5.0, 0.0, 3.0],
[5, 2.5, 0.0, 3.0],
])
app.model.geometry.add_member(
[[1, 1, 3], [2, 2, 4], [3, 3, 5], [4, 5, 4]],
material_name="S235",
section_name="IPE 200",
)
# Rigid pinned bases
app.model.supports.apply_node_support(1, "Pinned")
app.model.supports.apply_node_support(2, "Pinned")
# Elastic spring under node 5 (mid-span)
app.model.supports.apply_node_support(5, "SpringX_1000")
08 — Lattice tower¶
Parametric triangular lattice tower with ~160 nodes and ~300 members.
Demonstrates the performance benefit of begin_edit() for bulk geometry
creation, section database loading, and complex member topologies.
"""
Parametric triangular lattice tower.
Footprint: equilateral triangle centered at (0, 0, 0), one vertex pointing +Y.
The circumradius of the cross-section tapers inward with height and is capped at
the upper_segment_width once reached.
Each segment is divided at mid-height by intermediate leg nodes. All three faces
of the tower carry X-bracing in each half-panel (lower X and upper X per segment).
Parameters
----------
base_width : Side length of the equilateral triangle at z=0 [m]
segment_height : Height of one segment [m]
number_of_segments : Total number of segments
upper_segment_width : Minimum side length — maintained once taper reaches it [m]
taper_slope : Circumradius reduction per unit height [m/m]
(5% means each leg moves 5 cm inward per 1 m of height)
Members
-------
Legs and horizontals : MAIN_SECTION (CHS101.6x6)
Cross-bracing : BRACING_SECTION (L100x65x8)
Performance note
----------------
All node, member, and support creation is wrapped in ``app.model.begin_edit()``.
This calls ``BeginMultiOperation`` / ``EndMultiOperation`` around the entire bulk
build, reducing hundreds of individual COM round-trips to a single flush.
For a 7-segment tower (~160 nodes, ~300 members) this roughly halves build time.
NOTE: You need to open a blank Robot model, go for Shell Design or Frame 3D Design structure type.
"Pinned" support label must exist in the model (Robot built-in default).
"""
import math
import pyrobotstructural
# ---------------------------------------------------------------------------
# Parameters
# ---------------------------------------------------------------------------
base_width = 6.1 # Side length of base triangle [m]
segment_height = 6.0 # Height of each segment [m]
number_of_segments = 7 # Total number of segments
upper_segment_width = 2.5 # Minimum (upper) side length [m]
taper_slope = 0.05 # Circumradius reduction per unit height [m/m]
MAIN_SECTION = "CHS 114.3x6"
BRACING_SECTION = "CAI 90x70x8"
MATERIAL = "S235"
# ---------------------------------------------------------------------------
# Initialize
# ---------------------------------------------------------------------------
dll_path = r"C:\Program Files\Autodesk\Robot Structural Analysis Professional 2026\Exe\Interop.RobotOM.dll"
pyrobotstructural.initialize(dll_path)
app = pyrobotstructural.RobotApp()
app.model.management.clear()
# ---------------------------------------------------------------------------
# Load sections from database
# ---------------------------------------------------------------------------
app.model.sections.add_database("CORUS")
app.model.sections.load_from_database(database_name="CORUS", section_name=MAIN_SECTION)
app.model.sections.load_from_database(
database_name="EURO", section_name=BRACING_SECTION
)
# ---------------------------------------------------------------------------
# Geometry helpers
# ---------------------------------------------------------------------------
# Equilateral triangle: circumradius R = side_length / sqrt(3)
base_radius = base_width / math.sqrt(3)
upper_radius = upper_segment_width / math.sqrt(3)
# Leg angles: vertex at +Y, then 120° and 240° further around
LEG_ANGLES = [math.radians(a) for a in (90, 210, 330)]
def radius_at(z: float) -> float:
"""Circumradius of the triangular cross-section at height z."""
return max(base_radius - taper_slope * z, upper_radius)
def leg_xy(z: float, leg: int) -> tuple[float, float]:
r = radius_at(z)
return r * math.cos(LEG_ANGLES[leg]), r * math.sin(LEG_ANGLES[leg])
def node_id(level: int, leg: int) -> int:
"""1-based node ID. level: 0 … 2*N (full and mid), leg: 0, 1, 2."""
return level * 3 + leg + 1
# Total levels: bottom + one mid + one top per segment, shared between segments
# Pattern: [full, mid, full, mid, ..., full] → 2*N + 1 levels
n_levels = 2 * number_of_segments + 1
# ---------------------------------------------------------------------------
# Nodes
# ---------------------------------------------------------------------------
node_data = []
for level in range(n_levels):
z = level * segment_height / 2
for leg in range(3):
x, y = leg_xy(z, leg)
node_data.append([node_id(level, leg), x, y, z])
# ---------------------------------------------------------------------------
# Members
# ---------------------------------------------------------------------------
member_id = 1
leg_members = []
ring_members = []
bracing_members = []
# Leg members — connect each level to the next along the same leg
for level in range(n_levels - 1):
for leg in range(3):
leg_members.append([member_id, node_id(level, leg), node_id(level + 1, leg)])
member_id += 1
# Horizontal ring members — connect the three legs at every level (full and mid)
for level in range(n_levels):
for leg in range(3):
ring_members.append(
[member_id, node_id(level, leg), node_id(level, (leg + 1) % 3)]
)
member_id += 1
# Cross-bracing — X pattern in each half-panel on all three faces
# For each segment:
# lower half: leg_a[bot] → leg_b[mid] and leg_b[bot] → leg_a[mid]
# upper half: leg_a[mid] → leg_b[top] and leg_b[mid] → leg_a[top]
face_pairs = [(0, 1), (1, 2), (2, 0)]
for seg in range(number_of_segments):
level_bot = 2 * seg
level_mid = 2 * seg + 1
level_top = 2 * seg + 2
for a, b in face_pairs:
# Lower X
bracing_members.append(
[member_id, node_id(level_bot, a), node_id(level_mid, b)]
)
member_id += 1
bracing_members.append(
[member_id, node_id(level_bot, b), node_id(level_mid, a)]
)
member_id += 1
# Upper X
bracing_members.append(
[member_id, node_id(level_mid, a), node_id(level_top, b)]
)
member_id += 1
bracing_members.append(
[member_id, node_id(level_mid, b), node_id(level_top, a)]
)
member_id += 1
base_nodes = [node_id(0, leg) for leg in range(3)]
# ---------------------------------------------------------------------------
# Build everything in a single batched multi-operation
# All node, member, and support COM calls are flushed together at __exit__.
# ---------------------------------------------------------------------------
with app.model.begin_edit():
app.model.geometry.add_node(node_data)
app.model.geometry.add_member(
leg_members, section_name=MAIN_SECTION, material_name=MATERIAL
)
app.model.geometry.add_member(
ring_members, section_name=MAIN_SECTION, material_name=MATERIAL
)
app.model.geometry.add_member(
bracing_members, section_name=BRACING_SECTION, material_name=MATERIAL
)
app.model.supports.apply_node_support(node_number=base_nodes, support_name="Pinned")
# ---------------------------------------------------------------------------
# Summary
# ---------------------------------------------------------------------------
total_members = len(leg_members) + len(ring_members) + len(bracing_members)
print("Tower generated successfully.")
print(f" Segments : {number_of_segments}")
print(f" Total height : {number_of_segments * segment_height} m")
print(f" Base width : {base_width} m (circumradius {base_radius:.3f} m)")
print(
f" Top width : {radius_at(number_of_segments * segment_height) * math.sqrt(3):.3f} m"
)
print(f" Nodes : {len(node_data)}")
print(
f" Members : {total_members} "
f"(legs: {len(leg_members)}, rings: {len(ring_members)}, bracing: {len(bracing_members)})"
)
09 — Cladding local coordinate systems¶
Six cladding panel configurations showing dir_x for explicit span direction
control and flip_z for surface normal reversal. Useful for correctly
directing one-way cladding loads.
"""
Example: cladding objects with explicit local coordinate system.
Demonstrates how to use the ``dir_x`` and ``flip_z`` parameters of
``add_cladding`` to control the panel's local axes:
1. Default isotropic cladding (Robot auto-computes local axes).
2. One-way cladding spanning in a specific global direction (via ``dir_x``).
3. Cladding with a flipped normal (``flip_z=True``).
4. One-way cladding with a custom diagonal span direction.
Background
----------
A cladding object's local X axis determines the span direction for
one-way load distribution:
* ``"One-way X"`` — load carried along the local X axis
* ``"One-way Y"`` — load carried perpendicular to local X (along local Y)
* ``"Two-way"`` — isotropic; local X direction has no effect
``dir_x`` accepts any non-zero 3-D vector in global coordinates. The
vector is normalised internally, so only direction matters.
``flip_z=True`` reverses the panel outward normal, effectively swapping
which face is considered the "top" surface.
NOTE: Open a blank Robot model (Frame 3D or Shell Design) before running.
"""
import math
import pyrobotstructural
DLL_PATH = r"C:\Program Files\Autodesk\Robot Structural Analysis Professional 2026\Exe\Interop.RobotOM.dll"
pyrobotstructural.initialize(DLL_PATH)
app = pyrobotstructural.RobotApp()
app.model.management.clear()
# ---------------------------------------------------------------------------
# Helper: build a rectangular contour in the XY plane at a given Z offset
# ---------------------------------------------------------------------------
def rect_contour(x0, y0, x1, y1, z=0.0):
"""Return a four-point contour list for add_cladding."""
return [
[1, x0, y0, z],
[2, x1, y0, z],
[3, x1, y1, z],
[4, x0, y1, z],
]
# ---------------------------------------------------------------------------
# 1. Default two-way cladding — Robot auto-computes local X from first edge
# ---------------------------------------------------------------------------
app.model.geometry.add_cladding(
points=rect_contour(0.0, 0.0, 3.0, 4.0, z=0.0),
load_distribution="Two-way",
)
# ---------------------------------------------------------------------------
# 2. One-way cladding spanning in global X (dir_x = (1, 0, 0))
# Load is distributed along local X, i.e. in the global X direction.
# ---------------------------------------------------------------------------
app.model.geometry.add_cladding(
points=rect_contour(4.0, 0.0, 7.0, 4.0, z=0.0),
load_distribution="One-way X",
dir_x=(1.0, 0.0, 0.0), # local X = global X
)
# ---------------------------------------------------------------------------
# 3. One-way cladding spanning in global Y (dir_x = (0, 1, 0))
# To span in Y we align local X with global Y, then use "One-way X".
# ---------------------------------------------------------------------------
app.model.geometry.add_cladding(
points=rect_contour(8.0, 0.0, 11.0, 4.0, z=0.0),
load_distribution="One-way X",
dir_x=(0.0, 1.0, 0.0), # local X = global Y → spans in Y direction
)
# ---------------------------------------------------------------------------
# 4. Cladding with flipped normal — load collected from the underside
# ---------------------------------------------------------------------------
app.model.geometry.add_cladding(
points=rect_contour(0.0, 5.0, 3.0, 9.0, z=0.0),
load_distribution="Two-way",
flip_z=True,
)
# ---------------------------------------------------------------------------
# 5. One-way cladding with a 45° diagonal span direction
# dir_x does not need to be a unit vector — it is normalised internally.
# ---------------------------------------------------------------------------
app.model.geometry.add_cladding(
points=rect_contour(4.0, 5.0, 7.0, 9.0, z=0.0),
load_distribution="One-way X",
dir_x=(1.0, 1.0, 0.0), # 45° in the XY plane; normalised to (√2/2, √2/2, 0)
)
# ---------------------------------------------------------------------------
# 6. Inclined cladding panel (not in a global plane)
# The panel is tilted; dir_x points along global X regardless.
# ---------------------------------------------------------------------------
inclined_contour = [
[1, 0.0, 10.0, 0.0],
[2, 3.0, 10.0, 0.0],
[3, 3.0, 13.0, 2.0], # Z rises
[4, 0.0, 13.0, 2.0],
]
app.model.geometry.add_cladding(
points=inclined_contour,
load_distribution="One-way X",
dir_x=(1.0, 0.0, 0.0),
)
app.view.view.display(panel_lcs=True)
10 — Custom sections¶
Creates non-standard cross-sections programmatically: circular hollow sections (CHS / tubes), solid rectangular sections, and rectangular hollow sections (RHS). Also demonstrates assigning different sections to individual bars after creation.
"""
Example: custom tube (CHS) and rectangular (solid / RHS) sections.
Three custom sections are created and applied to bars of a simple portal frame:
Bar 1 (column left) → CHS 168.3x10 (tube section, D=168.3 mm, t=10 mm)
Bar 2 (column right) → CHS 168.3x10
Bar 3 (beam) → RECT 200x100 (solid rectangular section)
Bar 4 (diagonal) → RHS 150x100x6 (hollow rectangular section)
Unit conventions
----------------
create_tube_section : diameter and thickness in **meters**
create_rect_section : height, width and thickness in **millimeters**
NOTE: Open a blank Robot model (Frame 3D Design) before running.
A "Fixed" support label must exist (Robot built-in default).
"""
import pyrobotstructural
dll_path = r"C:\Program Files\Autodesk\Robot Structural Analysis Professional 2026\Exe\Interop.RobotOM.dll"
pyrobotstructural.initialize(dll_path)
app = pyrobotstructural.RobotApp()
app.model.management.clear()
# ---------------------------------------------------------------------------
# Geometry — simple portal frame with a diagonal brace
# ---------------------------------------------------------------------------
#
# 3 -------- 4
# | \ |
# | \ |
# 1 2
#
nodes = [
[1, 0.0, 0.0, 0.0], # base left
[2, 4.0, 0.0, 0.0], # base right
[3, 0.0, 0.0, 3.0], # top left
[4, 4.0, 0.0, 3.0], # top right
]
bars = [
[1, 1, 3], # column left
[2, 2, 4], # column right
[3, 3, 4], # beam
[4, 1, 4], # diagonal brace
]
with app.model.begin_edit():
app.model.geometry.add_node(nodes)
# Temporary section; will be overwritten per bar below
app.model.geometry.add_member(bars, material_name="S235", section_name="IPE 100")
app.model.supports.apply_node_support(node_number=[1, 2], support_name="Fixed")
# ---------------------------------------------------------------------------
# Custom sections
# ---------------------------------------------------------------------------
# CHS 168.3x10 — diameter 168.3 mm = 0.1683 m, wall thickness 10 mm = 0.010 m
app.model.sections.create_tube_section(
name="CHS 168.3x10",
diameter=0.1683,
thickness=0.010,
material="S235",
)
# Solid rectangle 200 mm (H) × 100 mm (W)
app.model.sections.create_rect_section(
name="RECT 200x100",
height=200,
width=100,
material="S235",
)
# Hollow RHS 150 mm (H) × 100 mm (W), wall thickness 6 mm
app.model.sections.create_rect_section(
name="RHS 150x100x6",
height=150,
width=100,
thickness=6,
material="S235",
)
# ---------------------------------------------------------------------------
# Assign sections to bars
# ---------------------------------------------------------------------------
app.model.sections.apply_section_to_bar(1, "CHS 168.3x10")
app.model.sections.apply_section_to_bar(2, "CHS 168.3x10")
app.model.sections.apply_section_to_bar(3, "RECT 200x100")
app.model.sections.apply_section_to_bar(4, "RHS 150x100x6")
print("Sections created and assigned successfully.")
11 — Timber roof truss¶
Full parametric timber roof truss: geometry generation, material and section assignment, load cases, combinations, calculation, and result extraction.
"""
Timber Roof Truss — Parametric 2D Frame
========================================
Geometry
--------
Symmetric Pratt-style truss in the XZ plane (Y = 0 for all nodes):
Ridge ──────────────────────────────
/ \\ /|\\ / |/ \\ | / |\\ /
/ X | X | X | X \\
/ / \\ | / \\ | / \\ |/ \\ \\
●━━━━━━━━●━━━━━━━━●━━━━━━━━●━━━━━━━━●
↑ Pinned Roller ↑
Diagonal pattern (Pratt): end panels are already closed triangles;
interior panels each get one diagonal:
• Left half panels: diagonal from bottom-right corner → top-left corner
• Right half panels: diagonal from bottom-left corner → top-right corner
Under dominant gravity loads these diagonals are in tension.
Member groups:
• Bottom chord (tie beam) — horizontal, C24 timber, BC_140x220
• Top chord (rafters) — sloped, C24 timber, TC_120x200
• Verticals (posts) — plumb, C24 timber, V_100x160
• Diagonals — C24 timber, D_80x140
Load cases
----------
1 Self-weight (SW) — structure self-weight in -Z
2 Permanent load (PL) — roofing, ceiling, etc. on top chord in -Z
3 Live load (LL) — maintenance load on top chord in -Z
4 Snow (S) — snow on horizontal projection of top chord
5 Wind_W (Ww) — wind from left (windward left rafter, suction right)
6 Wind_E (We) — wind from right (windward right rafter, suction left)
Wind model (EN 1991-1-4 simplified, duopitch roof):
Force per unit length on rafter = −Cpe · q_wind · tributary · n̂_outward
Left rafter outward normal n̂_L = (−sin α, 0, cos α)
Right rafter outward normal n̂_R = ( sin α, 0, cos α)
Cpe windward = +0.8 (pressure), Cpe leeward = −0.4 (suction)
ULS/SLS combinations (EN 1990 Eq. 6.10):
ULS-1: 1.35·G + 1.5·LL
ULS-2: 1.35·G + 1.5·S + 0.9·LL
ULS-3: 1.35·G + 1.5·Ww + 0.9·S
ULS-4: 1.35·G + 1.5·We + 0.9·S
SLS-1: 1.0·G + 1.0·LL
SLS-2: 1.0·G + 1.0·S
NOTE: Open a blank Robot model (Frame 2D Design) before running.
The built-in "Pinned" support label must exist in the model.
"""
import math
import pyrobotstructural
from pyrobotstructural.enums import CaseNature, CaseAnalizeType, CombinationType
# ===========================================================================
# USER PARAMETERS ← adjust to suit your project
# ===========================================================================
SPAN = 12.0 # Truss span [m]
N_PANELS = 4 # Number of panels along span (must be even for symmetry)
SLOPE_DEG = 20.0 # Roof slope [degrees]
TRIBUTARY = 5.0 # Spacing between trusses (tributary width) [m]
# Solid rectangular timber cross-sections: (section_name, height_mm, width_mm)
# Sizes proposed for C24 timber, 12 m span, 5 m spacing.
# Adjust based on verification results.
SEC_TOP_CHORD = ("TC_120x200", 200, 120) # rafter: 120 mm wide × 200 mm deep
SEC_BOT_CHORD = ("BC_140x220", 220, 140) # tie beam: 140 mm wide × 220 mm deep
SEC_VERTICAL = ("V_100x160", 160, 100) # post: 100 mm wide × 160 mm deep
SEC_DIAGONAL = ("D_80x140", 140, 80) # diagonal: 80 mm wide × 140 mm deep
MATERIAL = "C24"
# Characteristic load intensities
PERM_kPa = 1.0 # Permanent load (roofing, insulation, ceiling) [kN/m²]
LIVE_kPa = 0.4 # Live/maintenance load [kN/m²]
SNOW_kPa = 1.5 # Snow load on horizontal projection [kN/m²]
WIND_kPa = 0.6 # Reference wind pressure [kN/m²]
DLL_PATH = (
r"C:\Program Files\Autodesk\Robot Structural Analysis Professional 2026"
r"\Exe\Interop.RobotOM.dll"
)
# ===========================================================================
# DERIVED GEOMETRY
# ===========================================================================
slope_rad = math.radians(SLOPE_DEG)
sin_a = math.sin(slope_rad)
cos_a = math.cos(slope_rad)
dx = SPAN / N_PANELS # panel length along bottom chord [m]
h_ridge = (SPAN / 2) * math.tan(slope_rad) # ridge height [m]
def top_height(x: float) -> float:
"""Top-chord elevation at horizontal position x from left support."""
return (SPAN / 2 - abs(x - SPAN / 2)) * math.tan(slope_rad)
# --- Node coordinates ---
node_data = []
_nid = 1
bottom_ids: list[int] = [] # node IDs along bottom chord (left → right)
for i in range(N_PANELS + 1):
node_data.append([_nid, i * dx, 0.0, 0.0])
bottom_ids.append(_nid)
_nid += 1
top_ids: list[int] = [
bottom_ids[0]
] # top-chord node IDs; ends shared with bottom chord
for i in range(1, N_PANELS):
x = i * dx
node_data.append([_nid, x, 0.0, top_height(x)])
top_ids.append(_nid)
_nid += 1
top_ids.append(bottom_ids[-1])
# --- Member topology ---
_mid = 1
bot_chord_data: list[list[int]] = []
top_chord_data: list[list[int]] = []
vertical_data: list[list[int]] = []
diagonal_data: list[list[int]] = []
bot_chord_ids: list[int] = []
top_chord_ids: list[int] = []
top_left_ids: list[int] = [] # left rafter members (for wind loads)
top_right_ids: list[int] = [] # right rafter members
vertical_ids: list[int] = []
diagonal_ids: list[int] = []
half = N_PANELS // 2
for i in range(N_PANELS):
bot_chord_data.append([_mid, bottom_ids[i], bottom_ids[i + 1]])
bot_chord_ids.append(_mid)
_mid += 1
for i in range(N_PANELS):
top_chord_data.append([_mid, top_ids[i], top_ids[i + 1]])
top_chord_ids.append(_mid)
(top_left_ids if i < half else top_right_ids).append(_mid)
_mid += 1
for i in range(1, N_PANELS):
if bottom_ids[i] != top_ids[i]: # skip if nodes coincide (at supports)
vertical_data.append([_mid, bottom_ids[i], top_ids[i]])
vertical_ids.append(_mid)
_mid += 1
# Pratt diagonals — one per interior panel (panels 1 … N_PANELS-2).
# End panels (0 and N_PANELS-1) are already closed triangles formed by the
# top chord + vertical + bottom chord; no additional diagonal is needed there.
#
# Left half (i < half): bottom-right corner → top-left corner of the panel
# → diagonal slopes toward the support (in tension under gravity)
# Right half (i ≥ half): bottom-left corner → top-right corner of the panel
# → mirror image (symmetric)
for i in range(1, N_PANELS - 1):
if i < half:
n_start, n_end = bottom_ids[i + 1], top_ids[i]
else:
n_start, n_end = bottom_ids[i], top_ids[i + 1]
diagonal_data.append([_mid, n_start, n_end])
diagonal_ids.append(_mid)
_mid += 1
def _ids(lst: list[int]) -> str:
"""Convert a list of IDs to the space-separated string Robot expects."""
return " ".join(str(n) for n in lst)
# ===========================================================================
# INITIALIZE
# ===========================================================================
pyrobotstructural.initialize(DLL_PATH)
app = pyrobotstructural.RobotApp()
app.model.management.clear()
# ===========================================================================
# SECTIONS (must exist before add_member references them)
# ===========================================================================
for sec_name, height_mm, width_mm in (
SEC_TOP_CHORD,
SEC_BOT_CHORD,
SEC_VERTICAL,
SEC_DIAGONAL,
):
app.model.sections.create_rect_section(
name=sec_name,
height=height_mm,
width=width_mm,
material=MATERIAL,
)
# ===========================================================================
# SUPPORT LABELS (must be defined outside begin_edit)
# ===========================================================================
# Roller: free in X (horizontal), fixed in Y (out-of-plane) and Z (vertical)
app.model.supports.define_nodal_support(
name="Roller", ux=0, uy=1, uz=1, rx=0, ry=0, rz=0
)
# ===========================================================================
# GEOMETRY (batched into a single multi-operation flush)
# ===========================================================================
with app.model.begin_edit():
app.model.geometry.add_node(node_data)
# Each member group uses its own section directly
app.model.geometry.add_member(
top_chord_data, section_name=SEC_TOP_CHORD[0], material_name=MATERIAL
)
app.model.geometry.add_member(
bot_chord_data, section_name=SEC_BOT_CHORD[0], material_name=MATERIAL
)
if vertical_data:
app.model.geometry.add_member(
vertical_data, section_name=SEC_VERTICAL[0], material_name=MATERIAL
)
if diagonal_data:
app.model.geometry.add_member(
diagonal_data, section_name=SEC_DIAGONAL[0], material_name=MATERIAL
)
# Supports: pinned left, roller right
app.model.supports.apply_node_support(
node_number=bottom_ids[0], support_name="Pinned"
)
app.model.supports.apply_node_support(
node_number=bottom_ids[-1], support_name="Roller"
)
# ===========================================================================
# LOAD CASES
# ===========================================================================
app.loads.cases.add_loadcase(
name="Self-weight",
nature=CaseNature.PERMANENT,
analize_type=CaseAnalizeType.STATIC_LINEAR,
number=1,
label="SW",
)
app.loads.cases.add_loadcase(
name="Permanent load",
nature=CaseNature.PERMANENT,
analize_type=CaseAnalizeType.STATIC_LINEAR,
number=2,
label="PL",
)
app.loads.cases.add_loadcase(
name="Live load",
nature=CaseNature.EXPLOITATION,
analize_type=CaseAnalizeType.STATIC_LINEAR,
number=3,
label="LL",
)
app.loads.cases.add_loadcase(
name="Snow",
nature=CaseNature.SNOW,
analize_type=CaseAnalizeType.STATIC_LINEAR,
number=4,
label="S",
)
app.loads.cases.add_loadcase(
name="Wind_W",
nature=CaseNature.WIND,
analize_type=CaseAnalizeType.STATIC_LINEAR,
number=5,
label="Ww",
)
app.loads.cases.add_loadcase(
name="Wind_E",
nature=CaseNature.WIND,
analize_type=CaseAnalizeType.STATIC_LINEAR,
number=6,
label="We",
)
# ===========================================================================
# LOADS
# ===========================================================================
# Line intensities [N/m] = pressure [kN/m²] × 1 000 × tributary [m]
q_perm = PERM_kPa * 1_000 * TRIBUTARY
q_live = LIVE_kPa * 1_000 * TRIBUTARY
q_snow = SNOW_kPa * 1_000 * TRIBUTARY
q_wind = WIND_kPa * 1_000 * TRIBUTARY
all_top = _ids(top_chord_ids)
left_top = _ids(top_left_ids)
right_top = _ids(top_right_ids)
# 1 — Self-weight (structure dead load, -Z direction)
app.loads.load.add_self_weight(
case_name="Self-weight", objects="all", factors=[0, 0, -1]
)
# 2 — Permanent load: vertical uniform load on top chord (roofing, ceiling, etc.)
app.loads.load.add_uniform_load(
case_name="Permanent load",
objects=all_top,
loads=[0, 0, -q_perm, 0, 0, 0],
)
# 3 — Live load: vertical uniform load on top chord (maintenance)
app.loads.load.add_uniform_load(
case_name="Live load",
objects=all_top,
loads=[0, 0, -q_live, 0, 0, 0],
)
# 4 — Snow: vertical projected load on top chord (acts on horizontal projection)
app.loads.load.add_uniform_load(
case_name="Snow",
objects=all_top,
loads=[0, 0, -q_snow, 0, 0, 0],
projected=1,
)
# 5 — Wind_W (wind from left, +X direction)
#
# Force per unit member length = −Cpe · q_wind · n̂_outward
# Left rafter n̂_L = (−sin α, 0, cos α) → Fx = Cpe · q · sin α, Fz = −Cpe · q · cos α
# Right rafter n̂_R = ( sin α, 0, cos α) → Fx = −Cpe · q · sin α, Fz = −Cpe · q · cos α
Cpe_wind = 0.8 # windward pressure coefficient (positive = into surface)
Cpe_lee = -0.4 # leeward suction coefficient (negative = away from surface)
app.loads.load.add_uniform_load(
case_name="Wind_W",
objects=left_top,
loads=[
Cpe_wind * q_wind * sin_a, # Fx [N/m] rightward pressure component
0,
-Cpe_wind * q_wind * cos_a, # Fz [N/m] downward pressure component
0,
0,
0,
],
)
app.loads.load.add_uniform_load(
case_name="Wind_W",
objects=right_top,
loads=[
-Cpe_lee * q_wind * sin_a, # Fx [N/m] rightward (leeward suction)
0,
-Cpe_lee * q_wind * cos_a, # Fz [N/m] upward (uplift / suction)
0,
0,
0,
],
)
# 6 — Wind_E (wind from right, −X direction) — mirror of Wind_W
app.loads.load.add_uniform_load(
case_name="Wind_E",
objects=right_top,
loads=[
-Cpe_wind * q_wind * sin_a, # Fx [N/m] leftward pressure component
0,
-Cpe_wind * q_wind * cos_a, # Fz [N/m] downward pressure component
0,
0,
0,
],
)
app.loads.load.add_uniform_load(
case_name="Wind_E",
objects=left_top,
loads=[
Cpe_lee * q_wind * sin_a, # Fx [N/m] leftward (leeward suction)
0,
-Cpe_lee * q_wind * cos_a, # Fz [N/m] upward (uplift / suction)
0,
0,
0,
],
)
# ===========================================================================
# COMBINATIONS (EN 1990 Eq. 6.10 — ultimate and serviceability)
# G = SW (1) + PL (2), variable actions: LL (3), S (4), Ww (5), We (6)
# ===========================================================================
app.loads.combinations.add_combination(
comb_number=7,
comb_name="ULS-1 (LL dom.)",
label="ULS1",
comb_type=CombinationType.ULS,
case_analize_type=CaseAnalizeType.COMB_LINEAR,
case_nature=CaseNature.EXPLOITATION,
factors=[(1, 1.35), (2, 1.35), (3, 1.5)],
)
app.loads.combinations.add_combination(
comb_number=8,
comb_name="ULS-2 (Snow dom.)",
label="ULS2",
comb_type=CombinationType.ULS,
case_analize_type=CaseAnalizeType.COMB_LINEAR,
case_nature=CaseNature.SNOW,
factors=[(1, 1.35), (2, 1.35), (4, 1.5), (3, 0.9)],
)
app.loads.combinations.add_combination(
comb_number=9,
comb_name="ULS-3 (Wind_W dom.)",
label="ULS3",
comb_type=CombinationType.ULS,
case_analize_type=CaseAnalizeType.COMB_LINEAR,
case_nature=CaseNature.WIND,
factors=[(1, 1.35), (2, 1.35), (5, 1.5), (4, 0.9)],
)
app.loads.combinations.add_combination(
comb_number=10,
comb_name="ULS-4 (Wind_E dom.)",
label="ULS4",
comb_type=CombinationType.ULS,
case_analize_type=CaseAnalizeType.COMB_LINEAR,
case_nature=CaseNature.WIND,
factors=[(1, 1.35), (2, 1.35), (6, 1.5), (4, 0.9)],
)
app.loads.combinations.add_combination(
comb_number=11,
comb_name="SLS-1 (LL char.)",
label="SLS1",
comb_type=CombinationType.SLS,
case_analize_type=CaseAnalizeType.COMB_LINEAR,
case_nature=CaseNature.EXPLOITATION,
factors=[(1, 1.0), (2, 1.0), (3, 1.0)],
)
app.loads.combinations.add_combination(
comb_number=12,
comb_name="SLS-2 (Snow char.)",
label="SLS2",
comb_type=CombinationType.SLS,
case_analize_type=CaseAnalizeType.COMB_LINEAR,
case_nature=CaseNature.SNOW,
factors=[(1, 1.0), (2, 1.0), (4, 1.0)],
)
# ===========================================================================
# CALCULATE
# ===========================================================================
app.calculate(ignore_warnings=True)
# ===========================================================================
# SUMMARY
# ===========================================================================
total_members = (
len(top_chord_ids) + len(bot_chord_ids) + len(vertical_ids) + len(diagonal_ids)
)
print("Timber roof truss generated and calculated successfully.")
print(f" Span : {SPAN} m")
print(f" Slope : {SLOPE_DEG}° (ridge height {h_ridge:.3f} m)")
print(f" Panels : {N_PANELS} (panel width {dx:.3f} m)")
print(f" Tributary width: {TRIBUTARY} m")
print(f" Nodes : {len(node_data)}")
print(
f" Members : {total_members}"
f" (top chord: {len(top_chord_ids)}, bottom chord: {len(bot_chord_ids)},"
f" verticals: {len(vertical_ids)}, diagonals: {len(diagonal_ids)})"
)
print(f" Sections:")
print(
f" Top chord — {SEC_TOP_CHORD[0]} ({SEC_TOP_CHORD[2]} × {SEC_TOP_CHORD[1]} mm, {MATERIAL})"
)
print(
f" Bot chord — {SEC_BOT_CHORD[0]} ({SEC_BOT_CHORD[2]} × {SEC_BOT_CHORD[1]} mm, {MATERIAL})"
)
print(
f" Verticals — {SEC_VERTICAL[0]} ({SEC_VERTICAL[2]} × {SEC_VERTICAL[1]} mm, {MATERIAL})"
)
print(
f" Diagonals — {SEC_DIAGONAL[0]} ({SEC_DIAGONAL[2]} × {SEC_DIAGONAL[1]} mm, {MATERIAL})"
)
print(f" Load intensities (line loads after tributary {TRIBUTARY} m):")
print(f" Permanent : {q_perm / 1000:.1f} kN/m")
print(f" Live : {q_live / 1000:.1f} kN/m")
print(f" Snow : {q_snow / 1000:.1f} kN/m (projected)")
print(
f" Wind ref. : {q_wind / 1000:.1f} kN/m (Cpe_wind={Cpe_wind}, Cpe_lee={Cpe_lee})"
)