Steel MRF

Nonlinear seismic response of a MRF

March 2020, By Amir Hossein Namadchi

This is an OpenSeesPy simulation of a moment resisting frame subjected to seismic excitation. The model was introduced by C. Kolay & J. M. Ricles in their paper entitled Assessment of explicit and semi-explicit classes of model-based algorithms for direct integration in structural dynamics. The beams and columns of the MRF are modeled using dispBeamColumn fiber elements. The gravity load resisting system associated with the MRF is modeled using a lean-on column composed of linear elastic beam-column elements with 2nd Order $P-\Delta$ effects [1].

Curved Arch

import numpy as np
import opensees.openseespy as ops
import matplotlib.pyplot as plt
import sees
# %matplotlib notebook

Units

from opensees.units.mks import m, N, sec, inch, kg, gravity as g
import opensees.units.mks as units

KN = N*1000
# kg *= 1000 # *= gravity #  TODO!!!!!!!!!!!
## Units
# m = 1.0               # Meters
# KN = 1.0              # KiloNewtons
# sec = 1.0             # Seconds
# inch = 0.0254*m       # inches 

# kg = KN*(sec**2)/m    # mass unit (derived)
# g = 9.81*(m/sec**2)   # gravitational constant
inch

Output:

0.0254

Earthquake record

This will load 1994 Northridge earthquake ground motion record (Canyon Country - W Lost Cany station) downloaded for the PEER website. Then, the record is scaled by a factor of 3 as follows (scaling could also be done when defining timeSeries):

dt = 0.01*sec
northridge = np.loadtxt('RSN960_NORTHR_LOS270.AT2', skiprows=4).flatten()
northridge = np.column_stack((np.arange(0,len(northridge)*dt, dt),
                northridge*3*g))

Model Definition

Geometry

Node coordinates and element connectivity are defined.


model = ops.Model(ndm=2, ndf=3)

## Main Nodes
# Node Coordinates Matrix (size : nn x 2)
node_coords = np.array([[0,0],[6,0],
                        [0,1],[6,1],
                        [0,2],[6,2],
                        [0,2.5],[6,2.5],
                        [0,3],[0.5,3],[1,3],[3,3],[5,3],[5.5,3],[6,3],
                        [0,3.5],[6,3.5],
                        [0,4],[6,4],
                        [0,5],[6,5],
                        [0,5.5],[6,5.5],
                        [0,6],[0.5,6],[1,6],[3,6],[5,6],[5.5,6],[6,6]
                       ], dtype = np.float64)*m
## Main Elements
# Element Connectivity Matrix (size: nel x 2)
connectivity = [[1, 3], [3, 5], [5, 7], [7, 9],
                [9, 10], [10, 11], [11, 12], [12, 13],
                [13, 14], [14, 15], [15, 8], [8, 6],
                [6, 4], [4, 2], [9, 16], [16, 18],
                [18, 20], [20, 22], [22, 24], [24, 25],
                [25, 26], [26, 27], [27, 28], [28, 29],
                [29, 30], [30, 23], [23, 21], [21, 19],
                [19, 17], [17, 15]]

# Get Number of elements
nel = len(connectivity)

# Distinguish beams and columns by their element tag ID
all_the_beams = list(range(5, 10+1)) + list(range(20, 25+1))
all_the_cols = list(np.setdiff1d(np.arange(1, nel+1),
                                 all_the_beams))

Sections & Material

sections are defined in dict which is quite self-explanatory.

# Main Beams and Columns
sections = {'W24x55':{'d':23.57*inch, 'tw':0.395*inch,
                      'bf':7.005*inch, 'tf':0.505*inch,
                      'A':16.2*(inch**2),
                      'I1':1350*(inch**4), 'I2':29.1*(inch**4)},
            
            'W14x120':{'d':14.48*inch, 'tw':0.590*inch,
                      'bf':14.670*inch, 'tf':0.940*inch,
                      'A':35.3*(inch**2),
                      'I1':1380*(inch**4), 'I2':495*(inch**4)}
           }

# Leaning columns section properties
leaning_col = {'A':(9.76e-2)*m**2,
               'I1':(7.125e-4)*m**4}

# Material properties
F_y = 345000*(KN/m**2)     # yield strength
E_0 = 2e8*(KN/m**2)        # initial elastic tangent
eta = 0.01                 # strain-hardening ratio
rho = 7850*(kg/m**3)       # mass density

Adding to the Domain

# Nodal loads and Masses
lumped_mass = 50.97*kg     # seismic floor mass
P_1 = 500*KN               # Nodal loads

# Adding nodes to the domain
## Main Nodes
for n in range(len(node_coords)):
    model.node(n+1, *node_coords[n])

## Fictitious Nodes (Leaning columns)
model.node(100,*[7.0, 0.0])                                # @ Base
model.node(101,*[7.0, 3.0],
         '-mass', *[lumped_mass, 0.00001, 0.00001])      # @ Story 1
model.node(102,*[7.0, 6.0],
         '-mass', *[lumped_mass, 0.00001, 0.00001])      # @ Story 2 (roof)

# Material
# -> uniaxial bilinear steel material with kinematic hardening
model.uniaxialMaterial('Steel01', 1, F_y, E_0, eta)

# Adding Sections
## Beams
# model.section('WFSection2d', 1, 1,
#             sections['W24x55']['d'],
#             sections['W24x55']['tw'],
#             sections['W24x55']['bf'],
#             sections['W24x55']['tf'], 10, 3)

model.section('Fiber', 1, shape=("W24x55",  1, (10,3), units))

## Columns
model.section('Fiber', 2, shape=("W14x120", 1, (10,3), units))
# model.section('WFSection2d', 2, 1,
#             sections['W14x120']['d'],
#             sections['W14x120']['tw'],
#             sections['W14x120']['bf'],
#             sections['W14x120']['tf'], 10, 3)

# Boundary Conditions
## Fixing the Base Nodes
for n in [1, 2, 100]:
    model.fix(n, 1, 1, 0)

## Rigid floor diaphragm
model.equalDOF(12, 101, 1)
model.equalDOF(27, 102, 1)

# Transformations & Integration
## Transformation
model.geomTransf('Linear', 1)    # For Beams
model.geomTransf('PDelta', 2)    # For leaning Columns
## Integration scheme
model.beamIntegration('Lobatto', 1, 1, 5)   # For Beams
model.beamIntegration('Lobatto', 2, 2, 5)   # For Columns

# Adding Elements
## Beams
for e in all_the_beams:
    model.element('DispBeamColumn',
            e, *connectivity[e-1], 1, 1,
            '-cMass', rho*sections['W24x55']['A'])

## Columns
## -> OpenseesPy cannot handle numpy int types
## -> so I had to convert them to primitive python int type
for e in list(map(int, all_the_cols)):
     model.element('DispBeamColumn', e, *connectivity[e-1], 1, 2,
             '-cMass', rho*sections['W14x120']['A'])


## Leaning Columns
model.element('ElasticBeamColumn', nel+1, *[100, 101],
            leaning_col['A'], E_0, leaning_col['I1'], 2)
model.element('ElasticBeamColumn', nel+2, *[101, 102],
            leaning_col['A'], E_0, leaning_col['I1'], 2)

Draw Model

The model can now be drawn using sees:

sees.render(model)
model.print("-json", file="model.json")

Output:

<Figure size 640x480 with 1 Axes>

Damping Model

The model assumes 2% damping for the first and second modes of the system according to rayleigh’s damping model. for two modes, the damping coefficients can be obtained by:

$$ \left( \begin{array}{l} {\alpha _0}\\ {\alpha _1} \end{array} \right) = 2\frac{{{\omega _m}{\omega _n}}}{{\omega _n^2 - \omega _m^2}}\left[ {\begin{array}{*{20}{c}} {{\omega _n}}&{ - {\omega _m}}\\ { - 1/{\omega _n}}&{1/{\omega _m}} \end{array}} \right]\left( \begin{array}{l} {\zeta _m}\\ {\zeta _n} \end{array} \right)\ $$

So, we need to perform an eigen analysis to obtain first two natural frequencies.

# Building Rayleigh damping model
omega = np.sqrt(model.eigen('-fullGenLapack', 2))
print('Two first periods are:', 2*np.pi/omega)

a_m, b_k = 2*((omega[0]*omega[1])/(omega[1]**2-omega[0]**2))*(
    np.array([[omega[1],-omega[0]],
              [-1/omega[1],1/omega[0]]])@np.array([0.02,0.02]))
## Rayleigh damping based on initial stiffness
model.rayleigh(a_m, 0, b_k, 0)

Output:

Two first periods are: [1.17694844e-06 1.74174909e-07]

Analysis

Gravity Analysis

# Time Series
model.timeSeries('Linear', 1)                    # For gravitional loads

# Load Pattern
model.pattern('Plain', 1, 1)
model.load(101, *[0.0, -P_1, 0.0])
model.load(102, *[0.0, -P_1, 0.0])

# Settings
model.constraints('Transformation')
model.numberer('RCM')
model.system('ProfileSPD')
model.test('NormUnbalance', 0.000001, 100)
model.algorithm('Newton')
model.integrator('LoadControl', 0.1)
model.analysis('Static')

# Perform static analysis
assert model.analyze(10) == 0

Output:

StaticAnalysis::analyze - the Algorithm failed at step: 0 with domain at load factor 0.1

Time History Analysis

# Set time to zero
model.loadConst('-time', 0.0)
model.wipeAnalysis()

# Time Series
model.timeSeries('Path', 2, dt=dt,           # For EQ 
                 values=northridge[:,1],
                 time = northridge[:,0])

# Load Pattern
model.pattern('UniformExcitation', 2, 1, '-accel', 2)


# Settings
model.constraints('Plain')
model.numberer('RCM')
model.system('ProfileSPD')
model.test('NormUnbalance', 0.0000001, 100)
model.algorithm('Newton')
model.integrator('Newmark', 0.5, 0.25)
model.analysis('Transient')

# Record some responses to plot
time_lst =[]     # list to hold time stations for plotting
d_lst = []      # list to hold roof displacments
for i in range(len(northridge)):
    model.analyze(1, dt)
    time_lst.append(model.getTime())
    d_lst.append(model.nodeDisp(27,1))    

Visualization

Time history of the horizontal displacement of the roof is plotted here

plt.figure(figsize=(12,4))
plt.plot(time_lst, np.array(d_lst), color = '#d62d20', linewidth=1.75)
plt.ylabel('Horizontal Displacement (m)', {'fontstyle':'italic','size':14})
plt.xlabel('Time (sec)', {'fontstyle':'italic','size':14})
plt.grid()
plt.yticks(fontsize = 14)
plt.xticks(fontsize = 14);

References

  • Kolay, C. and Ricles, J.M., 2016. Assessment of explicit and semi‐explicit classes of model‐based algorithms for direct integration in structural dynamics. International Journal for Numerical Methods in Engineering, 107(1), pp.49-73.