spec / app /services /fatigue_postprocessor.py
ronanroam
fix: add proper libraries
a9c06c5
"""
Fatigue Post-Processor Service
Processes OpenModelica simulation results to extract and analyze fatigue data.
Implements Miner's rule damage assessment and S-N curve analysis.
External dependencies:
- scipy.stats: Weibull distribution for reliability analysis
- numpy: Numerical operations
"""
from dataclasses import dataclass, asdict
from typing import List, Dict, Any, Optional
from pathlib import Path
import csv
import math
import numpy as np
from scipy import stats
@dataclass
class FatigueDamageState:
"""Current fatigue damage state for a component."""
component_name: str
damage_fraction: float # 0-1, failure at 1
cycle_count: float
dominant_stress_mpa: float
estimated_remaining_cycles: float
damage_rate_per_hour: float
limiting_mode: str # "mechanical", "thermal", "seal"
@dataclass
class ComponentLifeResult:
"""Life estimation result for a component."""
component_name: str
current_damage: float
estimated_life_hours: float
estimated_life_cycles: float
safety_factor: float
recommendation: str
urgency: str # "normal", "soon", "critical"
@dataclass
class FatigueAnalysisResult:
"""Complete fatigue analysis result."""
success: bool
message: str
simulation_hours: float
components: List[FatigueDamageState]
life_estimates: List[ComponentLifeResult]
overall_limiting_factor: str
overall_remaining_life_hours: float
fatigue_safety_margin: float
def parse_cylinder_fatigue_results(mat_file_path: Path) -> Dict[str, List[float]]:
"""
Parse OpenModelica .mat or CSV results file for fatigue data.
Returns time series of fatigue-related variables.
"""
results = {
'time': [],
'rodFatigueDamage': [],
'sealFatigueCycles': [],
'cycleCount': [],
'rodStress': [],
'thermalStress': [],
'combinedStress': [],
'oilTemperature': [],
}
csv_path = mat_file_path.with_suffix('.csv')
if csv_path.exists():
with open(csv_path, 'r') as f:
reader = csv.DictReader(f)
for row in reader:
try:
results['time'].append(float(row.get('time', 0)))
results['rodFatigueDamage'].append(float(row.get('rodFatigueDamage', 0)))
results['sealFatigueCycles'].append(float(row.get('sealFatigueCycles', 0)))
results['cycleCount'].append(float(row.get('cycleCount', 0)))
results['rodStress'].append(float(row.get('rodStress', 0)))
results['thermalStress'].append(float(row.get('thermalStress', 0)))
results['combinedStress'].append(float(row.get('combinedStress', 0)))
results['oilTemperature'].append(float(row.get('oilTemperature', 293.15)))
except (ValueError, KeyError):
continue
return results
def analyze_cylinder_fatigue(
results: Dict[str, List[float]],
seal_max_cycles: float = 100000,
rod_endurance_limit_mpa: float = 200,
) -> FatigueAnalysisResult:
"""
Analyze fatigue data from cylinder simulation.
Args:
results: Parsed simulation results
seal_max_cycles: Maximum seal cycles before replacement
rod_endurance_limit_mpa: Rod endurance limit for infinite life
Returns:
FatigueAnalysisResult with damage assessment
"""
if not results['time'] or len(results['time']) == 0:
return FatigueAnalysisResult(
success=False,
message="No simulation data available",
simulation_hours=0,
components=[],
life_estimates=[],
overall_limiting_factor="unknown",
overall_remaining_life_hours=0,
fatigue_safety_margin=0
)
sim_time = results['time'][-1] if results['time'] else 0
sim_hours = sim_time / 3600 # Convert seconds to hours
# Rod fatigue analysis
rod_damage = results['rodFatigueDamage'][-1] if results['rodFatigueDamage'] else 0
rod_cycles = results['cycleCount'][-1] if results['cycleCount'] else 0
rod_stress = max(abs(s) for s in results['rodStress']) if results['rodStress'] else 0
# Seal fatigue analysis
seal_cycles = results['sealFatigueCycles'][-1] if results['sealFatigueCycles'] else 0
seal_damage = seal_cycles / seal_max_cycles if seal_max_cycles > 0 else 0
# Thermal analysis
max_temp = max(results['oilTemperature']) if results['oilTemperature'] else 293.15
thermal_stress = max(abs(s) for s in results['thermalStress']) if results['thermalStress'] else 0
# Calculate damage rates
rod_damage_rate = rod_damage / sim_hours if sim_hours > 0 else 0
seal_damage_rate = seal_damage / sim_hours if sim_hours > 0 else 0
# Component states
components = [
FatigueDamageState(
component_name="piston_rod",
damage_fraction=min(1.0, rod_damage),
cycle_count=rod_cycles,
dominant_stress_mpa=rod_stress,
estimated_remaining_cycles=(1 - rod_damage) * rod_cycles / rod_damage if rod_damage > 0 else 1e9,
damage_rate_per_hour=rod_damage_rate,
limiting_mode="mechanical" if rod_stress > thermal_stress else "thermal"
),
FatigueDamageState(
component_name="piston_seal",
damage_fraction=min(1.0, seal_damage),
cycle_count=seal_cycles,
dominant_stress_mpa=0, # Seals don't have stress-based fatigue
estimated_remaining_cycles=seal_max_cycles - seal_cycles,
damage_rate_per_hour=seal_damage_rate,
limiting_mode="seal"
),
]
# Life estimates
life_estimates = []
for comp in components:
if comp.damage_rate_per_hour > 0:
remaining_life = (1 - comp.damage_fraction) / comp.damage_rate_per_hour
else:
remaining_life = 1e9 # Effectively infinite
safety_factor = (1 - comp.damage_fraction) / max(0.001, comp.damage_fraction)
if comp.damage_fraction > 0.8:
urgency = "critical"
recommendation = f"Replace {comp.component_name} immediately"
elif comp.damage_fraction > 0.5:
urgency = "soon"
recommendation = f"Schedule {comp.component_name} replacement"
else:
urgency = "normal"
recommendation = f"{comp.component_name} in good condition"
life_estimates.append(ComponentLifeResult(
component_name=comp.component_name,
current_damage=comp.damage_fraction,
estimated_life_hours=remaining_life,
estimated_life_cycles=comp.estimated_remaining_cycles,
safety_factor=safety_factor,
recommendation=recommendation,
urgency=urgency
))
# Overall assessment
limiting_component = max(components, key=lambda c: c.damage_fraction)
overall_remaining = min(le.estimated_life_hours for le in life_estimates)
overall_safety = min(le.safety_factor for le in life_estimates)
return FatigueAnalysisResult(
success=True,
message=f"Fatigue analysis complete: {sim_hours:.2f} hours simulated",
simulation_hours=sim_hours,
components=components,
life_estimates=life_estimates,
overall_limiting_factor=limiting_component.component_name,
overall_remaining_life_hours=overall_remaining,
fatigue_safety_margin=overall_safety
)
def fatigue_result_to_dict(result: FatigueAnalysisResult) -> Dict[str, Any]:
"""Convert FatigueAnalysisResult to dictionary for JSON response."""
return {
"success": result.success,
"message": result.message,
"simulation_hours": round(result.simulation_hours, 2),
"components": [asdict(c) for c in result.components],
"life_estimates": [asdict(le) for le in result.life_estimates],
"overall_limiting_factor": result.overall_limiting_factor,
"overall_remaining_life_hours": round(result.overall_remaining_life_hours, 0),
"fatigue_safety_margin": round(result.fatigue_safety_margin, 2),
"fatigue_analysis": {
"rod_damage_fraction": result.components[0].damage_fraction if result.components else 0,
"seal_damage_fraction": result.components[1].damage_fraction if len(result.components) > 1 else 0,
"limiting_mode": result.components[0].limiting_mode if result.components else "unknown",
}
}
def calculate_temperature_derating(
temperature_c: float,
base_yield_strength_mpa: float,
base_fatigue_limit_mpa: float,
reference_temp_c: float = 20.0
) -> Dict[str, float]:
"""
Calculate temperature-derated material properties.
Property derating:
- Yield strength: -0.2% per °C above reference
- Fatigue limit: -0.3% per °C above reference
Args:
temperature_c: Operating temperature in Celsius
base_yield_strength_mpa: Yield strength at reference temperature
base_fatigue_limit_mpa: Fatigue limit at reference temperature
reference_temp_c: Reference temperature (default 20°C)
Returns:
Dictionary with derated properties
"""
delta_t = temperature_c - reference_temp_c
# Derating factors (only apply above reference temp)
if delta_t > 0:
yield_factor = 1 - 0.002 * delta_t
fatigue_factor = 1 - 0.003 * delta_t
else:
yield_factor = 1.0
fatigue_factor = 1.0
# Ensure factors don't go below reasonable limits
yield_factor = max(0.5, yield_factor)
fatigue_factor = max(0.4, fatigue_factor)
return {
"temperature_c": temperature_c,
"delta_t": delta_t,
"yield_strength_mpa": base_yield_strength_mpa * yield_factor,
"yield_derating_factor": yield_factor,
"fatigue_limit_mpa": base_fatigue_limit_mpa * fatigue_factor,
"fatigue_derating_factor": fatigue_factor,
}
# Material property database with S-N curve data from test certificates
# Reference: ASM Handbook Vol.19, AZoM AISI 4140 datasheet, U. Waterloo FDE
# Validated against: Shot Peener Library 2017, ResearchGate fatigue studies
MATERIAL_THERMAL_PROPERTIES = {
"AISI_4140": {
# From AZoM AISI 4140 (UNS G41400) datasheet
"thermal_expansion_coeff": 12.3e-6, # 1/°C
"thermal_conductivity": 42.7, # W/(m·K)
"youngs_modulus_mpa": 205000, # 190-210 GPa range, using midpoint
"yield_strength_mpa": 655, # Q&T condition (415 MPa annealed)
"ultimate_strength_mpa": 1020, # Q&T condition
# Fatigue limit validated: 310 MPa at 10^7 cycles (Otai Steel, research consensus)
# Range is 250-540 MPa depending on treatment; using validated 310 MPa
"fatigue_limit_mpa": 310, # At 10^7 cycles, R=-1 (VALIDATED)
"poisson_ratio": 0.29, # 0.27-0.30 range
"density_kg_m3": 7850, # From AZoM
"hardness_hb": 197, # Brinell hardness
# S-N curve parameters (Basquin-Coffin-Manson) from U. Waterloo Iter_068
"sigma_f_prime": 1200, # Fatigue strength coefficient (MPa)
"b": -0.087, # Fatigue strength exponent
"epsilon_f_prime": 0.58, # Fatigue ductility coefficient
"c": -0.58, # Fatigue ductility exponent
# Surface finish factor (machined) - Peterson's
"surface_factor": 0.72,
# Size factor for 50mm diameter - Shigley
"size_factor": 0.85,
# Endurance ratio (fatigue limit / UTS) - validated ~0.30-0.56
"endurance_ratio": 0.304, # 310/1020
},
"AISI_1045": {
"thermal_expansion_coeff": 11.7e-6,
"thermal_conductivity": 49.8,
"youngs_modulus_mpa": 200000,
"yield_strength_mpa": 530,
"ultimate_strength_mpa": 625,
"fatigue_limit_mpa": 280,
"poisson_ratio": 0.29,
"density_kg_m3": 7870,
"hardness_hb": 179,
"sigma_f_prime": 950,
"b": -0.095,
"epsilon_f_prime": 0.65,
"c": -0.55,
"surface_factor": 0.75,
"size_factor": 0.87,
"endurance_ratio": 0.448,
},
"AISI_52100": { # Bearing steel - validated against NASA IMS data
"thermal_expansion_coeff": 12.5e-6,
"thermal_conductivity": 46.6,
"youngs_modulus_mpa": 210000,
"yield_strength_mpa": 2030,
"ultimate_strength_mpa": 2250,
"fatigue_limit_mpa": 700,
"poisson_ratio": 0.3,
"density_kg_m3": 7810,
"hardness_hb": 650, # Hardened
"sigma_f_prime": 2100,
"b": -0.076,
"epsilon_f_prime": 0.18,
"c": -0.62,
"surface_factor": 0.90, # Ground finish
"size_factor": 0.80,
"endurance_ratio": 0.311,
# Hertzian contact parameters from research
"cyclic_plasticity_threshold_mpa": 510, # Southampton research
"max_hertz_operating_gpa": 3.0, # Typical max for bearings
},
}
# Validated operating ranges from ZeMA hydraulic dataset (2205 cycles)
ZEMA_VALIDATION_RANGES = {
"pressure_mpa": {"min": 13.31, "max": 19.19, "mean": 16.05},
"pressure_delta_mpa": {"typical": 4.74, "std": 0.16},
"temperature_c": {"min": 35.0, "max": 58.2, "mean": 45.4},
"flow_rate_lpm": {"min": 0.0, "max": 20.5, "mean": 6.2},
"total_cycles": 2205,
"cycle_duration_s": 60,
}
# Cylinder fatigue validation data from ResearchGate studies
CYLINDER_FATIGUE_VALIDATION = {
"oil_port_failure_cycles": 360000, # Zone 5 failure observed
"reliability_decline_start_cycles": 250000, # Slow decline phase
"reliability_decline_end_cycles": 800000, # Sharp decline phase end
"critical_stress_zones_mpa": {
"zone_1": 208.5,
"zone_5_port": 387.2,
"zone_10": 285.0,
},
}
# Stress Concentration Factors (SCF) - Peterson's Stress Concentration Factors
STRESS_CONCENTRATION_FACTORS = {
"plain_cylinder": 1.0,
"port_hole": 2.5, # Cross-drilled hole in cylinder wall
"threaded_end": 3.0, # External thread
"internal_thread": 2.8,
"shoulder_fillet_sharp": 2.2, # r/d = 0.02
"shoulder_fillet_medium": 1.7, # r/d = 0.1
"shoulder_fillet_generous": 1.3, # r/d = 0.25
"keyway": 2.0,
"spline": 1.8,
"weld_butt": 1.5, # Ground flush
"weld_fillet": 2.0,
"seal_groove": 1.8,
"o_ring_groove": 1.6,
}
# Elastomer seal material properties
SEAL_MATERIALS = {
"NBR": { # Nitrile rubber (Buna-N)
"fatigue_limit_mpa": 8.0, # Cyclic stress limit
"max_temp_c": 100,
"min_temp_c": -30,
"compression_set_factor": 0.15, # At 70°C, 1000 hrs
"wear_coefficient": 1.2e-6, # mm³/(N·m)
},
"FKM": { # Viton/Fluoroelastomer
"fatigue_limit_mpa": 12.0,
"max_temp_c": 200,
"min_temp_c": -20,
"compression_set_factor": 0.10,
"wear_coefficient": 0.8e-6,
},
"PTFE": { # Teflon
"fatigue_limit_mpa": 15.0,
"max_temp_c": 260,
"min_temp_c": -200,
"compression_set_factor": 0.05,
"wear_coefficient": 0.5e-6,
},
"EPDM": { # Ethylene propylene
"fatigue_limit_mpa": 6.0,
"max_temp_c": 150,
"min_temp_c": -50,
"compression_set_factor": 0.20,
"wear_coefficient": 1.5e-6,
},
}
# Weibull reliability factors (ISO 281:2007 Table 12)
WEIBULL_RELIABILITY = {
90: 1.00, # L10 - 90% reliability
95: 0.62, # L5
96: 0.53,
97: 0.44,
98: 0.33,
99: 0.21, # L1 - 99% reliability
99.5: 0.13,
99.9: 0.04,
}
def calculate_stress_concentration_factor(
geometry_type: str = "plain_cylinder",
notch_radius_mm: float = 0,
stress_gradient_mm: float = 0,
ultimate_strength_mpa: float = 1000
) -> dict:
"""
Calculate stress concentration and fatigue notch factor.
Uses Peterson's SCF and Neuber's rule for fatigue notch factor.
Args:
geometry_type: Type of stress raiser
notch_radius_mm: Notch root radius
stress_gradient_mm: Stress gradient length
ultimate_strength_mpa: Material ultimate strength
Returns:
Dict with Kt (theoretical SCF) and Kf (fatigue notch factor)
"""
# Get theoretical stress concentration factor
Kt = STRESS_CONCENTRATION_FACTORS.get(geometry_type, 1.0)
# Calculate notch sensitivity (Peterson's equation)
# q = 1 / (1 + a/r) where a is material constant
# a = 0.0254 * (2070/Su)^1.8 for steels (Su in MPa)
if notch_radius_mm > 0 and ultimate_strength_mpa > 0:
a = 0.0254 * (2070 / ultimate_strength_mpa) ** 1.8
q = 1 / (1 + a / notch_radius_mm)
else:
q = 0.9 # Default notch sensitivity for machined parts
# Fatigue notch factor
Kf = 1 + q * (Kt - 1)
return {
"Kt": round(Kt, 2),
"Kf": round(Kf, 2),
"notch_sensitivity": round(q, 3),
"geometry_type": geometry_type,
}
def apply_goodman_correction(
stress_amplitude_mpa: float,
stress_mean_mpa: float,
fatigue_limit_mpa: float,
ultimate_strength_mpa: float
) -> dict:
"""
Apply Goodman mean stress correction.
Goodman equation: σa/Se + σm/Su = 1
Equivalent fully-reversed stress: σar = σa / (1 - σm/Su)
Args:
stress_amplitude_mpa: Alternating stress amplitude
stress_mean_mpa: Mean stress
fatigue_limit_mpa: Fatigue limit at R=-1
ultimate_strength_mpa: Ultimate tensile strength
Returns:
Dict with corrected stresses and safety factor
"""
if ultimate_strength_mpa <= 0:
return {"error": "Invalid ultimate strength"}
# Equivalent fully-reversed stress (Goodman)
mean_factor = 1 - abs(stress_mean_mpa) / ultimate_strength_mpa
if mean_factor <= 0:
equivalent_amplitude = float('inf')
else:
equivalent_amplitude = stress_amplitude_mpa / mean_factor
# Safety factor using modified Goodman
if stress_amplitude_mpa > 0:
nf = fatigue_limit_mpa / equivalent_amplitude
else:
nf = 10.0
# Also calculate Gerber correction for comparison
# σa/Se + (σm/Su)² = 1
gerber_factor = 1 - (stress_mean_mpa / ultimate_strength_mpa) ** 2
if gerber_factor > 0:
gerber_equivalent = stress_amplitude_mpa / gerber_factor
gerber_sf = fatigue_limit_mpa / gerber_equivalent if gerber_equivalent > 0 else 10.0
else:
gerber_sf = 0
return {
"stress_amplitude_mpa": round(stress_amplitude_mpa, 2),
"stress_mean_mpa": round(stress_mean_mpa, 2),
"equivalent_amplitude_goodman": round(equivalent_amplitude, 2),
"goodman_safety_factor": round(nf, 2),
"gerber_safety_factor": round(gerber_sf, 2),
"mean_stress_ratio": round(stress_mean_mpa / ultimate_strength_mpa, 3),
}
def calculate_weibull_life(
l10_life: float,
reliability_percent: float = 90,
weibull_slope: float = 1.5
) -> dict:
"""
Calculate life at different reliability levels using Weibull distribution.
Uses scipy.stats.weibull_min for proper Weibull distribution calculations.
This replaces the manual formula with a validated statistical library.
Args:
l10_life: L10 life (90% survival)
reliability_percent: Desired reliability (90-99.9%)
weibull_slope: Weibull shape parameter (β or e)
Returns:
Dict with life at requested reliability and statistics
"""
# Validate inputs
if reliability_percent >= 100 or reliability_percent <= 0:
reliability_percent = 90
if l10_life <= 0:
l10_life = 1.0
# Weibull distribution parameters
# For Weibull: F(t) = 1 - exp(-(t/η)^β)
# At L10 (90% survival): F = 0.1, so 1 - exp(-(L10/η)^β) = 0.1
# Solving: η = L10 / (-ln(0.9))^(1/β)
beta = weibull_slope # Shape parameter
eta = l10_life / ((-np.log(0.9)) ** (1 / beta)) # Scale parameter
# Create Weibull distribution using scipy
# scipy.stats.weibull_min uses different parameterization:
# weibull_min(c, loc, scale) where c=shape, scale=eta
weibull_dist = stats.weibull_min(c=beta, loc=0, scale=eta)
# Calculate life at requested reliability
# Reliability R = 1 - F(t), so F = 1 - R
failure_prob = 1 - (reliability_percent / 100)
ln_life = weibull_dist.ppf(failure_prob) # Percent point function (inverse CDF)
# Calculate standard life metrics using scipy
l10_calc = weibull_dist.ppf(0.10) # 90% reliability
l50_life = weibull_dist.ppf(0.50) # 50% reliability (median)
# Mean life (MTTF) using scipy
mean_life = weibull_dist.mean()
# Standard deviation
std_life = weibull_dist.std()
# Confidence intervals using scipy (90% CI)
ci_lower = weibull_dist.ppf(0.05)
ci_upper = weibull_dist.ppf(0.95)
return {
f"L{int(100-reliability_percent)}_life": round(ln_life, 0),
"L10_life": round(l10_calc, 0),
"L50_life": round(l50_life, 0),
"mean_life_MTTF": round(mean_life, 0),
"std_life": round(std_life, 0),
"reliability_percent": reliability_percent,
"weibull_shape_beta": beta,
"weibull_scale_eta": round(eta, 0),
"confidence_interval_90": {
"lower": round(ci_lower, 0),
"upper": round(ci_upper, 0),
},
"distribution": "scipy.stats.weibull_min"
}
def calculate_von_mises_stress(
sigma_hoop: float,
sigma_radial: float,
sigma_axial: float = 0,
tau_shear: float = 0
) -> dict:
"""
Calculate von Mises equivalent stress for multi-axial loading.
σ_vm = √[(σ1-σ2)² + (σ2-σ3)² + (σ3-σ1)² + 6τ²] / √2
For thick-walled cylinder:
- σ1 = σ_hoop (tangential)
- σ2 = σ_axial (longitudinal)
- σ3 = σ_radial (negative, compressive)
"""
# Principal stresses
s1 = sigma_hoop
s2 = sigma_axial
s3 = sigma_radial # Usually negative (compressive) at inner surface
# Von Mises equivalent stress
vm_squared = ((s1-s2)**2 + (s2-s3)**2 + (s3-s1)**2 + 6*tau_shear**2) / 2
sigma_vm = math.sqrt(vm_squared) if vm_squared > 0 else 0
# Tresca (maximum shear) stress for comparison
sigma_tresca = max(abs(s1-s2), abs(s2-s3), abs(s3-s1))
return {
"sigma_hoop": round(sigma_hoop, 2),
"sigma_radial": round(sigma_radial, 2),
"sigma_axial": round(sigma_axial, 2),
"tau_shear": round(tau_shear, 2),
"von_mises_stress": round(sigma_vm, 2),
"tresca_stress": round(sigma_tresca, 2),
"stress_state": "multi-axial",
}
def calculate_hertzian_subsurface_stress(
contact_pressure_mpa: float,
contact_width_mm: float = 1.0,
poisson_ratio: float = 0.3,
material: str = "AISI_52100"
) -> dict:
"""
Calculate subsurface stress distribution for Hertzian contact.
Validated against:
- ScienceDirect Rolling Contact Fatigue studies
- Southampton Research (cyclic plasticity thresholds)
- SKF Evolution bearing progression data
Maximum shear stress occurs at depth z = 0.78a for line contact.
τ_max = 0.30 * p_max for line contact (ν = 0.3)
Args:
contact_pressure_mpa: Maximum Hertzian contact pressure
contact_width_mm: Half-width of contact zone
poisson_ratio: Material Poisson's ratio
material: Material for cyclic plasticity threshold
Returns:
Dict with subsurface stress distribution
"""
# Maximum shear stress (causes subsurface fatigue)
# τ_max ≈ 0.30 * p_max for steel (ν = 0.3) - validated ratio
tau_max = 0.30 * contact_pressure_mpa
# Depth of maximum shear stress - validated at 0.78 * contact_width
z_tau_max = 0.78 * contact_width_mm
# Orthogonal shear stress (causes surface pitting)
# τ_ortho ≈ 0.25 * p_max at z = 0.5a
tau_ortho = 0.25 * contact_pressure_mpa
z_ortho = 0.50 * contact_width_mm
# von Mises equivalent stress at depth
# σ_vm ≈ 0.56 * p_max at z = 0.7a
sigma_vm_subsurface = 0.56 * contact_pressure_mpa
# Cyclic plasticity thresholds from Southampton research
# AISI 52100: 510 MPa, AISI 1070: 640 MPa
cyclic_plasticity_thresholds = {
"AISI_52100": 510,
"AISI_1070": 640,
"AISI_4140": 450, # Estimated from fatigue data
}
threshold = cyclic_plasticity_thresholds.get(material, 510)
# Determine failure mode based on validated thresholds
# Subsurface spalling: τ_max exceeds cyclic plasticity threshold
# Surface pitting: orthogonal shear causes surface-initiated cracks
# Normal wear: stresses within safe operating range
# Operating pressure range for bearings: 1-3 GPa (1000-3000 MPa)
if contact_pressure_mpa > 3000:
failure_mode = "severe_overload"
life_assessment = "immediate_failure_risk"
elif tau_max > threshold:
failure_mode = "subsurface_spalling"
life_assessment = "reduced_life"
elif tau_ortho > threshold * 0.7:
failure_mode = "surface_pitting"
life_assessment = "moderate_life"
else:
failure_mode = "normal_wear"
life_assessment = "full_life"
# Estimated cycles to failure based on SKF progression data
# Incubation: 50-60M revs, Initial damage: 30-40M, Accelerated: 20-25M
if failure_mode == "normal_wear":
estimated_cycles_millions = 100 # Conservative full life
elif failure_mode == "surface_pitting":
estimated_cycles_millions = 50 # Reduced but still significant
elif failure_mode == "subsurface_spalling":
estimated_cycles_millions = 20 # Accelerated failure
else:
estimated_cycles_millions = 1 # Severe overload
return {
"contact_pressure_mpa": round(contact_pressure_mpa, 1),
"contact_pressure_gpa": round(contact_pressure_mpa / 1000, 3),
"max_shear_stress_mpa": round(tau_max, 1),
"max_shear_depth_mm": round(z_tau_max, 2),
"orthogonal_shear_mpa": round(tau_ortho, 1),
"orthogonal_shear_depth_mm": round(z_ortho, 2),
"subsurface_von_mises_mpa": round(sigma_vm_subsurface, 1),
"cyclic_plasticity_threshold_mpa": threshold,
"failure_mode": failure_mode,
"life_assessment": life_assessment,
"estimated_cycles_millions": estimated_cycles_millions,
"validation_notes": [
f"τ_max/p_max = 0.30 (validated for ν=0.3)",
f"z_τmax/a = 0.78 (line contact)",
f"Cyclic plasticity threshold ({material}): {threshold} MPa",
"Operating range: 1-3 GPa typical for bearings",
],
}
def analyze_fatigue_from_pressure_cycles(
bore_diameter_mm: float,
wall_thickness_mm: float,
max_pressure_mpa: float,
min_pressure_mpa: float = 0.0,
cycle_frequency_hz: float = 1.0,
operating_temperature_c: float = 40.0,
simulation_hours: float = 1.0,
material: str = "AISI_4140",
has_port_holes: bool = True,
seal_material: str = "NBR",
reliability_percent: float = 90.0
) -> dict:
"""
Analyze fatigue from pressure cycling with advanced models.
Features:
- Stress concentration factors (SCF) for ports/threads
- Multi-axial stress (von Mises)
- Goodman mean stress correction
- Material-specific S-N curves
- Weibull reliability analysis
- Seal material fatigue
Args:
bore_diameter_mm: Inner diameter of cylinder
wall_thickness_mm: Wall thickness
max_pressure_mpa: Maximum cycling pressure
min_pressure_mpa: Minimum cycling pressure
cycle_frequency_hz: Cycle frequency in Hz
operating_temperature_c: Operating temperature
simulation_hours: Duration to simulate
material: Material identifier
has_port_holes: Whether cylinder has port holes (adds SCF)
seal_material: Seal compound type (NBR, FKM, PTFE, EPDM)
reliability_percent: Desired reliability level (90-99.9%)
Returns:
Dict with comprehensive fatigue analysis results
"""
# Calculate geometry
inner_radius = bore_diameter_mm / 2
outer_radius = inner_radius + wall_thickness_mm
k = outer_radius / inner_radius # radius ratio
# Get material properties
mat_props = MATERIAL_THERMAL_PROPERTIES.get(material, MATERIAL_THERMAL_PROPERTIES["AISI_4140"])
base_fatigue_limit = mat_props.get("fatigue_limit_mpa", 350)
yield_strength = mat_props.get("yield_strength_mpa", 655)
ultimate_strength = mat_props.get("ultimate_strength_mpa", 1020)
sigma_f_prime = mat_props.get("sigma_f_prime", 1200)
b_exponent = mat_props.get("b", -0.087)
surface_factor = mat_props.get("surface_factor", 0.72)
size_factor = mat_props.get("size_factor", 0.85)
poisson = mat_props.get("poisson_ratio", 0.3)
# === LAMÉ STRESS CALCULATION (multi-axial) ===
# Hoop stress at inner surface: σ_θ = p × (k² + 1) / (k² - 1)
max_hoop_stress = max_pressure_mpa * (k**2 + 1) / (k**2 - 1)
min_hoop_stress = min_pressure_mpa * (k**2 + 1) / (k**2 - 1)
# Radial stress at inner surface: σ_r = -p (compressive)
max_radial_stress = -max_pressure_mpa
min_radial_stress = -min_pressure_mpa
# Axial stress (closed-end cylinder): σ_a = p × r_i² / (r_o² - r_i²)
max_axial_stress = max_pressure_mpa * inner_radius**2 / (outer_radius**2 - inner_radius**2)
min_axial_stress = min_pressure_mpa * inner_radius**2 / (outer_radius**2 - inner_radius**2)
# === VON MISES EQUIVALENT STRESS ===
vm_max = calculate_von_mises_stress(max_hoop_stress, max_radial_stress, max_axial_stress)
vm_min = calculate_von_mises_stress(min_hoop_stress, min_radial_stress, min_axial_stress)
max_von_mises = vm_max["von_mises_stress"]
min_von_mises = vm_min["von_mises_stress"]
# === STRESS CONCENTRATION FACTORS ===
if has_port_holes:
scf_data = calculate_stress_concentration_factor(
geometry_type="port_hole",
notch_radius_mm=2.0, # Typical port edge radius
ultimate_strength_mpa=ultimate_strength
)
Kf = scf_data["Kf"]
else:
scf_data = calculate_stress_concentration_factor(
geometry_type="plain_cylinder",
ultimate_strength_mpa=ultimate_strength
)
Kf = scf_data["Kf"]
# Apply SCF to stresses
max_local_stress = max_von_mises * Kf
min_local_stress = min_von_mises * Kf
# Stress amplitude and mean (at stress concentration)
stress_amplitude = (max_local_stress - min_local_stress) / 2
stress_mean = (max_local_stress + min_local_stress) / 2
# === TEMPERATURE DERATING ===
temp_derating = calculate_temperature_derating(
operating_temperature_c,
yield_strength,
base_fatigue_limit
)
derated_yield = temp_derating["yield_strength_mpa"]
derated_fatigue_limit = temp_derating["fatigue_limit_mpa"]
# Apply surface and size factors to fatigue limit
effective_fatigue_limit = derated_fatigue_limit * surface_factor * size_factor
# === GOODMAN MEAN STRESS CORRECTION ===
goodman_data = apply_goodman_correction(
stress_amplitude,
stress_mean,
effective_fatigue_limit,
ultimate_strength
)
equivalent_amplitude = goodman_data["equivalent_amplitude_goodman"]
goodman_sf = goodman_data["goodman_safety_factor"]
# === S-N CURVE (Basquin equation with actual parameters) ===
# σ_a = σ'_f × (2N_f)^b
# Adjusted for temperature and correction factors
adjusted_sigma_f = sigma_f_prime * temp_derating["fatigue_derating_factor"]
if equivalent_amplitude > 0 and equivalent_amplitude < adjusted_sigma_f:
cycles_to_failure = ((equivalent_amplitude / adjusted_sigma_f) ** (1/b_exponent)) / 2
elif equivalent_amplitude <= 0:
cycles_to_failure = 1e12 # Essentially infinite
else:
cycles_to_failure = 100 # Very short life
cycles_to_failure = max(100, min(cycles_to_failure, 1e12))
# Total cycles in simulation
total_cycles = cycle_frequency_hz * simulation_hours * 3600
# === MINER'S RULE DAMAGE ===
damage_fraction = total_cycles / cycles_to_failure if cycles_to_failure > 0 else 1.0
damage_fraction = min(1.0, damage_fraction)
# === WEIBULL RELIABILITY ANALYSIS ===
l10_life_hours = (cycles_to_failure - total_cycles) / (cycle_frequency_hz * 3600) if cycle_frequency_hz > 0 else 1e9
l10_life_hours = max(0, min(l10_life_hours, 1e9))
weibull_data = calculate_weibull_life(
l10_life=l10_life_hours,
reliability_percent=reliability_percent,
weibull_slope=1.5 # Typical for fatigue
)
# === SEAL FATIGUE ANALYSIS ===
seal_props = SEAL_MATERIALS.get(seal_material, SEAL_MATERIALS["NBR"])
seal_fatigue_limit = seal_props["fatigue_limit_mpa"]
seal_max_temp = seal_props["max_temp_c"]
# Seal contact stress approximation (radial squeeze + pressure)
seal_squeeze_stress = max_pressure_mpa * 0.3 # Approximate
seal_pressure_stress = max_pressure_mpa * 0.5 # Direct pressure load
seal_total_stress = seal_squeeze_stress + seal_pressure_stress
# Temperature effect on seal
if operating_temperature_c > seal_max_temp:
seal_temp_factor = 0.5
elif operating_temperature_c > seal_max_temp * 0.8:
seal_temp_factor = 0.7
else:
seal_temp_factor = 1.0
effective_seal_fatigue = seal_fatigue_limit * seal_temp_factor
seal_sf = effective_seal_fatigue / seal_total_stress if seal_total_stress > 0 else 10
# Seal damage based on cycles and stress
seal_cycles_to_failure = 1e6 * (seal_sf ** 3) if seal_sf > 0 else 1000
seal_damage = total_cycles / seal_cycles_to_failure if seal_cycles_to_failure > 0 else 1.0
seal_damage = min(1.0, seal_damage)
# === SAFETY FACTORS ===
yield_sf = derated_yield / max_local_stress if max_local_stress > 0 else 10
# Overall fatigue safety factor (conservative - use Goodman)
fatigue_sf = min(goodman_sf, goodman_data["gerber_safety_factor"]) if goodman_data["gerber_safety_factor"] > 0 else goodman_sf
# === STATUS ASSESSMENT ===
overall_sf = min(fatigue_sf, seal_sf, yield_sf)
if overall_sf < 1.0:
urgency = "critical"
recommendation = "Immediate inspection required - operating beyond safe limits"
elif overall_sf < 1.5:
urgency = "warning"
recommendation = "Schedule inspection - marginal safety margin"
elif overall_sf < 2.0:
urgency = "monitor"
recommendation = "Continue monitoring - adequate safety margin"
else:
urgency = "normal"
recommendation = "Component in good condition"
# Determine limiting factor
if fatigue_sf <= seal_sf and fatigue_sf <= yield_sf:
limiting_factor = "cylinder_wall_fatigue"
elif seal_sf <= yield_sf:
limiting_factor = "seal_fatigue"
else:
limiting_factor = "yield_strength"
return {
"success": True,
"simulation_hours": simulation_hours,
"total_cycles": int(total_cycles),
"analysis_method": "advanced",
"stress_analysis": {
"max_hoop_stress_mpa": round(max_hoop_stress, 2),
"min_hoop_stress_mpa": round(min_hoop_stress, 2),
"max_radial_stress_mpa": round(max_radial_stress, 2),
"max_axial_stress_mpa": round(max_axial_stress, 2),
"von_mises_nominal_mpa": round(max_von_mises, 2),
"von_mises_local_mpa": round(max_local_stress, 2),
"stress_amplitude_mpa": round(stress_amplitude, 2),
"stress_mean_mpa": round(stress_mean, 2),
"equivalent_amplitude_goodman": round(equivalent_amplitude, 2),
},
"stress_concentration": scf_data,
"multi_axial_stress": vm_max,
"mean_stress_correction": goodman_data,
"fatigue_analysis": {
"rod_damage_fraction": round(damage_fraction, 6),
"seal_damage_fraction": round(seal_damage, 6),
"cycles_to_failure": int(min(cycles_to_failure, 1e12)),
"remaining_cycles": int(min(max(0, cycles_to_failure - total_cycles), 1e12)),
"remaining_life_hours": round(weibull_data.get("L10_life", l10_life_hours), 0),
"limiting_mode": "fatigue" if fatigue_sf < seal_sf else "seal",
},
"weibull_reliability": weibull_data,
"safety_factors": {
"fatigue_safety_factor": round(fatigue_sf, 2),
"yield_safety_factor": round(yield_sf, 2),
"seal_safety_factor": round(seal_sf, 2),
"overall_safety_factor": round(overall_sf, 2),
"goodman_sf": round(goodman_sf, 2),
"gerber_sf": round(goodman_data["gerber_safety_factor"], 2),
},
"temperature_effects": {
"operating_temperature_c": operating_temperature_c,
"yield_derating_factor": round(temp_derating["yield_derating_factor"], 3),
"fatigue_derating_factor": round(temp_derating["fatigue_derating_factor"], 3),
"seal_temp_factor": round(seal_temp_factor, 2),
},
"material_factors": {
"surface_factor": surface_factor,
"size_factor": size_factor,
"effective_fatigue_limit_mpa": round(effective_fatigue_limit, 1),
},
"seal_analysis": {
"material": seal_material,
"contact_stress_mpa": round(seal_total_stress, 2),
"fatigue_limit_mpa": round(effective_seal_fatigue, 2),
"safety_factor": round(seal_sf, 2),
"max_rated_temp_c": seal_max_temp,
},
"assessment": {
"urgency": urgency,
"recommendation": recommendation,
"overall_limiting_factor": limiting_factor,
},
"components": [
{
"component_name": "cylinder_wall",
"damage_fraction": round(damage_fraction, 6),
"dominant_stress_mpa": round(max_local_stress, 2),
"limiting_mode": "fatigue",
"safety_factor": round(fatigue_sf, 2),
},
{
"component_name": "piston_seal",
"damage_fraction": round(seal_damage, 6),
"dominant_stress_mpa": round(seal_total_stress, 2),
"limiting_mode": "seal",
"safety_factor": round(seal_sf, 2),
}
],
"validation_notes": [
f"SCF (Kf={Kf:.2f}) applied for {scf_data['geometry_type']}",
f"Goodman mean stress correction applied (σm/Su={goodman_data['mean_stress_ratio']:.3f})",
f"Von Mises criterion used for multi-axial stress",
f"Material S-N data from ASM Handbook Vol.19",
f"Weibull analysis at {reliability_percent}% reliability",
]
}