Spaces:
Running
Running
| """ | |
| CFD Simulation Service | |
| Provides fluid flow analysis using the fluids library for validated calculations. | |
| OpenFOAM is too large (~2GB) for Hugging Face Spaces, so we use analytical methods. | |
| External dependencies: | |
| - fluids: Industry-standard fluid dynamics library | |
| - numpy: Numerical operations | |
| """ | |
| import math | |
| import uuid | |
| from typing import Dict, Any, Optional, List | |
| from dataclasses import dataclass | |
| import numpy as np | |
| import fluids | |
| from fluids import Reynolds, friction_factor, K_from_f, dP_from_K | |
| class CFDInput: | |
| """Input parameters for CFD simulation.""" | |
| pipe_diameter_mm: float | |
| pipe_length_mm: float | |
| flow_rate_lpm: float | |
| oil_viscosity_pa_s: float = 0.046 # ISO VG46 at 40°C | |
| oil_density_kg_m3: float = 870 | |
| inlet_temperature_c: float = 40.0 | |
| mesh_resolution: str = "medium" # Kept for API compatibility | |
| class CFDResult: | |
| """Results from CFD simulation.""" | |
| success: bool | |
| message: str | |
| job_id: str | |
| # Flow results | |
| flow_regime: str | |
| reynolds_number: float | |
| average_velocity_ms: float | |
| max_velocity_ms: float | |
| pressure_drop_mpa: float | |
| # Comparison with analytical | |
| analytical_pressure_drop_mpa: float | |
| error_percent: float | |
| # Mesh info (zero for analytical) | |
| num_cells: int | |
| # Visualization data | |
| velocity_field: Optional[Dict] = None | |
| pressure_field: Optional[Dict] = None | |
| def calculate_hagen_poiseuille( | |
| pipe_diameter_mm: float, | |
| pipe_length_mm: float, | |
| flow_rate_lpm: float, | |
| oil_viscosity_pa_s: float = 0.046, | |
| oil_density_kg_m3: float = 870 | |
| ) -> Dict[str, float]: | |
| """ | |
| Pipe flow analysis using the fluids library. | |
| Uses fluids.Reynolds and fluids.friction_factor for validated calculations. | |
| Handles laminar, transition, and turbulent flow regimes. | |
| Args: | |
| pipe_diameter_mm: Pipe inner diameter | |
| pipe_length_mm: Pipe length | |
| flow_rate_lpm: Flow rate in L/min | |
| oil_viscosity_pa_s: Dynamic viscosity | |
| oil_density_kg_m3: Oil density | |
| Returns: | |
| Dictionary with flow solution | |
| """ | |
| # Convert units | |
| d = pipe_diameter_mm / 1000 # m | |
| L = pipe_length_mm / 1000 # m | |
| Q = flow_rate_lpm / 60000 # m³/s | |
| # Cross-sectional area | |
| A = math.pi * (d/2)**2 | |
| # Average velocity | |
| v_avg = Q / A if A > 0 else 0 | |
| # Use fluids library for Reynolds number calculation | |
| # fluids.Reynolds(V, D, rho, mu) - returns Reynolds number | |
| if d > 0 and oil_viscosity_pa_s > 0: | |
| Re = Reynolds(V=v_avg, D=d, rho=oil_density_kg_m3, mu=oil_viscosity_pa_s) | |
| else: | |
| Re = 0 | |
| # Determine flow regime | |
| if Re < 2300: | |
| flow_regime = "laminar" | |
| v_max = 2 * v_avg # Parabolic profile for laminar | |
| elif Re < 4000: | |
| flow_regime = "transition" | |
| v_max = 1.5 * v_avg # Approximate | |
| else: | |
| flow_regime = "turbulent" | |
| v_max = 1.22 * v_avg # Power-law profile | |
| # Use fluids library for friction factor | |
| # friction_factor(Re, eD) - eD is relative roughness (0 for smooth pipe) | |
| if Re > 0: | |
| f = friction_factor(Re=Re, eD=0) # Smooth pipe | |
| else: | |
| f = 0 | |
| # Pressure drop using Darcy-Weisbach equation | |
| # ΔP = f × (L/D) × (ρV²/2) | |
| if d > 0 and L > 0: | |
| delta_P = f * (L / d) * (0.5 * oil_density_kg_m3 * v_avg**2) | |
| else: | |
| delta_P = 0 | |
| # Wall shear stress | |
| tau_wall = delta_P * d / (4 * L) if L > 0 else 0 | |
| # Entry length (distance for flow to become fully developed) | |
| if flow_regime == "laminar": | |
| entry_length = 0.06 * Re * d | |
| else: | |
| entry_length = 4.4 * d * Re**(1/6) if Re > 0 else 0 | |
| return { | |
| "flow_regime": flow_regime, | |
| "reynolds_number": round(Re, 0), | |
| "average_velocity_ms": round(v_avg, 4), | |
| "max_velocity_ms": round(v_max, 4), | |
| "pressure_drop_pa": round(delta_P, 2), | |
| "pressure_drop_mpa": round(delta_P / 1e6, 6), | |
| "friction_factor": round(f, 6), | |
| "wall_shear_stress_pa": round(tau_wall, 2), | |
| "entry_length_mm": round(entry_length * 1000, 1), | |
| } | |
| def _generate_velocity_profile( | |
| pipe_diameter_mm: float, | |
| pipe_length_mm: float, | |
| flow_rate_lpm: float, | |
| flow_regime: str | |
| ) -> Dict[str, Any]: | |
| """ | |
| Generate velocity profile data for visualization. | |
| """ | |
| import numpy as np | |
| d = pipe_diameter_mm / 1000 # m | |
| R = d / 2 | |
| Q = flow_rate_lpm / 60000 # m³/s | |
| A = math.pi * R**2 | |
| v_avg = Q / A if A > 0 else 0 | |
| # Generate radial positions and velocities | |
| n_radial = 20 | |
| n_axial = 10 | |
| r_vals = np.linspace(0, R, n_radial) | |
| z_vals = np.linspace(0, pipe_length_mm, n_axial) | |
| positions = [] | |
| velocities = [] | |
| magnitudes = [] | |
| for z in z_vals: | |
| for r in r_vals: | |
| for theta in [0, math.pi/2, math.pi, 3*math.pi/2]: | |
| x = r * math.cos(theta) * 1000 # Convert back to mm | |
| y = r * math.sin(theta) * 1000 | |
| positions.extend([x, y, z]) | |
| # Velocity profile (parabolic for laminar) | |
| if flow_regime == "laminar": | |
| v = 2 * v_avg * (1 - (r/R)**2) if R > 0 else 0 | |
| else: | |
| # Power-law profile for turbulent (1/7 power law) | |
| v = v_avg * 1.22 * (1 - r/R)**(1/7) if R > 0 and r < R else 0 | |
| # Velocity is axial (z-direction) | |
| velocities.extend([0, 0, v]) | |
| magnitudes.append(v) | |
| return { | |
| "positions": positions, | |
| "vectors": velocities, | |
| "magnitudes": magnitudes, | |
| } | |
| def _generate_pressure_field( | |
| pipe_diameter_mm: float, | |
| pipe_length_mm: float, | |
| pressure_drop_pa: float | |
| ) -> Dict[str, Any]: | |
| """ | |
| Generate pressure field data for visualization. | |
| Pressure varies linearly along the pipe. | |
| """ | |
| import numpy as np | |
| n_axial = 20 | |
| z_vals = np.linspace(0, pipe_length_mm, n_axial) | |
| positions = [] | |
| values = [] | |
| inlet_pressure = pressure_drop_pa # Gauge pressure | |
| for z in z_vals: | |
| # Linear pressure drop | |
| p = inlet_pressure * (1 - z / pipe_length_mm) | |
| # Add points at center | |
| positions.extend([0, 0, z]) | |
| values.append(p) | |
| return { | |
| "positions": positions, | |
| "values": values, | |
| "range": { | |
| "min": 0, | |
| "max": inlet_pressure, | |
| } | |
| } | |
| def run_cfd_simulation(params: CFDInput) -> CFDResult: | |
| """ | |
| Run CFD analysis using analytical Hagen-Poiseuille solution. | |
| Args: | |
| params: CFD input parameters | |
| Returns: | |
| CFDResult with flow analysis results | |
| """ | |
| job_id = str(uuid.uuid4())[:8] | |
| # Calculate analytical solution | |
| analytical = calculate_hagen_poiseuille( | |
| params.pipe_diameter_mm, | |
| params.pipe_length_mm, | |
| params.flow_rate_lpm, | |
| params.oil_viscosity_pa_s, | |
| params.oil_density_kg_m3 | |
| ) | |
| # Generate visualization data | |
| velocity_field = _generate_velocity_profile( | |
| params.pipe_diameter_mm, | |
| params.pipe_length_mm, | |
| params.flow_rate_lpm, | |
| analytical["flow_regime"] | |
| ) | |
| pressure_field = _generate_pressure_field( | |
| params.pipe_diameter_mm, | |
| params.pipe_length_mm, | |
| analytical["pressure_drop_pa"] | |
| ) | |
| return CFDResult( | |
| success=True, | |
| message=f"Flow analysis completed using analytical {analytical['flow_regime']} solution", | |
| job_id=job_id, | |
| flow_regime=analytical["flow_regime"], | |
| reynolds_number=analytical["reynolds_number"], | |
| average_velocity_ms=analytical["average_velocity_ms"], | |
| max_velocity_ms=analytical["max_velocity_ms"], | |
| pressure_drop_mpa=analytical["pressure_drop_mpa"], | |
| analytical_pressure_drop_mpa=analytical["pressure_drop_mpa"], | |
| error_percent=0, # No error since using analytical directly | |
| num_cells=0, # No mesh for analytical | |
| velocity_field=velocity_field, | |
| pressure_field=pressure_field, | |
| ) | |
| def cfd_result_to_dict(result: CFDResult) -> Dict[str, Any]: | |
| """Convert CFDResult to dictionary for JSON response.""" | |
| return { | |
| "success": result.success, | |
| "message": result.message, | |
| "job_id": result.job_id, | |
| "flow_analysis": { | |
| "flow_regime": result.flow_regime, | |
| "reynolds_number": int(result.reynolds_number), | |
| "average_velocity_ms": round(result.average_velocity_ms, 4), | |
| "max_velocity_ms": round(result.max_velocity_ms, 4), | |
| "pressure_drop_mpa": round(result.pressure_drop_mpa, 6), | |
| }, | |
| "analytical_comparison": { | |
| "analytical_pressure_drop_mpa": round(result.analytical_pressure_drop_mpa, 6), | |
| "cfd_vs_analytical_error_percent": round(result.error_percent, 2), | |
| }, | |
| "mesh_info": { | |
| "num_cells": result.num_cells, | |
| }, | |
| "visualization": { | |
| "velocity_field": result.velocity_field, | |
| "pressure_field": result.pressure_field, | |
| } if result.velocity_field else None, | |
| } | |
| def calculate_hydraulic_loss_coefficient( | |
| fitting_type: str, | |
| pipe_diameter_mm: float, | |
| flow_rate_lpm: float, | |
| oil_density_kg_m3: float = 870 | |
| ) -> Dict[str, float]: | |
| """ | |
| Calculate pressure loss for common hydraulic fittings. | |
| Uses fluids.fittings library where possible for validated K values, | |
| with fallback to standard handbook values. | |
| Args: | |
| fitting_type: Type of fitting ("elbow_90", "tee", "valve", etc.) | |
| pipe_diameter_mm: Pipe diameter | |
| flow_rate_lpm: Flow rate | |
| oil_density_kg_m3: Oil density | |
| Returns: | |
| Dictionary with loss coefficient and pressure drop | |
| """ | |
| d = pipe_diameter_mm / 1000 # m | |
| Q = flow_rate_lpm / 60000 # m³/s | |
| A = math.pi * (d/2)**2 | |
| v = Q / A if A > 0 else 0 | |
| # Try fluids library fittings first | |
| K = None | |
| source = "fluids" | |
| try: | |
| if fitting_type == "elbow_90": | |
| K = fluids.fittings.bend_rounded(Di=d, angle=90, fd=0.02) | |
| elif fitting_type == "elbow_90_long": | |
| K = fluids.fittings.bend_rounded(Di=d, angle=90, fd=0.02, bend_diameters=1.5) | |
| elif fitting_type == "elbow_45": | |
| K = fluids.fittings.bend_rounded(Di=d, angle=45, fd=0.02) | |
| elif fitting_type == "elbow_180": | |
| K = fluids.fittings.bend_rounded(Di=d, angle=180, fd=0.02) | |
| elif fitting_type == "sudden_expansion": | |
| K = fluids.fittings.contraction_sharp(Di1=d*0.8, Di2=d, fd=0.02) | |
| elif fitting_type == "sudden_contraction": | |
| K = fluids.fittings.contraction_sharp(Di1=d, Di2=d*0.8, fd=0.02) | |
| elif fitting_type == "pipe_entrance_sharp": | |
| K = fluids.fittings.entrance_sharp() | |
| elif fitting_type == "pipe_exit": | |
| K = fluids.fittings.exit_normal() | |
| except Exception: | |
| K = None | |
| # Fallback to handbook values if fluids library doesn't have the fitting | |
| if K is None: | |
| source = "handbook" | |
| K_VALUES = { | |
| "elbow_90": 0.9, | |
| "elbow_90_long": 0.6, | |
| "elbow_45": 0.4, | |
| "elbow_180": 1.5, | |
| "tee_through": 0.3, | |
| "tee_branch": 1.0, | |
| "gate_valve_open": 0.2, | |
| "gate_valve_half": 5.6, | |
| "ball_valve_open": 0.1, | |
| "ball_valve_half": 5.5, | |
| "check_valve": 2.5, | |
| "filter": 3.0, | |
| "strainer": 2.0, | |
| "sudden_expansion": 1.0, | |
| "sudden_contraction": 0.5, | |
| "gradual_expansion": 0.3, | |
| "gradual_contraction": 0.2, | |
| "pipe_entrance_sharp": 0.5, | |
| "pipe_entrance_rounded": 0.04, | |
| "pipe_exit": 1.0, | |
| } | |
| K = K_VALUES.get(fitting_type, 1.0) | |
| # Use fluids library for pressure drop calculation | |
| # dP_from_K(K, rho, V) - returns pressure drop in Pa | |
| if v > 0: | |
| delta_P = dP_from_K(K=K, rho=oil_density_kg_m3, V=v) | |
| else: | |
| delta_P = 0 | |
| return { | |
| "fitting_type": fitting_type, | |
| "loss_coefficient_K": round(K, 4), | |
| "velocity_ms": round(v, 4), | |
| "pressure_loss_pa": round(delta_P, 2), | |
| "pressure_loss_mpa": round(delta_P / 1e6, 6), | |
| "source": source, | |
| } | |
| def calculate_system_pressure_drop( | |
| pipes: List[Dict], | |
| fittings: List[Dict], | |
| flow_rate_lpm: float, | |
| oil_viscosity_pa_s: float = 0.046, | |
| oil_density_kg_m3: float = 870 | |
| ) -> Dict[str, Any]: | |
| """ | |
| Calculate total pressure drop for a hydraulic system. | |
| Args: | |
| pipes: List of pipe segments [{diameter_mm, length_mm}, ...] | |
| fittings: List of fittings [{type, diameter_mm}, ...] | |
| flow_rate_lpm: System flow rate | |
| oil_viscosity_pa_s: Oil viscosity | |
| oil_density_kg_m3: Oil density | |
| Returns: | |
| Dictionary with total pressure drop and breakdown | |
| """ | |
| total_pipe_loss = 0 | |
| total_fitting_loss = 0 | |
| pipe_details = [] | |
| fitting_details = [] | |
| # Calculate pipe losses | |
| for pipe in pipes: | |
| result = calculate_hagen_poiseuille( | |
| pipe["diameter_mm"], | |
| pipe["length_mm"], | |
| flow_rate_lpm, | |
| oil_viscosity_pa_s, | |
| oil_density_kg_m3 | |
| ) | |
| total_pipe_loss += result["pressure_drop_pa"] | |
| pipe_details.append({ | |
| "diameter_mm": pipe["diameter_mm"], | |
| "length_mm": pipe["length_mm"], | |
| "pressure_drop_mpa": result["pressure_drop_mpa"], | |
| "flow_regime": result["flow_regime"], | |
| }) | |
| # Calculate fitting losses | |
| for fitting in fittings: | |
| result = calculate_hydraulic_loss_coefficient( | |
| fitting["type"], | |
| fitting["diameter_mm"], | |
| flow_rate_lpm, | |
| oil_density_kg_m3 | |
| ) | |
| total_fitting_loss += result["pressure_loss_pa"] | |
| fitting_details.append({ | |
| "type": fitting["type"], | |
| "diameter_mm": fitting["diameter_mm"], | |
| "pressure_drop_mpa": result["pressure_loss_mpa"], | |
| "K_factor": result["loss_coefficient_K"], | |
| }) | |
| total_loss = total_pipe_loss + total_fitting_loss | |
| return { | |
| "total_pressure_drop_mpa": round(total_loss / 1e6, 4), | |
| "pipe_losses_mpa": round(total_pipe_loss / 1e6, 4), | |
| "fitting_losses_mpa": round(total_fitting_loss / 1e6, 4), | |
| "pipe_loss_fraction": round(total_pipe_loss / total_loss, 2) if total_loss > 0 else 0, | |
| "fitting_loss_fraction": round(total_fitting_loss / total_loss, 2) if total_loss > 0 else 0, | |
| "pipes": pipe_details, | |
| "fittings": fitting_details, | |
| } | |