spec / app /services /simulation.py
ronanroam
Deploy OpenModelica simulation platform
6757338
"""
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),
)