Stress Concentration Factors (SCF) are the increase in stress at a geometrical discontinuity in a structure such as a hole in a plate or at the intersection of a brace and a chord. In a typical structural analysis, the geometric detail necessary to resolve an SCF is not modeled and is usually not important for a strength analysis. However, for a fatigue analysis, the SCF must be computed and applied to the nominal stress to obtain the peak stress for use with fatigue life calculations. For the tubular structures typically used in offshore structures, there are several parametric equations used to predict the SCF at a brace-chord intersection. The most widely used is the Efthymiou/Durkin parametric equations 1. These equations predict the SCF at the crown and saddle of tubular connections subject to brace axial force and in-plane and out-of-plane bending. The Efthymiou/Durkin parametric equations can be calculated quite easily using an online tool such as 2 or within a structural analysis program. There are other parameteric equations such as the Kuang, Wordsworth/Smedley, Hellier/Connolly/Dover, and Lloyd’s Register equations. We will only address the Efthymiou/Durkin parametric equations.
Outside of the range of applicability of these equations are ring stiffened, grouted, and non-tubular joints for which the SCF is desired but there are no parametric equations from which to compute them. In these cases, the SCF can be readily computed using a Finite Element Analysis (FEA). A number of interpretative problems may arise in the use of FEA in the prediction of the SCF. There may be stress singularities when modeling a joint without modeling the weld. In that case, progressive mesh refinement will eventually yield an infinite SCF.
Likewise, the computation of the SCF for tubular joints can be dependent on the modeling methods used in the FEA model and the method of SCF extraction. The DNVGL Recommended Practice C203 (3) outlines a procedure for extracting the SCF from an FEA. This approach has been codified and automated in a proprietary CRM Engineering program which extracts the SCF along the intersection of an arbitrary connection.
However, little work has been reported in the literature which compares both the parametric and the FEA SCF against experimentally measured SCF. The reference 4 compiles a substantial literature survey of experimental SCF measurements and compares them with the aforementioned parametric equations. Thibaux and Cooreman 5 compare the Efthymiou SCF prediction against an FEA assessment using a rela- tively coarse tetrahedral mesh for the joints.
In this work we compare the Efthymiou SCF with the experimental results from 4, FEA results from four node shell elements meshed with two different strategies against the results from 4, and the FEA results with the Efthymiou SCF. The FEA models are built from selected joints reported in 4 in which $\beta \lt 1$ and the material is steel for T/Y joints (4, Tables B1.1 - B1.15). There are 37 unique joints in 4 modeled.
Two meshing strategies are used.
In the first the $t \times t$ to $2t \times 2t$ method advocated by 3 is employed.
This method is indicated by FEA/1
.
The global mesh seed is $\min\left(2 t_{\mathrm{brace}}, t_{\mathrm{chord}}\right)$.
The second method forces the number of elements at the intersection to be 128.
This method is indicated by FEA/2
.
In general, method FEA/2
yields a finer mesh than FEA/1
.
At the center of both ends of the chord and at the center of the brace end, a reference point is created. This reference point is tied to the member end with a kinematic coupling constraint. The chord end reference points are fixed. The axial force, in-plane and out-of-plane bending moments are applied to the brace reference point. The magnitudes of the force and moments are calculated to produce a unit stress on the extreme fiber of the brace. Thus the calculated SCF is referenced to unity.
The comparisons are plotted such that a perfect correlation would be on the 1:1 black line in the figures. A curve through the origin is fit to each dataset. If the fitted curve is above the 1:1 line, then the method overestimates the SCF. Likewise, if the fitted curve is below the 1:1 line, then the method underestimates the SCF.
import pandas as pd
import matplotlib
import numpy as np
from scipy import stats
from scipy.optimize import curve_fit
import matplotlib.pyplot as plt
%matplotlib inline
%config InlineBackend.figure_format = 'svg'
matplotlib.style.use('ggplot')
linear_func
is the function for a straight line with $b = 0$.
def linear_func(xdata, m):
return m*xdata
Read in the result summary into a pandas dataframe.
Not all the experimental results have results for each loading or location.
These missing values are indicated with a NaN
.
df = pd.read_csv('result_summary_combined.csv', na_values=['NaN'])
with pd.option_context('display.max_rows', None):
print(df)
def make_plot(x1, y1, x2, y2, popt_1, popt_2,ymax, x_axis, y_axis='FEA'):
fig = plt.figure(figsize=(10, 8))
ax = fig.add_subplot(111)
plt.scatter(x1.values,
y1.values,
color='r',
label='FEA/1')
plt.scatter(x2.values,
y2.values,
color='b',
label='FEA/2')
plt.plot([0,100],[0,100],color='k')
plt.plot(x1.values,
linear_func(x1.values, *popt_1),
'r')
plt.plot(x2.values,
linear_func(x2.values, *popt_2),
'b')
plt.xlabel(x_axis)
plt.ylabel(y_axis)
ax.set_aspect('equal')
ax.set_xlim(0,ymax)
ax.set_ylim(0,ymax)
plt.legend()
return fig
def make_plot_experiment(x1, y1, x2, y2, popt_1, popt_2, ymax):
fig = make_plot(x1, y1, x2, y2, popt_1, popt_2,ymax, 'Experiment')
return fig
def make_plot_experiment_efthymiou(x, y1, popt_1, ymax):
fig = plt.figure(figsize=(10, 8))
ax = fig.add_subplot(111)
plt.scatter(x.values,
y1.values,
color='r',
label='Efthymiou')
plt.plot([0,100],[0,100],color='k')
plt.plot(x.values,
linear_func(x.values, *popt_1),
'r')
plt.xlabel('Experiment')
plt.ylabel('Efthymiou')
ax.set_aspect('equal')
ax.set_xlim(0,ymax)
ax.set_ylim(0,ymax)
plt.legend()
return fig
def make_plot_efthymiou(x, y1, y2, popt_1, popt_2, ymax):
fig = make_plot(x, y1, y2, popt_1, popt_2,ymax, 'Efthymiou')
return fig
The outliers_iqr
function removes outliers using the inter-quartile method.
def outliers_iqr(ys):
quartile_1, quartile_3 = np.percentile(ys, [25, 75])
iqr = quartile_3 - quartile_1
lower_bound = quartile_1 - (iqr * 1.5)
#print(lower_bound)
upper_bound = quartile_3 + (iqr * 1.5)
#print(upper_bound)
return np.argwhere((ys > upper_bound) | (ys < lower_bound))
def process_case(lc):
'''
lc = 'AxialChordSaddle', etc.
'''
experiment = df.loc[df['Source'] == 'Experiment'][lc]
efthy_exp = df.loc[df['Source'] == 'Efthymiou'][lc]
fea = df.loc[df['Source'] == 'FEA'][lc]
fea_2 = df.loc[df['Source'] == 'FEA/2'][lc]
# Verify the lengths are equal
assert len( set( [len(experiment),
len(efthy_exp),
len(fea),
len(fea_2)]) ) == 1
# Create dataframe from columns
src = {'Experiment':experiment.values,
'Efthymiou':efthy_exp.values,
'FEA':fea.values,
'FEA/2':fea_2.values}
src_df = pd.DataFrame(data=src)
#with pd.option_context('display.max_rows', None):
# print(src_df)
# Process FEA
# Drop rows with NaN
fea_df = src_df.dropna(subset=['Experiment','FEA'])
# Reindex
fea_df = fea_df.reset_index(drop=True)
# Detect outliers using inter-quartile range method
indices_fea = outliers_iqr(fea_df['Experiment'].values -
fea_df['FEA'].values)
# Drop outliers
fea_df = fea_df.drop(indices_fea.flatten())
# Reindex
fea_df = fea_df.reset_index(drop=True)
# Process FEA/2
# Drop rows with NaN
fea_2_df = src_df.dropna(subset=['Experiment','FEA/2'])
# Reindex
fea_2_df = fea_2_df.reset_index(drop=True)
# Detect outliers using inter-quartile range method
indices_fea_2 = outliers_iqr(fea_2_df['Experiment'].values -
fea_2_df['FEA/2'].values)
# Drop outliers
fea_2_df = fea_2_df.drop(indices_fea_2.flatten())
# Reindex
fea_2_df = fea_2_df.reset_index(drop=True)
# Process Efthymiou
# Drop rows with NaN
efthy_df = src_df.dropna(subset=['Experiment','Efthymiou'])
# Reindex
efthy_df = efthy_df.reset_index(drop=True)
# Detect outliers using inter-quartile range method
indices_efthy = outliers_iqr(efthy_df['Experiment'].values -
efthy_df['Efthymiou'].values)
# Drop outliers
efthy_df = efthy_df.drop(indices_efthy.flatten())
# Reindex
efthy_df = efthy_df.reset_index(drop=True)
d = {'fea':fea_df,
'fea_2':fea_2_df,
'efthy':efthy_df}
return d
Function which fits both the FEA and the Ethymiou parameteric results to a straight line goig through the origin.
def fit(d):
popt_1, pcov_1 = curve_fit(linear_func,
d['fea']['Experiment'].values,
d['fea']['FEA'].values)
popt_2, pcov_2 = curve_fit(linear_func,
d['fea_2']['Experiment'].values,
d['fea_2']['FEA/2'].values)
popt_3, pcov_3 = curve_fit(linear_func,
d['efthy']['Experiment'].values,
d['efthy']['Efthymiou'].values)
return ((popt_1, popt_2, popt_3),
(pcov_1, pcov_2, pcov_3))
d_axial_chord_saddle = process_case('AxialChordSaddle')
popt_axial_chord_saddle, pcov_axial_chord_saddle = fit(d_axial_chord_saddle)
print('Slope FEA/1 vs experiment: {:0.2f}'.format(popt_axial_chord_saddle[0][0]))
perr = np.sqrt(np.diag(pcov_axial_chord_saddle[0]))[0]
print('One standard deviation error on the slope: {:0.2f}'.format(perr))
print('Slope FEA/2 vs experiment: {:0.2f}'.format(popt_axial_chord_saddle[1][0]))
perr = np.sqrt(np.diag(pcov_axial_chord_saddle[1]))[0]
print('One standard deviation error on the slope: {:0.2f}'.format(perr))
print('Slope: Efthymiou vs experiment: {:0.2f}'.format(popt_axial_chord_saddle[2][0]))
perr = np.sqrt(np.diag(pcov_axial_chord_saddle[2]))[0]
print('One standard deviation error on the slope: {:0.2f}'.format(perr))
fig = make_plot_experiment(d_axial_chord_saddle['fea']['Experiment'],
d_axial_chord_saddle['fea']['FEA'],
d_axial_chord_saddle['fea_2']['Experiment'],
d_axial_chord_saddle['fea_2']['FEA/2'],
popt_axial_chord_saddle[0],
popt_axial_chord_saddle[1],
20)
fig.savefig('AxialChordSaddle_fea_vs_exp.pdf', bbox_inches='tight')
fig.suptitle('FEA vs. Experimental SCF', fontsize=14, fontweight='bold')
fig.axes[0].set_title('Axial/Chord/Saddle')
fig = make_plot_experiment_efthymiou(d_axial_chord_saddle['efthy']['Experiment'],
d_axial_chord_saddle['efthy']['Efthymiou'],
popt_axial_chord_saddle[2],
20)
fig.savefig('AxialChordSaddle_efthy_vs_exp.pdf', bbox_inches='tight')
fig.suptitle('Efthymiou vs. Experimental SCF', fontsize=14, fontweight='bold')
fig.axes[0].set_title('Axial/Chord/Saddle')
d_axial_chord_crown = process_case('AxialChordCrown')
popt_axial_chord_crown, pcov_axial_chord_crown = fit(d_axial_chord_crown)
print('Slope FEA/1 vs experiment: {:0.2f}'.format(popt_axial_chord_crown[0][0]))
perr = np.sqrt(np.diag(pcov_axial_chord_crown[0]))[0]
print('One standard deviation error on the slope: {:0.2f}'.format(perr))
print('Slope FEA/2 vs experiment: {:0.2f}'.format(popt_axial_chord_crown[1][0]))
perr = np.sqrt(np.diag(pcov_axial_chord_crown[1]))[0]
print('One standard deviation error on the slope: {:0.2f}'.format(perr))
print('Slope: Efthymiou vs experiment: {:0.2f}'.format(popt_axial_chord_crown[2][0]))
perr = np.sqrt(np.diag(pcov_axial_chord_crown[2]))[0]
print('One standard deviation error on the slope: {:0.2f}'.format(perr))
fig = make_plot_experiment(d_axial_chord_crown['fea']['Experiment'],
d_axial_chord_crown['fea']['FEA'],
d_axial_chord_crown['fea_2']['Experiment'],
d_axial_chord_crown['fea_2']['FEA/2'],
popt_axial_chord_crown[0],
popt_axial_chord_crown[1],
7)
fig.savefig('AxialChordCrown_fea_vs_exp.pdf', bbox_inches='tight')
fig.suptitle('FEA vs. Experimental SCF', fontsize=14, fontweight='bold')
fig.axes[0].set_title('Axial/Chord/Crown')
fig = make_plot_experiment_efthymiou(d_axial_chord_crown['efthy']['Experiment'],
d_axial_chord_crown['efthy']['Efthymiou'],
popt_axial_chord_crown[2],
8)
fig.savefig('AxialChordCrown_efthy_vs_exp.pdf', bbox_inches='tight')
fig.suptitle('Efthymiou vs. Experimental SCF', fontsize=14, fontweight='bold')
fig.axes[0].set_title('Axial/Chord/Crown')
d_axial_brace_saddle = process_case('AxialBraceSaddle')
popt_axial_brace_saddle, pcov_axial_brace_saddle = fit(d_axial_brace_saddle)
print('Slope FEA/1 vs experiment: {:0.2f}'.format(popt_axial_brace_saddle[0][0]))
perr = np.sqrt(np.diag(pcov_axial_brace_saddle[0]))[0]
print('One standard deviation error on the slope: {:0.2f}'.format(perr))
print('Slope FEA/2 vs experiment: {:0.2f}'.format(popt_axial_brace_saddle[1][0]))
perr = np.sqrt(np.diag(pcov_axial_brace_saddle[1]))[0]
print('One standard deviation error on the slope: {:0.2f}'.format(perr))
print('Slope: Efthymiou vs experiment: {:0.2f}'.format(popt_axial_brace_saddle[2][0]))
perr = np.sqrt(np.diag(pcov_axial_brace_saddle[2]))[0]
print('One standard deviation error on the slope: {:0.2f}'.format(perr))
fig = make_plot_experiment(d_axial_brace_saddle['fea']['Experiment'],
d_axial_brace_saddle['fea']['FEA'],
d_axial_brace_saddle['fea_2']['Experiment'],
d_axial_brace_saddle['fea_2']['FEA/2'],
popt_axial_brace_saddle[0],
popt_axial_brace_saddle[1],
16)
fig.savefig('AxialBraceSaddle_fea_vs_exp.pdf', bbox_inches='tight')
fig.suptitle('FEA vs. Experimental SCF', fontsize=14, fontweight='bold')
fig.axes[0].set_title('Axial/Brace/Saddle')
fig = make_plot_experiment_efthymiou(d_axial_brace_saddle['efthy']['Experiment'],
d_axial_brace_saddle['efthy']['Efthymiou'],
popt_axial_brace_saddle[2],
14)
fig.savefig('AxialBraceSaddle_efthy_vs_exp.pdf', bbox_inches='tight')
fig.suptitle('Efthymiou vs. Experimental SCF', fontsize=14, fontweight='bold')
fig.axes[0].set_title('Axial/Brace/Saddle')
d_axial_brace_crown = process_case('AxialBraceCrown')
popt_axial_brace_crown, pcov_axial_brace_crown = fit(d_axial_brace_crown)
print('Slope FEA/1 vs experiment: {:0.2f}'.format(popt_axial_brace_crown[0][0]))
perr = np.sqrt(np.diag(pcov_axial_brace_crown[0]))[0]
print('One standard deviation error on the slope: {:0.2f}'.format(perr))
print('Slope FEA/2 vs experiment: {:0.2f}'.format(popt_axial_brace_crown[1][0]))
perr = np.sqrt(np.diag(pcov_axial_brace_crown[1]))[0]
print('One standard deviation error on the slope: {:0.2f}'.format(perr))
print('Slope: Efthymiou vs experiment: {:0.2f}'.format(popt_axial_brace_crown[2][0]))
perr = np.sqrt(np.diag(pcov_axial_brace_crown[2]))[0]
print('One standard deviation error on the slope: {:0.2f}'.format(perr))
fig = make_plot_experiment(d_axial_brace_crown['fea']['Experiment'],
d_axial_brace_crown['fea']['FEA'],
d_axial_brace_crown['fea_2']['Experiment'],
d_axial_brace_crown['fea_2']['FEA/2'],
popt_axial_brace_crown[0],
popt_axial_brace_crown[1],
4)
fig.savefig('AxialBraceCrown_fea_vs_exp.pdf', bbox_inches='tight')
fig.suptitle('FEA vs. Experimental SCF', fontsize=14, fontweight='bold')
fig.axes[0].set_title('Axial/Brace/Crown')
fig = make_plot_experiment_efthymiou(d_axial_brace_crown['efthy']['Experiment'],
d_axial_brace_crown['efthy']['Efthymiou'],
popt_axial_brace_crown[2],
4)
fig.savefig('AxialBraceCrown_efthy_vs_exp.pdf', bbox_inches='tight')
fig.suptitle('Efthymiou vs. Experimental SCF', fontsize=14, fontweight='bold')
fig.axes[0].set_title('Axial/Brace/Crown')
d_opb_chord_saddle = process_case('OPBChordSaddle')
popt_opb_chord_saddle, pcov_opb_chord_saddle = fit(d_opb_chord_saddle)
print('Slope FEA/1 vs experiment: {:0.2f}'.format(popt_opb_chord_saddle[0][0]))
perr = np.sqrt(np.diag(pcov_opb_chord_saddle[0]))[0]
print('One standard deviation error on the slope: {:0.2f}'.format(perr))
print('Slope FEA/2 vs experiment: {:0.2f}'.format(popt_opb_chord_saddle[1][0]))
perr = np.sqrt(np.diag(pcov_opb_chord_saddle[1]))[0]
print('One standard deviation error on the slope: {:0.2f}'.format(perr))
print('Slope: Efthymiou vs experiment: {:0.2f}'.format(popt_opb_chord_saddle[2][0]))
perr = np.sqrt(np.diag(pcov_opb_chord_saddle[2]))[0]
print('One standard deviation error on the slope: {:0.2f}'.format(perr))
fig = make_plot_experiment(d_opb_chord_saddle['fea']['Experiment'],
d_opb_chord_saddle['fea']['FEA'],
d_opb_chord_saddle['fea_2']['Experiment'],
d_opb_chord_saddle['fea_2']['FEA/2'],
popt_opb_chord_saddle[0],
popt_opb_chord_saddle[1],
20)
fig.savefig('OPBChordSaddle_fea_vs_exp.pdf', bbox_inches='tight')
fig.suptitle('FEA vs. Experimental SCF', fontsize=14, fontweight='bold')
fig.axes[0].set_title('OPB/Chord/Saddle')
fig = make_plot_experiment_efthymiou(d_opb_chord_saddle['efthy']['Experiment'],
d_opb_chord_saddle['efthy']['Efthymiou'],
popt_opb_chord_saddle[2],
25)
fig.savefig('OPBChordSaddle_efthy_vs_exp.pdf', bbox_inches='tight')
fig.suptitle('Efthymiou vs. Experimental SCF', fontsize=14, fontweight='bold')
fig.axes[0].set_title('OPB/Chord/Saddle')
d_opb_brace_saddle = process_case('OPBBraceSaddle')
popt_opb_brace_saddle, pcov_opb_brace_saddle = fit(d_opb_brace_saddle)
print('Slope FEA/1 vs experiment: {:0.2f}'.format(popt_opb_brace_saddle[0][0]))
perr = np.sqrt(np.diag(pcov_opb_brace_saddle[0]))[0]
print('One standard deviation error on the slope: {:0.2f}'.format(perr))
print('Slope FEA/2 vs experiment: {:0.2f}'.format(popt_opb_brace_saddle[1][0]))
perr = np.sqrt(np.diag(pcov_opb_brace_saddle[1]))[0]
print('One standard deviation error on the slope: {:0.2f}'.format(perr))
print('Slope: Efthymiou vs experiment: {:0.2f}'.format(popt_opb_brace_saddle[2][0]))
perr = np.sqrt(np.diag(pcov_opb_brace_saddle[2]))[0]
print('One standard deviation error on the slope: {:0.2f}'.format(perr))
fig = make_plot_experiment(d_opb_brace_saddle['fea']['Experiment'],
d_opb_brace_saddle['fea']['FEA'],
d_opb_brace_saddle['fea_2']['Experiment'],
d_opb_brace_saddle['fea_2']['FEA/2'],
popt_opb_brace_saddle[0],
popt_opb_brace_saddle[1],
15)
fig.savefig('OPBBraceSaddle_fea_vs_exp.pdf', bbox_inches='tight')
fig.suptitle('FEA vs. Experimental SCF', fontsize=14, fontweight='bold')
fig.axes[0].set_title('OPB/Brace/Saddle')
fig = make_plot_experiment_efthymiou(d_opb_brace_saddle['efthy']['Experiment'],
d_opb_brace_saddle['efthy']['Efthymiou'],
popt_opb_brace_saddle[2],
12)
fig.savefig('OPBBraceSaddle_efthy_vs_exp.pdf', bbox_inches='tight')
fig.suptitle('Efthymiou vs. Experimental SCF', fontsize=14, fontweight='bold')
fig.axes[0].set_title('OPB/Brace/Saddle')
d_ipb_chord_crown = process_case('IPBChordCrown')
popt_ipb_chord_crown, pcov_ipb_chord_crown = fit(d_ipb_chord_crown)
print('Slope FEA/1 vs experiment: {:0.2f}'.format(popt_ipb_chord_crown[0][0]))
perr <