using System; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Windows.Media; using CtrEditor.HydraulicSimulator; using CtrEditor.ObjetosSim; using CtrEditor.FuncionesBase; using HydraulicSimulator.Models; using Newtonsoft.Json; using Xceed.Wpf.Toolkit.PropertyGrid.Attributes; using CommunityToolkit.Mvvm.ComponentModel; namespace CtrEditor.ObjetosSim { /// /// Bomba hidráulica que implementa las interfaces del simulador hidráulico /// public partial class osHydPump : osBase, IHydraulicComponent, IHydraulicFlowReceiver, IHydraulicPressureReceiver, IosBase { // Referencia al objeto de simulación hidráulica específico private simHydraulicPump SimHydraulicPump; public static string NombreCategoria() => "Componentes Hidráulicos"; public static string NombreClase() { return "Bomba Hidráulica"; } // Properties [ObservableProperty] [property: JsonIgnore] [property: Category("Apariencia")] [property: Description("Imagen visual")] [property: Name("Imagen")] public ImageSource imageSource_oculta; [ObservableProperty] [property: Category("🎨 Apariencia")] [property: Description("Tamaño visual de la bomba")] [property: Name("Tamaño")] public float tamano; public override void OnResize(float Delta_Width, float Delta_Height) { Tamano += Delta_Width + Delta_Height; } private double _pumpHead = 50.0; // metros private double _maxFlow = 0.01; // m³/s (36 m³/h) private double _speedRatio = 1.0; private bool _isRunning = true; private int _pumpDirection = 1; private double _currentFlow = 0.0; private double _currentPressure = 0.0; // Propiedades visuales específicas de la bomba [ObservableProperty] [property: Category("🎨 Apariencia")] [property: Description("Color visual de la bomba")] [property: Name("Color")] private Brush colorButton_oculto = Brushes.Blue; [Category("🔧 Bomba Hidráulica")] [DisplayName("Cabeza de bomba")] [Description("Cabeza de la bomba a caudal cero (m)")] public double PumpHead { get => _pumpHead; set { if (SetProperty(ref _pumpHead, Math.Max(0, value))) { InvalidateHydraulicNetwork(); } } } [Category("🔧 Bomba Hidráulica")] [DisplayName("Caudal máximo")] [Description("Caudal máximo de la bomba (m³/s)")] public double MaxFlow { get => _maxFlow; set { // Si el valor es mayor a 1, asumir que está en m³/h y convertir a m³/s double valueInM3s = value > 1.0 ? value / 3600.0 : value; if (SetProperty(ref _maxFlow, Math.Max(0.0001, valueInM3s))) { InvalidateHydraulicNetwork(); Debug.WriteLine($"Bomba {Nombre}: MaxFlow establecido a {_maxFlow:F6} m³/s ({_maxFlow * 3600:F2} m³/h)"); } } } [Category("🔧 Bomba Hidráulica")] [DisplayName("Velocidad relativa")] [Description("Velocidad relativa de la bomba (0.0 a 1.0)")] public double SpeedRatio { get => _speedRatio; set { SetProperty(ref _speedRatio, Math.Max(0.0, Math.Min(1.0, value))); } } [Category("🔧 Bomba Hidráulica")] [DisplayName("Funcionando")] [Description("Indica si la bomba está encendida")] public bool IsRunning { get => _isRunning; set { if (SetProperty(ref _isRunning, value)) { UpdatePumpImage(); } } } private void UpdatePumpImage() { if (IsRunning) ImageSource_oculta = ImageFromPath("/imagenes/pump_run.png"); else ImageSource_oculta = ImageFromPath("/imagenes/pump_stop.png"); } [Category("🔧 Bomba Hidráulica")] [DisplayName("Dirección")] [Description("Dirección de la bomba (+1 o -1)")] [ItemsSource(typeof(PumpDirectionItemsSource))] public int PumpDirection { get => _pumpDirection; set { if (SetProperty(ref _pumpDirection, value == -1 ? -1 : 1)) { InvalidateHydraulicNetwork(); } } } [Category("📊 Estado Actual")] [DisplayName("Caudal actual")] [Description("Caudal actual de la bomba (m³/s)")] [JsonIgnore] public double CurrentFlow { get => _currentFlow; private set => SetProperty(ref _currentFlow, value); } [Category("📊 Estado Actual")] [DisplayName("Presión actual")] [Description("Presión actual en la bomba (Pa)")] [JsonIgnore] public double CurrentPressure { get => _currentPressure; private set => SetProperty(ref _currentPressure, value); } [Category("📊 Estado Actual")] [DisplayName("Presión (bar)")] [Description("Presión actual en la bomba (bar)")] [JsonIgnore] public double CurrentPressureBar => CurrentPressure / 100000.0; [Category("📊 Estado Actual")] [DisplayName("Caudal (L/min)")] [Description("Caudal actual en L/min")] [JsonIgnore] public double CurrentFlowLMin => CurrentFlow * 60000.0; // m³/s a L/min [Category("📊 Estado Actual")] [DisplayName("NPSH Disponible")] [Description("NPSH disponible calculado (m)")] [JsonIgnore] public double NPSHAvailable { get { var (inletNodeName, _) = GetConnectedNodeNames(); if (!string.IsNullOrEmpty(inletNodeName) && hydraulicSimulationManager?.LastSolutionResult?.Pressures?.ContainsKey(inletNodeName) == true) { var suctionPressure = hydraulicSimulationManager.LastSolutionResult.Pressures[inletNodeName]; var pump = new PumpHQ(0, 0); // Solo para usar el método de cálculo return pump.CalculateNPSHAvailable(suctionPressure, hydraulicSimulationManager.SimulationFluid); } return 0.0; } } [Category("📊 Estado Actual")] [DisplayName("Factor Cavitación")] [Description("Factor de cavitación (1=sin cavitación, 0=cavitación total)")] [JsonIgnore] public double CavitationFactor { get { var (inletNodeName, _) = GetConnectedNodeNames(); if (!string.IsNullOrEmpty(inletNodeName) && hydraulicSimulationManager?.LastSolutionResult?.Pressures?.ContainsKey(inletNodeName) == true) { var suctionPressure = hydraulicSimulationManager.LastSolutionResult.Pressures[inletNodeName]; var pump = new PumpHQ(0, 0); // Solo para usar el método de cálculo return pump.GetCavitationFactor(suctionPressure, hydraulicSimulationManager.SimulationFluid); } return 1.0; } } [Category("📊 Estado Actual")] [DisplayName("Puede Operar")] [Description("Indica si la bomba puede operar sin cavitación")] [JsonIgnore] public bool CanOperateWithoutCavitation { get { var (inletNodeName, _) = GetConnectedNodeNames(); if (!string.IsNullOrEmpty(inletNodeName) && hydraulicSimulationManager?.LastSolutionResult?.Pressures?.ContainsKey(inletNodeName) == true) { var suctionPressure = hydraulicSimulationManager.LastSolutionResult.Pressures[inletNodeName]; var pump = new PumpHQ(0, 0); // Solo para usar el método de cálculo return pump.CanOperateWithoutCavitation(suctionPressure, hydraulicSimulationManager.SimulationFluid); } return false; } } [Category("📊 Estado Actual")] [DisplayName("Estado Cavitación")] [Description("Estado actual de cavitación de la bomba")] [JsonIgnore] public string CavitationStatus { get { if (!IsRunning) return "Bomba detenida"; var factor = CavitationFactor; var canOperate = CanOperateWithoutCavitation; var npshAvailable = NPSHAvailable; if (canOperate && factor >= 0.9) return "✅ Operación normal"; else if (factor >= 0.5) return $"⚠️ Riesgo cavitación (Factor: {factor:F2})"; else return $"❌ NPSH insuficiente ({npshAvailable:F2}m)"; } } [Category("📊 Estado Actual")] [DisplayName("Estado Detallado")] [Description("Información detallada del estado operacional")] [JsonIgnore] public string DetailedStatus { get { if (!IsRunning) return "Bomba detenida"; var factor = CavitationFactor; var npshAvailable = NPSHAvailable; var flow = CurrentFlowLMin; return $"NPSH: {npshAvailable:F2}m | Factor: {factor:F2} | Flujo: {flow:F1} L/min"; } } // Propiedades de fluido actual private FluidProperties _currentFluid = new FluidProperties(FluidType.Air); [Category("🧪 Fluido Actual")] [DisplayName("Tipo de fluido")] [Description("Tipo de fluido que está bombeando")] [ReadOnly(true)] public FluidType CurrentFluidType => _currentFluid.Type; [Category("🧪 Fluido Actual")] [DisplayName("Descripción")] [Description("Descripción del fluido que se está bombeando")] [ReadOnly(true)] public string CurrentFluidDescription => _currentFluid.Description; [Category("📊 Estado Actual")] [DisplayName("Factor Viscosidad")] [Description("Factor de eficiencia debido a la viscosidad del fluido")] [ReadOnly(true)] public double ViscosityEffect { get { // Factor basado en viscosidad del fluido vs agua var waterViscosity = 0.001; // Pa·s var currentViscosity = _currentFluid.Viscosity; return Math.Max(0.1, Math.Min(1.0, waterViscosity / currentViscosity)); } } [Category("📊 Estado Actual")] [DisplayName("Flujo Efectivo (L/min)")] [Description("Flujo real ajustado por viscosidad")] [ReadOnly(true)] public double EffectiveFlowLMin => CurrentFlowLMin * ViscosityEffect; [Category("📊 Estado Actual")] [DisplayName("Temperatura (°C)")] [Description("Temperatura del fluido bombeado")] [ReadOnly(true)] public double FluidTemperature => _currentFluid.Temperature; [Category("📊 Estado Actual")] [DisplayName("Densidad (kg/L)")] [Description("Densidad del fluido bombeado")] [ReadOnly(true)] public double FluidDensity => _currentFluid.Density; [Category("📊 Estado Actual")] [DisplayName("Brix (%)")] [Description("Concentración en grados Brix (para jarabes)")] [ReadOnly(true)] public double FluidBrix => _currentFluid.Brix; [Category("📊 Estado Actual")] [DisplayName("Concentración (%)")] [Description("Concentración en porcentaje (para químicos)")] [ReadOnly(true)] public double FluidConcentration => _currentFluid.Concentration; [Category("📊 Estado Actual")] [DisplayName("Viscosidad (cP)")] [Description("Viscosidad dinámica en centipoise")] [ReadOnly(true)] public double FluidViscosity => _currentFluid.Viscosity; [Category("📊 Estado Actual")] [DisplayName("Descripción del Fluido")] [Description("Descripción completa del fluido")] [ReadOnly(true)] public string FluidDescription => _currentFluid.Description; // Constructor y Métodos Base private string nombre = NombreClase(); [Category("🏷️ Identificación")] [Description("Nombre identificativo del objeto")] [Name("Nombre")] public override string Nombre { get => nombre; set => SetProperty(ref nombre, value); } public osHydPump() { Nombre = "Bomba Hidráulica"; Tamano = 0.30f; // Inicializar dimensiones usando las propiedades de osBase Ancho = 0.15f; Alto = 0.10f; Angulo = 0f; // Asegurar que el movimiento esté habilitado Lock_movement = false; ImageSource_oculta = ImageFromPath("/imagenes/pump_stop.png"); } public override void UpdateGeometryStart() { // Se llama cuando inicia la simulación - crear objeto hidráulico si no existe if (SimHydraulicPump == null) { SimHydraulicPump = hydraulicSimulationManager.AddPump(PumpHead, MaxFlow, SpeedRatio, IsRunning, PumpDirection); SimHydraulicPump.SimObjectType = "HydraulicPump"; SimHydraulicPump.WpfObject = this; SimHydraulicPump.Nombre = Nombre; } else { // Actualizar propiedades si el objeto ya existe SimHydraulicPump.PumpHead = PumpHead; SimHydraulicPump.MaxFlow = MaxFlow; SimHydraulicPump.SpeedRatio = SpeedRatio; SimHydraulicPump.IsRunning = IsRunning; SimHydraulicPump.PumpDirection = PumpDirection; } } public override void UpdateGeometryStep() { // Los objetos hidráulicos actualizan sus resultados // a través de ApplySimulationResults() desde HydraulicSimulationManager } // Método para actualizar fluido desde tanque de succión public void UpdateFluidFromSuction() { if (_mainViewModel?.ObjetosSimulables == null) return; try { // Buscar el componente conectado en la succión (entrada de la bomba) var suctionComponent = FindSuctionComponent(); if (suctionComponent is osHydTank tank) { var sourceFluid = tank.CurrentOutputFluid; if (sourceFluid != null) { _currentFluid = sourceFluid.Clone(); OnPropertyChanged(nameof(CurrentFluidType)); OnPropertyChanged(nameof(CurrentFluidDescription)); OnPropertyChanged(nameof(ViscosityEffect)); OnPropertyChanged(nameof(EffectiveFlowLMin)); OnPropertyChanged(nameof(FluidTemperature)); OnPropertyChanged(nameof(FluidDensity)); // Actualizar color de la bomba según el fluido UpdatePumpColorFromFluid(); } } } catch (Exception ex) { // Mantener fluido por defecto en caso de error System.Diagnostics.Debug.WriteLine($"Error updating pump fluid: {ex.Message}"); } } private void UpdatePumpColorFromFluid() { if (!IsRunning) { ColorButton_oculto = Brushes.Gray; return; } // Si no hay flujo, verificar cavitación primero if (Math.Abs(CurrentFlow) < 1e-6) { if (hydraulicSimulationManager?.EnableNPSHVerification == true) { var cavitationFactor = CavitationFactor; if (cavitationFactor < 0.1) ColorButton_oculto = Brushes.Red; // Cavitación severa else if (cavitationFactor < 0.5) ColorButton_oculto = Brushes.Orange; // Riesgo de cavitación else ColorButton_oculto = Brushes.Yellow; // Standby } else { ColorButton_oculto = Brushes.Yellow; // Standby } return; } // Colorear según tipo de fluido bombeado try { var fluidColorHex = _currentFluid.Color; var fluidColor = (Color)System.Windows.Media.ColorConverter.ConvertFromString(fluidColorHex); ColorButton_oculto = new SolidColorBrush(fluidColor); } catch { ColorButton_oculto = Brushes.Green; // Color por defecto si hay flujo } } private IHydraulicComponent FindSuctionComponent() { // Buscar tuberías conectadas a esta bomba var connectedPipes = _mainViewModel.ObjetosSimulables .OfType() .Where(pipe => pipe.Id_ComponenteA == Nombre || pipe.Id_ComponenteB == Nombre) .ToList(); foreach (var pipe in connectedPipes) { // La succión es donde el flujo viene HACIA la bomba string suctionComponentName = null; if (pipe.Id_ComponenteB == Nombre && pipe.CurrentFlow >= 0) { // Flujo de A hacia B (hacia esta bomba) suctionComponentName = pipe.Id_ComponenteA; } else if (pipe.Id_ComponenteA == Nombre && pipe.CurrentFlow < 0) { // Flujo de B hacia A (hacia esta bomba) suctionComponentName = pipe.Id_ComponenteB; } if (!string.IsNullOrEmpty(suctionComponentName)) { return _mainViewModel.ObjetosSimulables .OfType() .FirstOrDefault(c => c is osBase osb && osb.Nombre == suctionComponentName); } } return null; } public override void UpdateControl(int elapsedMilliseconds) { // Actualizar propiedades desde la simulación hidráulica if (SimHydraulicPump != null) { CurrentFlow = SimHydraulicPump.CurrentFlow; CurrentPressure = SimHydraulicPump.CurrentPressure; } // Actualizar propiedades del fluido cada ciclo UpdateFluidFromSuction(); // Actualizar propiedades de UI OnPropertyChanged(nameof(CurrentFlowLMin)); OnPropertyChanged(nameof(EffectiveFlowLMin)); // Actualizar el color según el estado (ya se hace en UpdatePumpColorFromFluid) if (!IsRunning) { ColorButton_oculto = Brushes.Gray; // Bomba apagada } } public override void ucLoaded() { // Objeto hidráulico se crea en UpdateGeometryStart cuando inicia la simulación base.ucLoaded(); UpdatePumpImage(); } public override void ucUnLoaded() { // Remover objeto hidráulico de la simulación if (SimHydraulicPump != null) { hydraulicSimulationManager.Remove(SimHydraulicPump); SimHydraulicPump = null; } } // IHydraulicComponent Implementation [JsonIgnore] public bool HasHydraulicComponents => true; public List GetHydraulicNodes() { var nodes = new List(); // Las bombas no crean nodos propios - deben estar conectadas a otros componentes para funcionar if (!HasConnectedComponents()) { Debug.WriteLine($"Bomba {Nombre}: Sin componentes conectados - no puede funcionar"); } else { //Debug.WriteLine($"Bomba {Nombre}: Conectada a otros componentes - lista para operar"); } return nodes; } public List GetHydraulicElements() { var elements = new List(); // Solo crear elementos si la bomba está conectada a otros componentes if (!HasConnectedComponents()) { Debug.WriteLine($"Bomba {Nombre}: Sin conexiones - no se puede crear elemento hidráulico"); return elements; } if (HasHydraulicComponents) { // Calcular velocidad efectiva basada en el estado de la bomba double effectiveSpeed = IsRunning ? SpeedRatio : 0.0; // Asegurar que la velocidad esté en rango válido effectiveSpeed = Math.Max(0.0, Math.Min(1.0, effectiveSpeed)); // Obtener los nombres de nodos correctos basados en las conexiones var (inletNode, outletNode) = GetConnectedNodeNames(); // Verificar que tenemos nodos válidos if (string.IsNullOrEmpty(inletNode) || string.IsNullOrEmpty(outletNode)) { Debug.WriteLine($"Bomba {Nombre}: Nodos de conexión inválidos - inlet: '{inletNode}', outlet: '{outletNode}'"); return elements; } // Crear bomba con parámetros actuales Element pump; if (hydraulicSimulationManager.EnableNPSHVerification) { // Usar bomba con verificación de NPSH pump = new PumpHQWithSuctionCheck( h0: PumpHead, q0: MaxFlow, speedRel: effectiveSpeed, direction: PumpDirection, enableNpshCheck: true ); } else { // Usar bomba estándar pump = new PumpHQ( h0: PumpHead, q0: MaxFlow, speedRel: effectiveSpeed, direction: PumpDirection ); } // Asignar nombres de nodos para verificación de NPSH if (pump is PumpHQ pumpHQ) { pumpHQ.InletNodeName = inletNode; pumpHQ.OutletNodeName = outletNode; } var pumpElement = new HydraulicElementDefinition( $"{Nombre}_Pump", inletNode, outletNode, pump, $"Bomba {Nombre}" ); elements.Add(pumpElement); Debug.WriteLine($"Bomba {Nombre}: Creando elemento hidráulico - H0={PumpHead}m, Q0={MaxFlow:F6}m³/s ({MaxFlow*3600:F2}m³/h), Velocidad={effectiveSpeed:F2}, Dirección={PumpDirection}"); Debug.WriteLine($"Bomba {Nombre}: Conectando {inletNode} -> {outletNode}"); } return elements; } public void UpdateHydraulicProperties() { // Aquí se pueden hacer validaciones o ajustes antes de la simulación if (!IsRunning) { SpeedRatio = 0.0; } if (SpeedRatio < 0.0) SpeedRatio = 0.0; if (SpeedRatio > 1.0) SpeedRatio = 1.0; // Las verificaciones de NPSH ahora se muestran a través de propiedades // Se eliminaron los Debug.WriteLine para evitar saturación del sistema //Debug.WriteLine($"Bomba {Nombre}: Velocidad={SpeedRatio:F2}, Funcionando={IsRunning}"); } public void ApplyHydraulicResults(Dictionary flows, Dictionary pressures) { // Buscar resultados para esta bomba string pumpBranchName = $"{Nombre}_Pump"; var (inletNodeName, outletNodeName) = GetConnectedNodeNames(); if (flows.ContainsKey(pumpBranchName)) { CurrentFlow = flows[pumpBranchName]; //Debug.WriteLine($"Bomba {Nombre}: Aplicando caudal={CurrentFlow:F6} m³/s ({CurrentFlowLMin:F2} L/min)"); } if (pressures.ContainsKey(inletNodeName)) { CurrentPressure = pressures[inletNodeName]; //Debug.WriteLine($"Bomba {Nombre}: Presión entrada={CurrentPressure:F2} Pa ({CurrentPressureBar:F2} bar)"); } else if (pressures.ContainsKey(outletNodeName)) { CurrentPressure = pressures[outletNodeName]; //Debug.WriteLine($"Bomba {Nombre}: Presión salida={CurrentPressure:F2} Pa ({CurrentPressureBar:F2} bar)"); } // Disparar PropertyChanged para actualizar el UI OnPropertyChanged(nameof(CurrentFlow)); OnPropertyChanged(nameof(CurrentFlowLMin)); OnPropertyChanged(nameof(CurrentPressure)); OnPropertyChanged(nameof(CurrentPressureBar)); } // IHydraulicFlowReceiver Implementation public void SetFlow(double flow) { CurrentFlow = flow; } public double GetFlow() { return CurrentFlow; } // IHydraulicPressureReceiver Implementation public void SetPressure(double pressure) { CurrentPressure = pressure; } public double GetPressure() { return CurrentPressure; } // Helper Methods /// /// Verifica si la bomba tiene componentes conectados a través de tuberías /// private bool HasConnectedComponents() { if (_mainViewModel == null) return false; // Buscar tuberías que conecten esta bomba con otros componentes var connectedPipes = _mainViewModel.ObjetosSimulables .OfType() .Where(pipe => pipe.Id_ComponenteA == Nombre || pipe.Id_ComponenteB == Nombre) .ToList(); return connectedPipes.Any(); } /// /// Obtiene los nombres de nodos correctos para la bomba basándose en las conexiones /// private (string inletNode, string outletNode) GetConnectedNodeNames() { if (_mainViewModel == null) return (string.Empty, string.Empty); string inletNode = string.Empty; string outletNode = string.Empty; // Buscar tuberías conectadas a esta bomba var connectedPipes = _mainViewModel.ObjetosSimulables .OfType() .Where(pipe => pipe.Id_ComponenteA == Nombre || pipe.Id_ComponenteB == Nombre) .ToList(); foreach (var pipe in connectedPipes) { if (pipe.Id_ComponenteB == Nombre && !string.IsNullOrEmpty(pipe.Id_ComponenteA)) { // Esta bomba es el destino, el componente A es la fuente (inlet) inletNode = pipe.Id_ComponenteA; //Debug.WriteLine($"Bomba {Nombre}: Nodo inlet identificado como '{inletNode}'"); } else if (pipe.Id_ComponenteA == Nombre && !string.IsNullOrEmpty(pipe.Id_ComponenteB)) { // Esta bomba es la fuente, el componente B es el destino (outlet) outletNode = pipe.Id_ComponenteB; //Debug.WriteLine($"Bomba {Nombre}: Nodo outlet identificado como '{outletNode}'"); } } return (inletNode, outletNode); } private void InvalidateHydraulicNetwork() { hydraulicSimulationManager?.InvalidateNetwork(); } } /// /// Proveedor de items para la dirección de la bomba /// public class PumpDirectionItemsSource : IItemsSource { public Xceed.Wpf.Toolkit.PropertyGrid.Attributes.ItemCollection GetValues() { var items = new Xceed.Wpf.Toolkit.PropertyGrid.Attributes.ItemCollection(); items.Add(1, "Adelante (+1)"); items.Add(-1, "Atrás (-1)"); return items; } } }