""" 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", ] }