OpenSees Cloud

OpenSees AMI

Rotated Local Axes

Original Post - 21 Sep 2025 - Michael H. Scott

Show your support at Buy Me a Coffee.


OpenSeesing through the SeismoStruct Verification Report (v2025), I expected smooth sailing across Chapter 2, Comparison with Independent Hand-Calcs, where “hand-calcs” means SAP2000 analysis results. But light storm clouds set in on Example 2, Rotated Local Axes.

The model is a W12x106 cantilever rotated 30 degrees about its longitudinal axis. A uniform distributed load (roughly the member self-weight) is applied in the global Y-direction.

Both the model definition and the distributed loading can get a little tricky in OpenSees.

Model Definition

Due to the rotated axes, the cantilever will deflect out-of-plane under in-plane loading. So we have to define a 3D model. No need to alter the section properties though, just rotate the vector in the x-z plane, vxz by 30 degrees from [0,0,1] to [0,sin 30,cos 30].

import openseespy.opensees as ops
import numpy as np
from math import isclose

kip = 1.0
inch = 1.0
ksi = kip/inch**2

E = 29000*ksi
nu = 0.3
G = 0.5*E/(1+nu)

# W12x106
A = 31.2*inch**2
Iz = 933*inch**4
Iy = 301*inch**4
J = 9.13*inch**4

theta = 30*np.pi/180

ops.wipe()
ops.model('basic','-ndm',3,'-ndf',6)

ops.node(1,0,0,0); ops.fix(1,1,1,1,1,1,1)
ops.node(2,144*inch,0,0)

ops.geomTransf('Linear',1, 0,np.sin(theta),np.cos(theta))

ops.element('elasticBeamColumn',1,1,2,A,E,G,J,Iy,Iz,1)

Note that the model does not include either shear or warping effects.

Distributed Load

Member loads in OpenSees are defined with respect to the element local axes. Some day you’ll be able to input member loads defined in the global axis directions. Until then, you can use dot products.

First, the global distributed load is defined in a vector, W, then this vector is projected on to each of the element local axes to get the local distributed loads.

W = [0,-0.01*kip/inch,0] # Global X,Y,Z

# Element local axes
x = ops.eleResponse(1,'xaxis')
y = ops.eleResponse(1,'yaxis')
z = ops.eleResponse(1,'zaxis')

# Dot products
wx = np.dot(W,x)
wy = np.dot(W,y)
wz = np.dot(W,z)

ops.timeSeries('Constant',1)
ops.pattern('Plain',1,1)
ops.eleLoad('-ele',1,'-type','beamUniform',wy,wz,wx)

ops.analysis('Static','-noWarnings')
ops.analyze(1)

Dot products are a bit overkill for this model, but can get you out of a jam in less simplistic situations.

Assertions

The reported hand-calcs for the deflection at the free end of the cantilever are 0.03029 inch in Y and 0.01806 inch in Z.

assert isclose(-0.03029*inch,ops.nodeDisp(2,2),abs_tol=0.5e-5*inch)
assert isclose(-0.01806*inch,ops.nodeDisp(2,3),abs_tol=0.5e-5*inch)

The absolute tolerance for the assertions is 0.5e-5 inch because the hand-calcs are reported to five decimal places. For example, the first assertion checks that the Y deflection is between -0.030285 and -0.030295 inch, i.e., any value that rounds to -0.03029 inch.

If I run into any issues with the SeismoStruct verficiation examples, or find more OpenSees modeling issues that make me think “that was kinda weird, others should know about it”, I’ll let you know.