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.

White noise pulses

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.

Modulated white noise pulses

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.

Scaled, modulated white noise pulses

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:

The imposed strain history and Steel02 stress-strain response are shown below.

Steel02 stress-strain history

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.