OpenSees Cloud
OpenSees AMI
More Than One Way to Heat Up a Truss
03 Mar 2026 - Michael H. Scott
OpenSees can solve every reasonable problem from any structural analysis textbook. But I nearly had a meltdown over a simple truss problem because truss elements in OpenSees do not directly support thermal loading.
Consider Example 6.17, shown below, from J.C. Smith’s Structural Analysis. All members of the truss have E=29000 ksi, A=10 inch2, and \(\alpha\)=6.5e-6 (coefficient of thermal expansion, strain per degree Fahrenheit). The three members indicated in the figure are subjected to a temperature increase of 100 degrees F.

The textbook solution for the displacement of the indicated joint is 0.8970 inch down and 0.4485 inch to the right. In addition, because the truss model is statically determinate, the temperature change should not induce any member forces or external reactions–only displacements.
Because truss elements in OpenSees cannot handle thermal loading (it’s not difficult, just the developers of the thermal modules were focused on frame elements), workarounds are required in order to analyze trusses subjected to temperature changes.
Among workarounds, there is an easy way involving initial strains and a hard way forcing beam elements to behave like truss elements.
The Easy Way
Thermal strain can be treated as an initial strain, so we can wrap an
Elastic material with an InitStrain material wrapper where the
initial strain is \(\varepsilon_o=-\alpha \Delta T\). The InitStrain
wrapper will send \((\varepsilon-\varepsilon_o)\) to its wrapped
material, i.e., \(\varepsilon_o\) is the stress-free strain, so the
negative sign on the change in temperature is correct.
An OpenSeesPy script is shown below for the truss analysis with initial strain wrappers for the heated members.
import openseespy.opensees as ops
from numpy import isclose
kip = 1
inch = 1
ft = 12*inch
ksi = kip/inch**2
ops.wipe()
ops.model('basic','-ndm',2,'-ndf',2)
ops.node(1,0,0); ops.fix(1,1,1)
ops.node(2,30*ft,0)
ops.node(3,30*ft,30*ft)
ops.node(4,60*ft,0); ops.fix(4,0,1)
ops.node(5,60*ft,22.5*ft)
ops.node(6,90*ft,15*ft)
E = 29000*ksi
alpha = 6.5e-6
A = 10*inch**2
dT = 100
ops.uniaxialMaterial('Elastic',1,E)
ops.uniaxialMaterial('InitStrain',2,1,-alpha*dT)
ops.element('truss',1,1,2,A,1)
ops.element('truss',2,2,4,A,1)
ops.element('truss',3,4,6,A,1)
ops.element('truss',4,1,3,A,2)
ops.element('truss',5,3,5,A,2)
ops.element('truss',6,5,6,A,2)
ops.element('truss',7,3,4,A,1)
ops.element('truss',8,2,3,A,1)
ops.element('truss',9,4,5,A,1)
ops.analysis('Static','-noWarnings')
ops.analyze(1)
ops.reactions()
# Verify nodal displacement against textbook solution
ux,uy = ops.nodeDisp(6)
assert isclose( 0.44850,ux/inch,rtol=1e-5)
assert isclose(-0.89700,uy/inch,rtol=1e-5)
# Verify all member forces are zero
for ele in ops.getEleTags():
assert isclose(0,ops.basicForce(ele)).all()
# Verify all reactions are zero
for nd in ops.getNodeTags():
assert isclose(0,ops.nodeReaction(nd)).all()
All assertions on joint displacements, member forces, and external reactions should pass. Try it yourself.
The Hard Way
Here’s how to analyze the truss using beam elements with thermal loading capabilities built directly into OpenSees:
- Change the model builder to three DOFs/node to accommodate beam elements
- Fix all rotational DOFs to satisfy the truss modeling assumption
- Define an
ElasticThermaluniaxial material withEandalpha - Define a
FiberThermalsection with one fiber- We do not want flexural response from the section
- One fiber ensures flexural stiffness will be zero
- Define
dispBeamColumnThermalelements- Use
Lineargeometric transformation andLegendrebeam integration - If we used
forceBeamColumnThermal, the element state determination will fail because the section has only one fiber
- Use
- Use a
-beamThermalelement load for the heated elements- The reference locations for temperature gradient through the section depth are irrelevant
- But, the locations have to be non-zero with negative (bottom of section) and positive (top of section) values
An OpenSees Python script using the thermal elements and constitutive models for the truss analysis is shown below with annotations for the points listed above.
import openseespy.opensees as ops
from numpy import isclose
kip = 1
inch = 1
ft = 12*inch
ksi = kip/inch**2
# 1.
ops.wipe()
ops.model('basic','-ndm',2,'-ndf',3)
ops.node(1,0,0); ops.fix(1,1,1,0)
ops.node(2,30*ft,0)
ops.node(3,30*ft,30*ft)
ops.node(4,60*ft,0); ops.fix(4,0,1,0)
ops.node(5,60*ft,22.5*ft)
ops.node(6,90*ft,15*ft)
# 2.
for nd in ops.getNodeTags():
ops.fix(nd,0,0,1)
E = 29000*ksi
alpha = 6.5e-6
A = 10*inch**2
dT = 100
# 3.
ops.uniaxialMaterial('ElasticThermal',1,E,alpha)
# 4.
ops.section('FiberThermal',1)
ops.fiber(0,0,A,1)
ops.beamIntegration('Legendre',1,1,2)
ops.geomTransf('Linear',1)
# 5.
ops.element('dispBeamColumnThermal',1,1,2,1,1)
ops.element('dispBeamColumnThermal',2,2,4,1,1)
ops.element('dispBeamColumnThermal',3,4,6,1,1)
ops.element('dispBeamColumnThermal',4,1,3,1,1)
ops.element('dispBeamColumnThermal',5,3,5,1,1)
ops.element('dispBeamColumnThermal',6,5,6,1,1)
ops.element('dispBeamColumnThermal',7,3,4,1,1)
ops.element('dispBeamColumnThermal',8,2,3,1,1)
ops.element('dispBeamColumnThermal',9,4,5,1,1)
# 6.
ops.timeSeries('Constant',1)
ops.pattern('Plain',1,1)
ops.eleLoad('-ele',4,5,6,'-type','-beamThermal',dT,-1,dT,1)
ops.analysis('Static','-noWarnings')
ops.analyze(1)
ops.reactions()
# Verify nodal displacement against textbook solution
ux,uy,_ = ops.nodeDisp(6)
assert isclose( 0.44850,ux/inch,rtol=1e-5)
assert isclose(-0.89700,uy/inch,rtol=1e-5)
# Verify all member forces are zero
for ele in ops.getEleTags():
assert isclose(0,ops.basicForce(ele)).all()
# Verify all reactions are zero
for nd in ops.getNodeTags():
assert isclose(0,ops.nodeReaction(nd)).all()
Again, all assertions should pass.
Although the model with beam elements and thermal loading is not many more lines than the truss model with initial strains, the cognitive load is much higher.