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.