""" Cylinder simulation service. Calculates hydraulic cylinder performance based on geometry and operating parameters. """ import math from typing import Optional from dataclasses import dataclass @dataclass class SimulationInput: """Input parameters for cylinder simulation.""" # Geometry (required) bore_diameter_mm: float rod_diameter_mm: float stroke_mm: float # Operating conditions supply_pressure_mpa: float = 21.0 # Typical excavator hydraulic pressure return_pressure_mpa: float = 0.5 # Back pressure flow_rate_lpm: float = 100.0 # Liters per minute # Load conditions external_load_kn: float = 0.0 # External load on rod # Efficiency factors mechanical_efficiency: float = 0.95 volumetric_efficiency: float = 0.98 # Pressure drop parameters (valve + line losses) valve_pressure_drop_mpa: float = 0.8 # Directional valve pressure drop line_pressure_drop_mpa: float = 0.4 # Hose/fitting pressure losses # FIX 15: Pump dynamics parameters pump_type: str = "fixed" # "fixed" or "variable" displacement pump_max_pressure_mpa: float = 25.0 # Max pump pressure before relief relief_valve_cracking_mpa: float = 23.0 # Relief valve cracking pressure relief_valve_full_flow_mpa: float = 25.0 # Relief valve full flow pressure pump_speed_rpm: float = 1500.0 # Pump rotational speed (for ripple calc) pump_pistons: int = 9 # Number of pump pistons (for ripple) variable_pump_cutoff_mpa: float = 20.0 # Pressure where variable pump destokes @dataclass class SimulationResult: """Calculated results from cylinder simulation.""" # Areas (mm²) piston_area_mm2: float rod_area_mm2: float annulus_area_mm2: float area_ratio: float # Forces (kN) max_push_force_kn: float max_pull_force_kn: float effective_push_force_kn: float effective_pull_force_kn: float # Speeds (m/s) extend_speed_m_s: float retract_speed_m_s: float # Volumes (liters) extend_volume_liters: float retract_volume_liters: float # Cycle times (seconds) extend_time_s: float retract_time_s: float cycle_time_s: float cycles_per_minute: float # Power (kW) extend_power_kw: float retract_power_kw: float # Additional info pressure_to_overcome_load_mpa: Optional[float] = None speed_under_load_m_s: Optional[float] = None # Pressure drop info (for transparency) total_pressure_drop_mpa: float = 0.0 effective_supply_pressure_mpa: float = 0.0 # FIX 15: Pump dynamics outputs pump_flow_factor: float = 1.0 # Flow reduction due to pump curve relief_valve_flow_lpm: float = 0.0 # Flow through relief valve pump_ripple_amplitude_percent: float = 0.0 # Pressure ripple amplitude pump_ripple_frequency_hz: float = 0.0 # Pressure ripple frequency def run_simulation(params: SimulationInput) -> SimulationResult: """ Run cylinder performance simulation. Calculates forces, speeds, volumes, and cycle times based on cylinder geometry and operating conditions. """ # Calculate areas piston_area_mm2 = math.pi * (params.bore_diameter_mm / 2) ** 2 rod_area_mm2 = math.pi * (params.rod_diameter_mm / 2) ** 2 annulus_area_mm2 = piston_area_mm2 - rod_area_mm2 area_ratio = piston_area_mm2 / annulus_area_mm2 if annulus_area_mm2 > 0 else 0 # Convert areas to m² for force calculations piston_area_m2 = piston_area_mm2 / 1_000_000 annulus_area_m2 = annulus_area_mm2 / 1_000_000 # Total pressure drop (valve + line losses) # Real systems have 0.5-2.0 MPa drop across valves, fittings, and hoses total_pressure_drop_mpa = params.valve_pressure_drop_mpa + params.line_pressure_drop_mpa # Effective supply pressure at cylinder (after losses) effective_supply_pressure_mpa = params.supply_pressure_mpa - total_pressure_drop_mpa # Net pressure (effective supply - back pressure) net_pressure_mpa = effective_supply_pressure_mpa - params.return_pressure_mpa # Force calculations (F = P × A) # Pressure in MPa = N/mm², Area in mm² → Force in N → /1000 = kN # Correct formula accounts for pressure acting on BOTH chambers: # F_extend = P_supply × A_piston - P_return × A_annulus # F_retract = P_supply × A_annulus - P_return × A_piston # Return pressure acts on the OPPOSITE chamber, not the same side max_push_force_kn = (effective_supply_pressure_mpa * piston_area_mm2 - params.return_pressure_mpa * annulus_area_mm2) / 1000 max_pull_force_kn = (effective_supply_pressure_mpa * annulus_area_mm2 - params.return_pressure_mpa * piston_area_mm2) / 1000 # Apply mechanical efficiency effective_push_force_kn = max_push_force_kn * params.mechanical_efficiency effective_pull_force_kn = max_pull_force_kn * params.mechanical_efficiency # Speed calculations (v = Q / A) # Flow in LPM → m³/s: LPM / 60000 # Area in mm² → m²: mm² / 1_000_000 flow_m3_s = (params.flow_rate_lpm / 60000) * params.volumetric_efficiency extend_speed_m_s = flow_m3_s / piston_area_m2 if piston_area_m2 > 0 else 0 retract_speed_m_s = flow_m3_s / annulus_area_m2 if annulus_area_m2 > 0 else 0 # Volume calculations stroke_m = params.stroke_mm / 1000 extend_volume_m3 = piston_area_m2 * stroke_m retract_volume_m3 = annulus_area_m2 * stroke_m extend_volume_liters = extend_volume_m3 * 1000 retract_volume_liters = retract_volume_m3 * 1000 # Cycle time calculations extend_time_s = stroke_m / extend_speed_m_s if extend_speed_m_s > 0 else 0 retract_time_s = stroke_m / retract_speed_m_s if retract_speed_m_s > 0 else 0 cycle_time_s = extend_time_s + retract_time_s cycles_per_minute = 60 / cycle_time_s if cycle_time_s > 0 else 0 # Power calculations (P = F × v) extend_power_kw = effective_push_force_kn * extend_speed_m_s retract_power_kw = effective_pull_force_kn * retract_speed_m_s # Load analysis pressure_to_overcome_load_mpa = None speed_under_load_m_s = None # FIX 15: Initialize pump dynamics variables pump_flow_factor = 1.0 relief_valve_flow_lpm = 0.0 pump_ripple_frequency_hz = params.pump_speed_rpm * params.pump_pistons / 60.0 pump_ripple_amplitude_percent = 2.0 if params.pump_type == "fixed" else 1.5 if params.external_load_kn > 0: # Pressure needed to overcome external load during extension pressure_to_overcome_load_mpa = (params.external_load_kn * 1000) / piston_area_mm2 # Available force after overcoming load available_force_kn = effective_push_force_kn - params.external_load_kn if available_force_kn > 0: # Speed reduction under load due to: # 1. Internal leakage increases with higher working pressure # 2. Pump flow curve - flow drops as pressure rises (typically 10-15% at max pressure) # 3. Valve pressure drop increases with flow squared # Calculate working pressure ratio (higher load = higher pressure) working_pressure_mpa = pressure_to_overcome_load_mpa + 1.0 # Add margin pressure_ratio = working_pressure_mpa / params.supply_pressure_mpa pressure_ratio = min(1.0, pressure_ratio) # Cap at 100% # Internal leakage increases with pressure differential # Leakage flow: Q_leak = k * ΔP (approximately linear with pressure) leakage_factor = 1.0 - (params.leakage_coefficient if hasattr(params, 'leakage_coefficient') else 0.005) * pressure_ratio * 10 # ===== FIX 15: ENHANCED PUMP DYNAMICS ===== # 1. Pump flow curve depends on pump type if params.pump_type == "variable": # Variable displacement pump: destroke (reduce displacement) as pressure rises # Flow drops sharply after cutoff pressure (pressure compensated) if working_pressure_mpa < params.variable_pump_cutoff_mpa: # Below cutoff: full flow with minimal losses pump_flow_factor = 1.0 - 0.03 * pressure_ratio # 3% loss at max else: # Above cutoff: rapid destroke - flow proportional to (1 - (P-Pcutoff)/(Pmax-Pcutoff)) destroke_ratio = (working_pressure_mpa - params.variable_pump_cutoff_mpa) / \ max(0.1, params.pump_max_pressure_mpa - params.variable_pump_cutoff_mpa) destroke_ratio = min(1.0, destroke_ratio) pump_flow_factor = max(0.05, 1.0 - 0.95 * destroke_ratio) # Min 5% flow else: # Fixed displacement pump: flow drops due to internal leakage # Typical curve: Q = Q_nominal × (1 - k × (P/P_max)²) where k ≈ 0.10-0.15 pump_flow_factor = 1.0 - 0.12 * pressure_ratio ** 2 # 2. Relief valve dynamics # Relief valve opens progressively between cracking and full-flow pressure # Q_relief = Cv × sqrt(P - P_crack) when P > P_crack relief_valve_flow_lpm = 0.0 if working_pressure_mpa > params.relief_valve_cracking_mpa: # Progressive opening: flow increases with sqrt of pressure above cracking pressure_above_crack = working_pressure_mpa - params.relief_valve_cracking_mpa full_flow_pressure_diff = params.relief_valve_full_flow_mpa - params.relief_valve_cracking_mpa if full_flow_pressure_diff > 0: opening_ratio = min(1.0, math.sqrt(pressure_above_crack / full_flow_pressure_diff)) # At full opening, relief can pass full pump flow relief_valve_flow_lpm = params.flow_rate_lpm * opening_ratio # Reduce effective pump flow by relief amount pump_flow_factor *= max(0.1, 1.0 - opening_ratio * 0.9) # 3. Pump ripple (pressure pulsation) # Frequency = pump_speed × number_of_pistons / 60 # Amplitude depends on pump type and pressure (typically 2-5% of working pressure) pump_ripple_frequency_hz = params.pump_speed_rpm * params.pump_pistons / 60.0 # Ripple amplitude as percentage of working pressure if params.pump_type == "variable": # Variable pumps have lower ripple due to swashplate damping pump_ripple_amplitude_percent = 1.5 + 1.0 * pressure_ratio # 1.5-2.5% else: # Fixed displacement pumps have higher ripple pump_ripple_amplitude_percent = 2.5 + 2.0 * pressure_ratio # 2.5-4.5% # Combined speed reduction speed_reduction_factor = leakage_factor * pump_flow_factor * params.volumetric_efficiency speed_reduction_factor = max(0.1, min(1.0, speed_reduction_factor)) # Limit reduction speed_under_load_m_s = extend_speed_m_s * speed_reduction_factor else: speed_under_load_m_s = 0.0 return SimulationResult( piston_area_mm2=round(piston_area_mm2, 2), rod_area_mm2=round(rod_area_mm2, 2), annulus_area_mm2=round(annulus_area_mm2, 2), area_ratio=round(area_ratio, 3), max_push_force_kn=round(max_push_force_kn, 2), max_pull_force_kn=round(max_pull_force_kn, 2), effective_push_force_kn=round(effective_push_force_kn, 2), effective_pull_force_kn=round(effective_pull_force_kn, 2), extend_speed_m_s=round(extend_speed_m_s, 4), retract_speed_m_s=round(retract_speed_m_s, 4), extend_volume_liters=round(extend_volume_liters, 3), retract_volume_liters=round(retract_volume_liters, 3), extend_time_s=round(extend_time_s, 2), retract_time_s=round(retract_time_s, 2), cycle_time_s=round(cycle_time_s, 2), cycles_per_minute=round(cycles_per_minute, 2), extend_power_kw=round(extend_power_kw, 2), retract_power_kw=round(retract_power_kw, 2), pressure_to_overcome_load_mpa=round(pressure_to_overcome_load_mpa, 2) if pressure_to_overcome_load_mpa else None, speed_under_load_m_s=round(speed_under_load_m_s, 4) if speed_under_load_m_s is not None else None, total_pressure_drop_mpa=round(total_pressure_drop_mpa, 2), effective_supply_pressure_mpa=round(effective_supply_pressure_mpa, 2), # FIX 15: Pump dynamics outputs pump_flow_factor=round(pump_flow_factor, 3), relief_valve_flow_lpm=round(relief_valve_flow_lpm, 2), pump_ripple_amplitude_percent=round(pump_ripple_amplitude_percent, 2), pump_ripple_frequency_hz=round(pump_ripple_frequency_hz, 1), )