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