OpenSees Cloud
OpenSees AMI
Material Testing with White Noise
Original Post - 21 Apr 2024 - Michael H. Scott
Visit Structural Analysis Is Simple on Substack.
Pushes, pulls, and cyclic strain histories of increasing magnitude are solid approaches to testing the stress-strain response of material models. But I’m not convinced these tests will hit every code block of a material model implementation. I mean, have you seen all the nested if-statements and uninitialized local variables that went into Concrete23?
Although I did not take Random Vibrations in graduate school (it is the one course I wish I had taken), I have absorbed enough of the basic ideas. You too can learn a lot from Terje Haukaas’s reliability webpage and Armen der Kiureghian’s textbook. In addition to simulated ground motions, you can use white noise and modulating functions to construct random strain histories that should reach more code blocks in a material model than a prescribed, deterministic strain history.
White noise is a sequence of pulses, or shocks, sampled from independent random variables with zero mean. Gaussian white noise indicates the random variables have a normal distribution.
To create a single random pulse, get a random number between 0 and 1
using random.random()
, then pass that number to the norm.ppf
function, which is the inverse normal CDF. Put these function calls in a
Python list comprehension and you get a sequence of random pulses, i.e.,
white noise.
import random
from scipy.stats import norm
seed = random.randint(1e5,1e6-1)
random.seed(seed)
Npulse = 1000 # Number of pulses
noise = [norm.ppf(random.random()) for i in range(Npulse)]
So that the sequence is recoverable, e.g., for debugging, I generate and
store a random number seed. In the code above, I obtain a six-digit
number via random.randint
, but you can use whatever seed you want.
The generated white noise pulses are shown below.
We are going to turn these pulses into a strain history. Since we don’t want the first few pulses to cause immediate, large yield excursions, we can scale, or modulate, the pulse magnitudes. A trapezoidal modulating function gets the job done.
def modulator(t, t1, t2, t3, t4):
if t <= t1 or t >= t4:
return 0.0
elif t < t2: # Ramp up
return (t-t1)/(t2-t1)
elif t < t3: # Plateau
return 1.0
else: # Ramp down
return 1.0 - (t-t3)/(t4-t3)
t1 = 0
t2 = 0.1*Npulse
t3 = 0.9*Npulse
t4 = Npulse
mod_noise = [noise[i]*modulator(i,t1,t2,t3,t4) for i in range(Npulse)]
The modulated pulses are shown below. The trailing pulses have been modulated as well.
Finally, we can scale the sequence so that the maximum modulated pulse is of unit magnitude.
max_noise = max(abs(max(mod_noise)),abs(min(mod_noise)))
mod_noise = [mod_noise[i]/max_noise for i in range(Npulse)]
The scaled, modulated pulses are shown below.
The following code tests a uniaxial material with the white noise
contained in the mod_noise
list. I used Steel02
, but you can use
whatever material you like.
import openseespy.opensees as ops
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
b = 0.02
epsy = Fy/E
epsMax = 4*epsy # Peak strain
ops.uniaxialMaterial('Steel02',1,Fy,E,b)
ops.element('zeroLength',1,1,2,'-mat',1,'-dir',1)
ops.timeSeries('Path',1,'-dt',1,'-values',*mod_noise)
ops.pattern('Plain',1,1,'-factor',epsMax) # Peak strain
ops.sp(2,1,1.0) # Reference value
Nincr = 10 # Analysis steps between each pulse
dt = 1/Nincr
Nsteps = int(Npulse/dt) # Npulse = len(mode_noise)
ops.integrator('LoadControl',dt)
ops.constraints('Transformation')
ops.system('UmfPack')
ops.analysis('Static','-noWarnings')
sig = [0]*Nsteps
eps = [0]*Nsteps
for i in range(Nsteps):
ops.analyze(1)
sig[i] = ops.eleResponse(1,'material',1,'stress')[0]
eps[i] = ops.getLoadFactor(1)
A few things to note with this analysis:
- Strain is imposed via a time-varying
sp
constraint on a zero length element. - Due to the non-homogeneous
sp
constraint, the analysis uses theTransformation
constraint handler. ThePlain
handler will give an error. - The integrator is
LoadControl
with pseudo-time stepdt
. - The strain history linearly interpolates
Nincr
points between pulses. I used 10 points, but it’s better to use more points, as there will be consecutive pulses of large magnitude but opposite signs. - There are no equations to solve, so
UmfPack
is the solver to use.
The imposed strain history and Steel02
stress-strain response are
shown below.
The response looks pretty good–nothing obviously wrong. But you have to zoom in to see what’s really going on. In general, are there odd jumps in the response? Does the response inexplicably go beyond the envelope? Does the unloading/reloading behavior make sense? If you run this analysis through a memory checker like valgrind, do you leak memory or encounter uninitialized data, e.g., a temporary variable inside a seldom executed code block?
If you have written, or are writing, a UniaxialMaterial model in OpenSees, throw some white noise strain history at your model. You may find some unexpected behaviors.