If you coded a UniaxialMaterial or any other constitutive model or element in OpenSees, you had to implement pure virtual methods commitState(), revertToLastCommit(), and revertToStart(). The behavior of each method is pretty clear:
- commitState() – After convergence of the global solution at the current time step, get the material ready for the next time step. This typically involves swapping history variables from “trial” to “committed”, from values at tn+1 to tn.
- revertToLastCommit() – If the global solution fails at the current time step, make sure the material goes back to its last committed state at tn. Wipe out the current trial state and any uncommitted state variables.
- revertToStart() – Reset the material back to its initial state at t=0.
The commitState() method is called at the end of each simulation time step. If this method is implemented incorrectly, you’ll see odd results in the material’s cyclic stress-strain response. Your manuscript on fragility curves using Concrete24 will be in limbo until you fix the issue.
And it’s straightforward to test revertToStart(), which is invoked by the reset() command. Without calling wipe(), repeat an analysis many times calling reset() before each run and ensure the results do not change between runs. A common culprit here is uninitialized state variables, as explained in Issue #1379 with the SAWSMaterial.
But how will you know if your implementation of revertToLastCommit() is correct? This method is only called when the analysis fails and the domain asks all elements and materials to go back to their last committed state. Reverting to the last committed state is necessary when switching algorithms or reducing the time step after failed analysis steps.
Because revertToLastCommit() is only called after an analysis fails, it is difficult to verify the implementation is correct. This post explains one approach to isolating and testing calls to revertToLastCommit().
Baseline Response
First, we must establish a baseline, repeatable response history against which we can test reverted responses. I like to put materials through a strain history of white noise, which I implemented in bennycloth, a Python package named after a university mascot and a learning management system.
import openseespy.opensees as ops
import bennycloth as bc
from numpy import isclose
ops.wipe()
ops.model('basic','-ndm',1,'-ndf',1)
ops.node(1,0); ops.fix(1,1)
ops.node(2,0)
E = 29000
Fy = 60
epsy = Fy/E
ops.uniaxialMaterial('Hardening',1,E,Fy,0,0.005*E)
ops.element('zeroLength',1,1,2,'-mat',1,'-dir',1)
dtp = 1 # Time between pulses
T = 40 # Time series duration
t_up = T/4 # Ramp up time from t=0
t_down = T/10 # Ramp down time to t=T
white_noise = bc.WhiteNoise(dtp,T,t_up,t_down,seed=221537)
eps_max = 4*epsy # Maximum strain
ops.timeSeries('Path',1,'-dt',dtp,'-values',*white_noise,'-factor',eps_max)
ops.pattern('Plain',1,1)
ops.sp(2,1,1.0)
dt = dtp/20 # Analysis time step
Nsteps = int(T/dt)
ops.integrator('LoadControl',dt) # It's really pseudo-time control
ops.constraints('Transformation')
ops.system('UmfPack')
ops.analysis('Static','-noWarnings')
sig0 = [0]*Nsteps
for i in range(Nsteps):
ok = ops.analyze(1)
if ok < 0:
break
sig0[i] = ops.eleResponse(1,'material',1,'stress')[0]
The sig0 list contains the baseline stress history against which we can compare reverted responses.
Revert to Last Commit
Testing the revertToLastCommit() method requires you to force the analysis to fail, which is easier said than done. The most reliable way to ensure analysis failure is to use a negative tolerance in the convergence test.
We can repeat the baseline analysis and at each time step use a negative tolerance to intentionally fail the analysis, inducing a call to revertToLastCommit(), then analyze again with a positive tolerance. If revertToLastCommit() is implemented correctly, the response history should be the same as the baseline response.
#
# Model and analysis options defined above
#
ops.reset() # Assuming revertToStart() is implemented correctly
for i in range(Nsteps):
# This analysis will fail because the tolerance is negative
ops.test('NormUnbalance',-1e-8,10,0)
ops.analyze(1)
# This analysis should succeed
ops.test('NormUnbalance',1e-8,10,0)
ok = ops.analyze(1)
sigi = ops.eleResponse(1,'material',1,'stress')[0]
assert isclose(sigi,sig0[i])
If the assertion fails at any time step, we know that something is off in the revertToLastCommit() implementation.
Why Is This Important?
Running about 100 UniaxialMaterials through the white noise / forced failure gauntlet described above, only a handful of materials did not pass the revertToLastCommit() test, i.e., the repeated stress history was different from the baseline response.
One notable failure was the SeriesMaterial, a frequently used model for bridge abutments, member connections, and various other structural components. If you have used a series material along with smart analyze logic, odds are convergence issues compounded when switching algorithms or reducing time steps.
Basically, the trial state from the most recent non-converged step was used as the committed state for the next analysis in the SeriesMaterial. The issue was resolved in PR #1726 by storing the committed strain and tangent of the component materials.
Considering the SeriesMaterial is over 25 years old, the best time to have found this bug was 25 years ago. The second best time is now.
