OpenSees Cloud
OpenSees AMI
How to Use pytest with OpenSees
Original Post - 30 Aug 2025 - Michael H. Scott
Visit Structural Analysis Is Simple on Substack.
Despite plentiful constitutive models and analysis options, testing and verifying OpenSees has been quite limited. At this point, going back and testing all the contributions from the last 25 years is a nearly insurmountable task.
But, as the saying goes:
The best time to start testing OpenSees was 25 years ago.
The second best time is now.
And if we do start testing OpenSees, the
pytest
Python package
will be a big help.
pip install pytest
To start using pytest
, define a script whose filename begins with
test_
, e.g., test_some-stuff.py
, then in that script put assertions
inside functions whose names also begin with test_
.
For example, in the test_particle-dynamics.py
script below, there are
three test_
functions, one each for the acceleration, velocity, and
displacement of a particle subjected to a ramp force (linearly
increasing acceleration). Each test function calls the
perform_analysis
function to define the model and run the analysis.
Tracking down failed tests is easier with one assertion per test
function as opposed to multiple assertions per function.
import openseespy.opensees as ops
from math import isclose
m = 1.0
F = 1.0
t = 1.0
a = F/m
def perform_analysis():
ops.wipe()
ops.model('basic','-ndm',1,'-ndf',1)
ops.node(1,0); ops.mass(1,m)
ops.timeSeries('Linear',1)
ops.pattern('Plain',1,1)
ops.load(1,F)
ops.analysis('Transient','-noWarnings')
Nsteps = 10
dt = t/Nsteps
ops.analyze(Nsteps,dt)
def test_acceleration():
perform_analysis()
assert isclose(ops.nodeAccel(1,1),a)
def test_velocity():
perform_analysis()
assert isclose(ops.nodeVel(1,1),0.5*a*t)
def test_displacement():
perform_analysis()
assert isclose(ops.nodeDisp(1,1),a*t*t/6)
Now run pytest
from the command line.
We see that 2 tests passed and 1 test failed–the assertion in the
test_displacement()
function. The output is not super friendly, but
it’s saying OpenSees computed 0.1675
for the displacement while the
expected answer is 1.0/6
.
Did we just find the Holy Grail, a simple dynamics problem that OpenSees cannot solve?
No, Indiana. The acceleration is linear but the default time integration assumes constant acceleration over each time step.
Change the Newmark \(\beta\) parameter to 1.0/6
for linear acceleration.
ops.integrator('Newmark',0.5,1.0/6)
ops.analysis('Transient','-noWarnings')
Like all the 0.85
factors in reinforced concrete design, let’s be
clear that this 1.0/6
is different from the 1.0/6
in the expected
answer for the displacement.
Anyway, running pytest
again, we see that all 3 tests passed.
Tests of particle dynamics are great, but not all that useful for testing the numerous element formulations, constitutive models, and analysis options in OpenSees.
Much of testing the element formulations in OpenSees will be verifications of closed-form solutions for linear-elastic constitutive response. There are a few closed-form solutions for material and geometric nonlinearity, as well as published benchmarks, but I don’t want to get into those right now.
Consider another test script, test_force-beam-column.py
, that performs
simple checks on the beloved forceBeamColumn
element. The model is a
cantilever with linear-elastic sections at three Gauss-Lobatto points
and a transverse load at the free end. We can compare the deflection and
rotation at the free end and the reactions at the fixed end to
well-known solutions.
import openseespy.opensees as ops
from math import isclose
L = 48
E = 29000
A = 20
I = 800
P = 10
def end_loaded_cantilever():
ops.wipe()
ops.model('basic','-ndm',2,'-ndf',3)
ops.node(1,0,0); ops.fix(1,1,1,1)
ops.node(2,L,0)
ops.section('Elastic',1,E,A,I)
ops.beamIntegration('Lobatto',1,1,3)
ops.geomTransf('Linear',1)
ops.element('forceBeamColumn',1,1,2,1,1)
ops.timeSeries('Constant',1,1)
ops.pattern('Plain',1,1)
ops.load(2,0,P,0)
ops.analysis('Static','-noWarnings')
ops.analyze(1)
ops.reactions()
def test_deflection():
end_loaded_cantilever()
assert isclose(ops.nodeDisp(2,2),P*L**3/(3*E*I))
def test_rotation():
end_loaded_cantilever()
assert isclose(ops.nodeDisp(2,3),P*L**2/(2*E*I))
def test_force():
end_loaded_cantilever()
assert isclose(ops.nodeReaction(1,2),-P)
def test_moment():
end_loaded_cantilever()
assert isclose(ops.nodeReaction(1,3),-P*L)
Again, we keep one assertion per test function. pytest
will gobble up
all the test_
functions in all the test_*.py
scripts in the current
directory.
All 7 tests passed. Yes, we can use elastic sections in nonlinear beam-column elements!
To get a little more detail from pytest
, use the -v
verbose option.
There are several more options and features of pytest
that are not
covered here. Check out the
documentation
to learn more.
I added the two test scripts from this post to the OpenSees/tests/
directory via
PR #1654.
Feel free to make PRs adding your own tests to this directory. The more
minimal the tests, the better.
You’ll be testing the waters of OpenSees. You won’t find any sharks, but you will find a few garbage patches.