Material Testing with White Noise

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 the Transformation constraint handler. The Plain handler will give an error.
  • The integrator is LoadControl with pseudo-time step dt.
  • 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.

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.