Tables 7-6 through 7-13 of the AISC Steel Manual contain values for *C*, the effective number of bolts that resist shear in eccentrically loaded bolt groups.

For example, in a bolt group with three vertical rows of 4 bolts spaced *s*=3 inch with *s _{row}*=3 inch row spacing and a load at =30 degrees from vertical at a horizontal eccentricity of

*e*=12 inch from the bolt group centroid, the coefficient is

_{X}*C*=3.82, i.e., the available strength of this bolt group is 3.82 times (not 12 times) the available strength of one bolt.

The tabulated values of *C* are based on the Instantaneous Center of Rotation (IC) Method. The Steel Manual also describes the Elastic Method to compute *C*, but that approach can be too conservative. The IC Method assumes all bolts in the group rotate about an instantaneous center of rotation under the action of the eccentric load.

OpenSees can handle the IC method pretty easily using zero length elements and a rigid diaphragm constraint; however, we have to define the model in 3D to accommodate the instantaneous center of rotation.

```
import openseespy.opensees as ops
import math
ops.wipe()
ops.model('basic','-ndm',3,'-ndf',6)
```

In addition to the kinematic assumption, the IC Method assumes a nonlinear force-deformation relationship for each bolt, , where is the bolt ultimate strength, which we can assume is 1 kip in order to compute the *C* coefficients. The maximum bolt deformation is assumed to be 0.34 inch.

OpenSees does not have a *uniaxialMaterial* or *hystereticBackbone* model that fits this function, but it’s easy enough the create the force-deformation relationship as a *MultiLinear* material.

```
Rult = 1.0 # Bolt strength
dmax = 0.34 # Maximum bolt deformation
N = 10 # Number of points
uf = [] # Array of deformation and force values
for i in range(1,N+1):
d = i*dmax/N
uf.append(d)
R = Rult*(1-math.exp(-10*d))**0.55
uf.append(R)
ops.uniaxialMaterial('MultiLinear',1,*uf)
```

Next, the force-deformation response needs to be applied in arbitrary directions for each bolt. We cannot use a regular *zeroLength* element because its materials act in orthogonal directions, uncoupled. Instead, we can use a *CoupledZeroLength* element which applies a uniaxial force-deformation relationship based on the strain magnitude and direction, i.e., “along the hypotenuse”.

For each bolt, we create two collocated nodes–one completely fixed (tag < 0) and the other (tag > 0) with out of plane DOFs constrained–then we define a *CoupledZeroLength* element. Finally, we define an additional node (tag = 0) at the point of loading (with out of plane DOFs constrained) and enforce a rigid diaphragm constraint with the loaded node as primary and the bolt nodes (tag > 0) as secondary.

```
s = 3 # bolt spacing
ex = 12 # load eccentricity
n = 4 # bolts per row
Nrows = 3 # number of rows
srow = 3 # row spacing
theta = 30*math.pi/180 # load direction
ndTags = []
for r in range(Nrows):
for i in range(n):
ndTag = r*n + i + 1
ndTags.append(ndTag)
ops.node(-ndTag,-r*srow,i*s,0)
ops.fix(-ndTag,1,1,1,1,1,1)
ops.node(ndTag,-r*srow,i*s,0)
ops.fix(ndTag,0,0,1,1,1,0)
ops.element('CoupledZeroLength',ndTag,-ndTag,ndTag,1,2,1)
ops.node(0,ex-srow*(Nrows-1)/2,s*(n-1)/2,0)
ops.fix(0,0,0,1,1,1,0)
ops.rigidDiaphragm(3,0,*ndTags)
```

This gives the 3D model shown below, plotted with `opsvis`

.

We then apply a load at the primary node and perform nonlinear static analysis until the deformation of any bolt reaches the 0.34 inch limit prescribed in the IC method.

```
ops.timeSeries('Linear',1)
ops.pattern('Plain',1,1)
ops.load(0,-math.sin(theta),-math.cos(theta),0,0,0,0)
ops.test('NormDispIncr',1e-6,10,0)
ops.algorithm('KrylovNewton')
ops.system('UmfPack')
ops.constraints('Transformation')
Pmax = Rult*Nrows*n # Maximum possible load
dP = 0.01
Nsteps = int(Pmax/dP)
ops.integrator('LoadControl',dP)
ops.analysis('Static')
for i in range(Nsteps):
ok = ops.analyze(1)
if ok < 0:
break
stopAnalysis = False
for j in ops.getEleTags():
eps = ops.eleResponse(j,'material','strain')
if abs(eps[0]) >= dmax:
stopAnalysis = True
break
if stopAnalysis:
break
C = ops.getLoadFactor(1)/Rult
```

I resorted to the KrylovNewton algorithm because it seems the *CoupledZeroLength* element does not return a consistent tangent, which led to convergence problems when I ran the analysis with the default Newton-Raphson algorithm.

Anyway, the coefficient computed by the OpenSees analysis is *C*=3.82, right on with the value tabulated in Table 7-10 of the Steel Manual!

The displaced shape of the model (magnification=5) at the end of the analysis is shown below, where we can see the bolt group has moved about an instantaneous center of rotation located somewhere around (*X*,*Y*)=(-4,6).

It would be a snap to put this code in nested loops to generate all of Tables 7-6 through 7-13 and to explore specific cases not shown in the tables.

And add this to the list of things you didn’t know could be done with OpenSees.

This post was suggested by a collaborator and seconded by current and former colleagues in Eastchester.

Very cool post, Mike!

LikeLiked by 1 person