Claude-skill-registry fatigue-analysis
Fatigue analysis for offshore structures including S-N curves, rainflow counting, Miner's rule, and DNV standards
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/fatigue-analysis" ~/.claude/skills/majiayu000-claude-skill-registry-fatigue-analysis && rm -rf "$T"
manifest:
skills/data/fatigue-analysis/SKILL.mdsource content
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!