| name | fatigue-analysis |
| version | 1.0.0 |
| description | Fatigue analysis for offshore structures including S-N curves, rainflow counting, Miner's rule, and DNV standards |
| author | workspace-hub |
| category | subject-matter-expert |
| tags | ["fatigue","s-n-curve","rainflow-counting","miners-rule","dnv","mooring-fatigue","structural-fatigue"] |
| platforms | ["engineering"] |
Fatigue Analysis SME Skill
Comprehensive fatigue analysis expertise for offshore structures including mooring lines, risers, and structural components using industry-standard methods and DNV regulations.
When to Use This Skill
Use fatigue analysis when:
- Mooring line fatigue - Calculate fatigue life of mooring components
- Riser fatigue - Analyze fatigue damage in flexible and rigid risers
- Structural fatigue - Assess fatigue in hull, joints, connections
- S-N curve analysis - Apply appropriate fatigue curves
- Rainflow counting - Process stress/load time series
- Miner's rule - Cumulative damage calculation
- Fatigue design - Size components for target life
Core Knowledge Areas
1. S-N Curve Fundamentals
S-N Curve Equation:
N = a / (Δσ)^m
Where:
- N = Number of cycles to failure
- Δσ = Stress range
- a = S-N curve constant
- m = Slope of S-N curve (typically 3 for steel, 3-5 for welds)
DNV S-N Curves:
import numpy as np
def get_dnv_sn_curve(
curve_class: str,
thickness: float = 25
) -> dict:
"""
Get DNV S-N curve parameters.
DNV-RP-C203 S-N curves:
- B1: High strength welds, machined
- C: Good quality welds
- D: Normal welds
- E: Rough welds
- F, F1, F3: Poor quality, notches
- G: Severe notches
- W1, W2, W3: Seawater with cathodic protection
Args:
curve_class: DNV curve classification
thickness: Plate thickness (mm) for thickness effect
Returns:
S-N curve parameters
"""
sn_curves = {
'B1': {'log_a1': 15.117, 'm1': 4.0, 'log_a2': 17.146, 'm2': 5.0},
'B2': {'log_a1': 14.885, 'm1': 4.0, 'log_a2': 16.856, 'm2': 5.0},
'C': {'log_a1': 12.592, 'm1': 3.0, 'log_a2': 16.320, 'm2': 5.0},
'C1': {'log_a1': 12.449, 'm1': 3.0, 'log_a2': 16.081, 'm2': 5.0},
'C2': {: , : , : , : },
: {: , : , : , : },
: {: , : , : , : },
: {: , : , : , : },
: {: , : , : , : },
: {: , : , : , : },
: {: , : , : , : },
: {: , : , : , : },
: {: , : , : , : },
: {: , : , : , : }
}
curve_class sn_curves:
ValueError()
params = sn_curves[curve_class]
a1 = ** params[]
a2 = ** params[]
thickness > :
t_factor = ( / thickness) **
a1 *= t_factor ** params[]
a2 *= t_factor ** params[]
{
: curve_class,
: a1,
: params[],
: a2,
: params[],
: thickness
}
sn_f3 = get_dnv_sn_curve(, thickness=)
()
()
()
Calculate Cycles to Failure:
def calculate_cycles_to_failure(
stress_range: float,
sn_curve: dict
) -> float:
"""
Calculate cycles to failure for given stress range.
N = a / (Δσ)^m
Args:
stress_range: Stress range (MPa)
sn_curve: S-N curve parameters from get_dnv_sn_curve()
Returns:
Cycles to failure
"""
N1 = sn_curve['a1'] / (stress_range ** sn_curve['m1'])
if N1 <= 1e7:
return N1
else:
N2 = sn_curve['a2'] / (stress_range ** sn_curve['m2'])
return N2
stress_range = 50
N = calculate_cycles_to_failure(stress_range, sn_f3)
print(f"Stress range: {stress_range} MPa")
print(f"Cycles to failure: {N:.2e}")
print(f"Years at 1 Hz: {N / (365.25 * 24 * 3600):.2f}")
2. Rainflow Counting
Rainflow Algorithm:
def rainflow_counting(
time_series: np.ndarray,
bin_width: float = None
) -> tuple[np.ndarray, np.ndarray]:
"""
Rainflow cycle counting algorithm.
ASTM E1049-85 standard implementation.
Args:
time_series: Stress or load time series
bin_width: Bin width for histogram (None = auto)
Returns:
(ranges, counts) - Stress ranges and cycle counts
"""
peaks_valleys = []
for i in range(1, len(time_series) - 1):
if (time_series[i] > time_series[i-1] and time_series[i] > time_series[i+1]) or \
(time_series[i] < time_series[i-1] and time_series[i] < time_series[i+1]):
peaks_valleys.append(time_series[i])
stack = []
ranges = []
for value in peaks_valleys:
stack.append(value)
while len(stack) >= 3:
X = abs(stack[-2] - stack[-3])
Y = abs(stack[-1] - stack[-2])
if len(stack) == 3:
if Y >= X:
ranges.append(X)
stack.pop(-2)
stack.pop(-)
:
:
Z = (stack[-] - stack[-])
Y >= X X >= Z:
ranges.append(X)
stack.pop(-)
stack.pop(-)
:
ranges = np.array(ranges)
bin_width :
bin_width = (np.(ranges) - np.(ranges)) /
bins = np.arange(, np.(ranges) + bin_width, bin_width)
counts, bin_edges = np.histogram(ranges, bins=bins)
bin_centers = (bin_edges[:-] + bin_edges[:]) /
bin_centers, counts
t = np.linspace(, , )
tension = + * np.sin(*np.pi*t/) + * np.sin(*np.pi*t/) + *np.random.randn((t))
ranges, counts = rainflow_counting(tension, bin_width=)
()
()
()
3. Miner's Rule (Cumulative Damage)
Palmgren-Miner Damage:
def calculate_fatigue_damage_miners_rule(
stress_ranges: np.ndarray,
cycle_counts: np.ndarray,
sn_curve: dict,
design_factor: float = 10.0
) -> dict:
"""
Calculate fatigue damage using Miner's rule.
D = Σ(n_i / N_i)
Where:
- n_i = number of cycles at stress range i
- N_i = cycles to failure at stress range i
Args:
stress_ranges: Array of stress ranges (MPa)
cycle_counts: Array of cycle counts for each range
sn_curve: S-N curve parameters
design_factor: Safety factor (DNV: 10 for mooring)
Returns:
Fatigue damage and life prediction
"""
total_damage = 0.0
damage_breakdown = []
for stress_range, n_cycles in zip(stress_ranges, cycle_counts):
if stress_range > 0:
N = calculate_cycles_to_failure(stress_range, sn_curve)
damage = n_cycles / N
total_damage += damage
damage_breakdown.append({
'stress_range': stress_range,
'cycles': n_cycles,
'N_failure': N,
'damage': damage,
'damage_percent': 0
})
for item in damage_breakdown:
item['damage_percent'] = (item['damage'] / total_damage * 100) if total_damage > 0 else 0
total_damage_with_df = total_damage * design_factor
total_damage > :
fatigue_life = / total_damage
:
fatigue_life = np.inf
{
: total_damage,
: total_damage_with_df,
: fatigue_life,
: total_damage_with_df,
: total_damage_with_df <= ,
: damage_breakdown
}
hours_per_year =
design_life_years =
scale_factor = hours_per_year * design_life_years
stress_ranges = ranges /
cycle_counts_scaled = counts * scale_factor
fatigue_result = calculate_fatigue_damage_miners_rule(
stress_ranges,
cycle_counts_scaled,
sn_f3,
design_factor=
)
()
()
()
()
()
()
4. Spectral Fatigue Analysis
Narrow-Band Spectral Method:
def spectral_fatigue_narrow_band(
spectrum: np.ndarray,
frequencies: np.ndarray,
sn_curve: dict,
duration: float,
design_factor: float = 10.0
) -> dict:
"""
Calculate fatigue damage using narrow-band spectral method.
Assumes Rayleigh distribution of stress ranges.
Args:
spectrum: Stress response spectrum S(f)
frequencies: Frequency array (Hz)
sn_curve: S-N curve parameters
duration: Duration of analysis (seconds)
design_factor: Safety factor
Returns:
Fatigue damage
"""
m0 = np.trapz(spectrum, frequencies)
m2 = np.trapz(spectrum * frequencies**2, frequencies)
m4 = np.trapz(spectrum * frequencies**4, frequencies)
f0 = np.sqrt(m2 / m0)
N0 = f0 * duration
sigma = np.sqrt(m0)
m = sn_curve['m1']
a = sn_curve['a1']
from scipy.special import gamma
damage = N0 * (2 * sigma)**m * gamma(1 + m/2) / a
damage_with_df = damage * design_factor
if damage > 0:
fatigue_life = duration / damage
else:
fatigue_life = np.inf
return {
'total_damage': damage,
'damage_with_design_factor': damage_with_df,
'fatigue_life_seconds': fatigue_life,
'fatigue_life_years': fatigue_life / ( * * ),
: sigma,
: f0
}
freq_hz = np.linspace(, , )
S_stress = * freq_hz**(-)
fatigue_spectral = spectral_fatigue_narrow_band(
S_stress,
freq_hz,
sn_f3,
duration=,
design_factor=
)
fatigue_spectral[] = fatigue_spectral[] * *
()
()
()
5. Mooring Line Fatigue
Chain Fatigue at Fairlead:
def mooring_chain_fatigue_analysis(
tension_time_series: np.ndarray,
chain_diameter: float,
chain_grade: str = 'R4',
design_life_years: float = 25,
time_step: float = 0.1
) -> dict:
"""
Complete mooring chain fatigue analysis.
Args:
tension_time_series: Tension time series (kN)
chain_diameter: Chain diameter (mm)
chain_grade: Chain grade (R3, R4, R5)
design_life_years: Design life (years)
time_step: Time step (seconds)
Returns:
Fatigue results
"""
grade_factors = {'R3': 0.0219, 'R4': 0.0246, 'R5': 0.0273}
MBL = grade_factors[chain_grade] * chain_diameter**2
d_mm = chain_diameter
A = np.pi * (d_mm/2)**2
stress_time_series = tension_time_series * 1000 / A
stress_ranges, cycle_counts = rainflow_counting(stress_time_series)
duration_hours = len(tension_time_series) * time_step / 3600
hours_total = 8760 * design_life_years
scale_factor = hours_total / duration_hours
cycle_counts_scaled = cycle_counts * scale_factor
sn_curve = get_dnv_sn_curve('F3', thickness=chain_diameter)
fatigue_result = calculate_fatigue_damage_miners_rule(
stress_ranges,
cycle_counts_scaled,
sn_curve,
design_factor=
)
{
: chain_diameter,
: chain_grade,
: MBL,
: design_life_years,
: fatigue_result[],
: fatigue_result[],
: fatigue_result[],
: fatigue_result[],
: stress_ranges,
: cycle_counts_scaled
}
tension = + * np.sin(*np.pi*np.arange()/)
chain_fatigue = mooring_chain_fatigue_analysis(
tension,
chain_diameter=,
chain_grade=,
design_life_years=,
time_step=
)
()
()
()
()
()
()
Complete Examples
Example 1: Complete Fatigue Assessment
def complete_fatigue_assessment(
tension_file: str,
output_dir: str = 'reports/fatigue'
) -> dict:
"""
Complete fatigue assessment from tension time series.
Args:
tension_file: CSV file with tension time series
output_dir: Output directory
Returns:
Fatigue assessment results
"""
import pandas as pd
import plotly.graph_objects as go
from plotly.subplots import make_subplots
from pathlib import Path
output_path = Path(output_dir)
output_path.mkdir(parents=True, exist_ok=True)
df = pd.read_csv(tension_file)
tension = df['Tension'].values
time = df['Time'].values
ranges, counts = rainflow_counting(tension)
chain_diameter = 127
sn_curve = get_dnv_sn_curve('F3', thickness=chain_diameter)
fatigue = mooring_chain_fatigue_analysis(
tension,
chain_diameter=chain_diameter,
design_life_years=25,
time_step=time[1] - time[0]
)
fig = make_subplots(
rows=2, cols=2,
subplot_titles=(
'Tension Time Series',
'Rainflow Histogram',
'S-N Curve with Load Points',
'Damage Breakdown'
)
)
fig.add_trace(
go.Scatter(x=time, y=tension, name=, line=(width=)),
row=, col=
)
fig.add_trace(
go.Bar(x=ranges, y=counts, name=),
row=, col=
)
stress_plot = np.logspace(, , )
N_plot = sn_curve[] / stress_plot**sn_curve[]
fig.add_trace(
go.Scatter(
x=N_plot, y=stress_plot,
mode=, name=,
line=(color=)
),
row=, col=
)
stress_ranges_chain = fatigue[]
N_values = [calculate_cycles_to_failure(s, sn_curve) s stress_ranges_chain]
fig.add_trace(
go.Scatter(
x=N_values, y=stress_ranges_chain,
mode=, name=,
marker=(size=)
),
row=, col=
)
fig.update_xaxes(=, title_text=, row=, col=)
fig.update_yaxes(=, title_text=, row=, col=)
breakdown = fatigue_result[][:]
damage_pct = [item[] item breakdown]
stress_labels = [ item breakdown]
fig.add_trace(
go.Bar(x=stress_labels, y=damage_pct, name=),
row=, col=
)
fig.update_layout(height=, showlegend=, title_text=)
fig.write_html(output_path / )
summary = pd.DataFrame({
: [
,
,
,
,
,
,
,
],
: [
fatigue[],
fatigue[],
,
fatigue[],
,
,
,
fatigue[]
]
})
summary.to_csv(output_path / , index=)
()
()
()
fatigue
Resources
- DNV-RP-C203: Fatigue Design of Offshore Steel Structures
- DNV-OS-E301: Position Mooring (Section 7: Fatigue)
- API RP 2SK: Design and Analysis of Stationkeeping Systems for Floating Structures
- ASTM E1049: Standard Practices for Cycle Counting in Fatigue Analysis
- BS 7608: Code of Practice for Fatigue Design and Assessment of Steel Structures
Use this skill for all fatigue analysis in DigitalModel!