Delegate independent tasks to specialized agents working concurrently with isolated context.
Fatigue Analysis SME Skill
fatigue-analysis
Fatigue analysis for offshore structures including S-N curves, rainflow counting, Miner's rule, and DNV standards
SKILL.md
Full skill instructions
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
"""
# DNV-RP-C203 Table 2-1
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': {'log_a1': 12.301, 'm1': 3.0, 'log_a2': 15.835, 'm2': 5.0},
'D': {'log_a1': 12.164, 'm1': 3.0, 'log_a2': 15.606, 'm2': 5.0},
'E': {'log_a1': 11.972, 'm1': 3.0, 'log_a2': 15.350, 'm2': 5.0},
'F': {'log_a1': 11.699, 'm1': 3.0, 'log_a2': 14.832, 'm2': 5.0},
'F1': {'log_a1': 11.546, 'm1': 3.0, 'log_a2': 14.576, 'm2': 5.0},
'F3': {'log_a1': 11.398, 'm1': 3.0, 'log_a2': 14.330, 'm2': 5.0},
'G': {'log_a1': 11.245, 'm1': 3.0, 'log_a2': 14.080, 'm2': 5.0},
'W1': {'log_a1': 11.764, 'm1': 3.0, 'log_a2': 15.091, 'm2': 5.0},
'W2': {'log_a1': 11.533, 'm1': 3.0, 'log_a2': 14.706, 'm2': 5.0},
'W3': {'log_a1': 11.262, 'm1': 3.0, 'log_a2': 14.183, 'm2': 5.0}
}
if curve_class not in sn_curves:
raise ValueError(f"Unknown S-N curve class: {curve_class}")
params = sn_curves[curve_class]
# Convert log_a to a
a1 = 10 ** params['log_a1']
a2 = 10 ** params['log_a2']
# Thickness correction (ref thickness = 25mm)
if thickness > 25:
t_factor = (25 / thickness) ** 0.25
a1 *= t_factor ** params['m1']
a2 *= t_factor ** params['m2']
return {
'class': curve_class,
'a1': a1,
'm1': params['m1'],
'a2': a2,
'm2': params['m2'],
'thickness_mm': thickness
}
# Example: Get F3 curve for mooring chain
sn_f3 = get_dnv_sn_curve('F3', thickness=127) # 127mm chain
print(f"S-N Curve F3 (Chain):")
print(f" a1 = {sn_f3['a1']:.2e}, m1 = {sn_f3['m1']}")
print(f" a2 = {sn_f3['a2']:.2e}, m2 = {sn_f3['m2']}")
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
"""
# Use first segment if stress range is high
# Switch to second segment if N > 1e7 (DNV bi-linear curve)
N1 = sn_curve['a1'] / (stress_range ** sn_curve['m1'])
if N1 <= 1e7:
return N1
else:
# Use second segment
N2 = sn_curve['a2'] / (stress_range ** sn_curve['m2'])
return N2
# Example
stress_range = 50 # MPa
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
"""
# Simple peak-valley extraction
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])
# Rainflow counting
stack = []
ranges = []
for value in peaks_valleys:
stack.append(value)
while len(stack) >= 3:
# Check for cycle
X = abs(stack[-2] - stack[-3])
Y = abs(stack[-1] - stack[-2])
if len(stack) == 3:
if Y >= X:
# Extract cycle
ranges.append(X)
stack.pop(-2)
stack.pop(-2)
else:
break
else:
Z = abs(stack[-3] - stack[-4])
if Y >= X and X >= Z:
# Extract cycle
ranges.append(X)
stack.pop(-2)
stack.pop(-2)
else:
break
# Create histogram
ranges = np.array(ranges)
if bin_width is None:
bin_width = (np.max(ranges) - np.min(ranges)) / 20
bins = np.arange(0, np.max(ranges) + bin_width, bin_width)
counts, bin_edges = np.histogram(ranges, bins=bins)
# Use bin centers
bin_centers = (bin_edges[:-1] + bin_edges[1:]) / 2
return bin_centers, counts
# Example: Mooring tension time series
t = np.linspace(0, 3600, 36000) # 1 hour
tension = 2000 + 300 * np.sin(2*np.pi*t/10) + 100 * np.sin(2*np.pi*t/3) + 50*np.random.randn(len(t))
ranges, counts = rainflow_counting(tension, bin_width=10)
print(f"Rainflow cycles:")
print(f" Total cycles: {np.sum(counts)}")
print(f" Max range: {np.max(ranges):.1f} kN")
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:
# Cycles to failure
N = calculate_cycles_to_failure(stress_range, sn_curve)
# Damage contribution
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 # Will be filled later
})
# Calculate percentage contributions
for item in damage_breakdown:
item['damage_percent'] = (item['damage'] / total_damage * 100) if total_damage > 0 else 0
# Apply design factor
total_damage_with_df = total_damage * design_factor
# Fatigue life
if total_damage > 0:
fatigue_life = 1.0 / total_damage # In units of analysis duration
else:
fatigue_life = np.inf
return {
'total_damage': total_damage,
'damage_with_design_factor': total_damage_with_df,
'fatigue_life': fatigue_life,
'utilization': total_damage_with_df,
'passed': total_damage_with_df <= 1.0,
'breakdown': damage_breakdown
}
# Example: Calculate fatigue damage
# Assume 1 hour of data, scale to 25 years
hours_per_year = 8760
design_life_years = 25
scale_factor = hours_per_year * design_life_years
# Convert tension ranges to stress (simplified)
stress_ranges = ranges / 100 # kN to MPa (simplified)
cycle_counts_scaled = counts * scale_factor
fatigue_result = calculate_fatigue_damage_miners_rule(
stress_ranges,
cycle_counts_scaled,
sn_f3,
design_factor=10.0
)
print(f"Fatigue Analysis Results:")
print(f" Total damage: {fatigue_result['total_damage']:.4f}")
print(f" With DF=10: {fatigue_result['damage_with_design_factor']:.4f}")
print(f" Utilization: {fatigue_result['utilization']*100:.1f}%")
print(f" Passed: {fatigue_result['passed']}")
print(f" Fatigue life: {fatigue_result['fatigue_life']:.1f} years")
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
"""
# Spectral moments
m0 = np.trapz(spectrum, frequencies)
m2 = np.trapz(spectrum * frequencies**2, frequencies)
m4 = np.trapz(spectrum * frequencies**4, frequencies)
# Zero-crossing frequency
f0 = np.sqrt(m2 / m0)
# Number of zero crossings in duration
N0 = f0 * duration
# Standard deviation of stress
sigma = np.sqrt(m0)
# Damage integral for Rayleigh distribution
# D = N0 * (2*sigma)^m * Γ(1 + m/2) / a
m = sn_curve['m1'] # Use first slope
a = sn_curve['a1']
from scipy.special import gamma
damage = N0 * (2 * sigma)**m * gamma(1 + m/2) / a
# Apply design factor
damage_with_df = damage * design_factor
# Fatigue life
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 / (365.25 * 24 * 3600),
'sigma_stress': sigma,
'zero_crossing_freq': f0
}
# Example
freq_hz = np.linspace(0.01, 0.5, 500)
S_stress = 100 * freq_hz**(-2) # Simplified stress spectrum
fatigue_spectral = spectral_fatigue_narrow_band(
S_stress,
freq_hz,
sn_f3,
duration=3600, # 1 hour
design_factor=10.0
)
# Scale to 25 years
fatigue_spectral['damage_25yr'] = fatigue_spectral['total_damage'] * 8760 * 25
print(f"Spectral Fatigue (25 years):")
print(f" Damage: {fatigue_spectral['damage_25yr']:.4f}")
print(f" Utilization: {fatigue_spectral['damage_25yr'] * 10:.1f}%")
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
"""
# Chain properties
grade_factors = {'R3': 0.0219, 'R4': 0.0246, 'R5': 0.0273}
MBL = grade_factors[chain_grade] * chain_diameter**2 # tonnes
# Cross-sectional area (nominal)
d_mm = chain_diameter
A = np.pi * (d_mm/2)**2 # mm²
# Convert tension to stress
stress_time_series = tension_time_series * 1000 / A # MPa
# Rainflow counting
stress_ranges, cycle_counts = rainflow_counting(stress_time_series)
# Duration of time series
duration_hours = len(tension_time_series) * time_step / 3600
# Scale to design life
hours_total = 8760 * design_life_years
scale_factor = hours_total / duration_hours
cycle_counts_scaled = cycle_counts * scale_factor
# Select S-N curve (DNV: F3 for chain at connector)
sn_curve = get_dnv_sn_curve('F3', thickness=chain_diameter)
# Calculate damage
fatigue_result = calculate_fatigue_damage_miners_rule(
stress_ranges,
cycle_counts_scaled,
sn_curve,
design_factor=10.0 # DNV-OS-E301
)
return {
'chain_diameter_mm': chain_diameter,
'chain_grade': chain_grade,
'MBL_tonnes': MBL,
'design_life_years': design_life_years,
'fatigue_damage': fatigue_result['total_damage'],
'utilization': fatigue_result['utilization'],
'passed': fatigue_result['passed'],
'fatigue_life_years': fatigue_result['fatigue_life'],
'stress_ranges': stress_ranges,
'cycle_counts': cycle_counts_scaled
}
# Example
tension = 2000 + 400 * np.sin(2*np.pi*np.arange(36000)/100) # 1 hour, varied tension
chain_fatigue = mooring_chain_fatigue_analysis(
tension,
chain_diameter=127, # mm
chain_grade='R4',
design_life_years=25,
time_step=0.1
)
print(f"Mooring Chain Fatigue:")
print(f" Diameter: {chain_fatigue['chain_diameter_mm']} mm {chain_fatigue['chain_grade']}")
print(f" MBL: {chain_fatigue['MBL_tonnes']:.1f} tonnes")
print(f" Damage (25 years): {chain_fatigue['fatigue_damage']:.4f}")
print(f" Utilization: {chain_fatigue['utilization']*100:.1f}%")
print(f" Status: {'PASS' if chain_fatigue['passed'] else 'FAIL'}")
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)
# Load tension data
df = pd.read_csv(tension_file)
tension = df['Tension'].values # kN
time = df['Time'].values # seconds
# Rainflow counting
ranges, counts = rainflow_counting(tension)
# Chain properties
chain_diameter = 127 # mm
sn_curve = get_dnv_sn_curve('F3', thickness=chain_diameter)
# Calculate fatigue
fatigue = mooring_chain_fatigue_analysis(
tension,
chain_diameter=chain_diameter,
design_life_years=25,
time_step=time[1] - time[0]
)
# Create visualizations
fig = make_subplots(
rows=2, cols=2,
subplot_titles=(
'Tension Time Series',
'Rainflow Histogram',
'S-N Curve with Load Points',
'Damage Breakdown'
)
)
# Plot 1: Time series
fig.add_trace(
go.Scatter(x=time, y=tension, name='Tension', line=dict(width=1)),
row=1, col=1
)
# Plot 2: Rainflow histogram
fig.add_trace(
go.Bar(x=ranges, y=counts, name='Cycle Counts'),
row=1, col=2
)
# Plot 3: S-N curve
stress_plot = np.logspace(0, 3, 100)
N_plot = sn_curve['a1'] / stress_plot**sn_curve['m1']
fig.add_trace(
go.Scatter(
x=N_plot, y=stress_plot,
mode='lines', name='S-N Curve F3',
line=dict(color='red')
),
row=2, col=1
)
# Add load points
stress_ranges_chain = fatigue['stress_ranges']
N_values = [calculate_cycles_to_failure(s, sn_curve) for s in stress_ranges_chain]
fig.add_trace(
go.Scatter(
x=N_values, y=stress_ranges_chain,
mode='markers', name='Load Points',
marker=dict(size=8)
),
row=2, col=1
)
fig.update_xaxes(type='log', title_text='Cycles N', row=2, col=1)
fig.update_yaxes(type='log', title_text='Stress Range (MPa)', row=2, col=1)
# Plot 4: Damage breakdown (top contributors)
breakdown = fatigue_result['breakdown'][:10] # Top 10
damage_pct = [item['damage_percent'] for item in breakdown]
stress_labels = [f"{item['stress_range']:.1f} MPa" for item in breakdown]
fig.add_trace(
go.Bar(x=stress_labels, y=damage_pct, name='Damage %'),
row=2, col=2
)
fig.update_layout(height=800, showlegend=True, title_text='Fatigue Assessment Report')
fig.write_html(output_path / 'fatigue_assessment.html')
# Export summary
summary = pd.DataFrame({
'Parameter': [
'Chain Diameter (mm)',
'Chain Grade',
'MBL (tonnes)',
'Design Life (years)',
'Total Damage',
'Utilization (%)',
'Fatigue Life (years)',
'Status'
],
'Value': [
fatigue['chain_diameter_mm'],
fatigue['chain_grade'],
f"{fatigue['MBL_tonnes']:.1f}",
fatigue['design_life_years'],
f"{fatigue['fatigue_damage']:.4f}",
f"{fatigue['utilization']*100:.1f}",
f"{fatigue['fatigue_life_years']:.1f}",
'PASS' if fatigue['passed'] else 'FAIL'
]
})
summary.to_csv(output_path / 'fatigue_summary.csv', index=False)
print(f"✓ Fatigue assessment complete")
print(f" Output: {output_dir}")
print(f" Status: {'PASS' if fatigue['passed'] else 'FAIL'}")
return 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!
More Other skills
brand-guidelines
Applies Anthropic's official brand colors and typography to any sort of artifact that may benefit from having Anthropic's look-and-feel. Use it when brand colors or style guidelines, visual formatting, or company design standards apply.
theme-factory
Toolkit for styling artifacts with a theme. These artifacts can be slides, docs, reportings, HTML landing pages, etc. There are 10 pre-set themes with colors/fonts that you can apply to any artifact that has been creating, or can generate a new theme on-the-fly.
internal-comms
A set of resources to help me write all kinds of internal communications, using the formats that my company likes to use. Claude should use this skill whenever asked to write some sort of internal communications (status reports, leadership updates, 3P updates, company newsletters, FAQs, incident reports, project updates, etc.).
Fundamental design principles for writing maintainable, testable Python code.
responsive-design
Modern responsive layouts using container queries, fluid typography, CSS Grid, and mobile-first strategies.
Generate Playwright tests from scenarios using interactive browser exploration and validation.
Automated website exploration and test case generation using Playwright.
Modern React state management with Redux Toolkit, Zustand, Jotai, and React Query for every state category.
gdpr-data-handling
GDPR-compliant data handling with consent management, data subject rights, and privacy controls.
Python error handling patterns including input validation, exception hierarchies, and partial failure handling. Use when implementing validation logic, designing exception strategies, handling batch processing failures, or building robust APIs.
refactor-plan
Detailed multi-file refactor planning with safety checks and rollback guidance.
