CtrEditor/ObjetosSim/HydraulicComponents/osHydPipe.cs

755 lines
28 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
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;
using LibS7Adv;
namespace CtrEditor.ObjetosSim
{
/// <summary>
/// Tubería hidráulica que conecta componentes del sistema hidráulico
/// </summary>
public partial class osHydPipe : osBase, IHydraulicComponent, IHydraulicFlowReceiver, IHydraulicPressureReceiver, IosBase
{
// Properties
private double _length = 1.0; // metros
private double _diameter = 0.05; // metros (50mm)
private double _roughness = 4.5e-5; // metros - rugosidad del acero comercial
private double _currentFlow = 0.0;
private double _pressureDrop = 0.0;
// Propiedades visuales
[ObservableProperty]
[property: Category("🎨 Apariencia")]
[property: Description("Ancho visual de la tubería en metros")]
[property: Name("Ancho")]
private float ancho = 1.0f;
[ObservableProperty]
[property: Category("🎨 Apariencia")]
[property: Description("Alto visual de la tubería en metros")]
[property: Name("Alto")]
private float alto = 0.05f;
[ObservableProperty]
[property: Category("🎨 Apariencia")]
[property: Description("Color visual de la tubería")]
[property: Name("Color")]
private Brush colorButton_oculto = Brushes.Gray;
[ObservableProperty]
[property: Category("🎨 Apariencia")]
[property: Description("Ángulo de rotación de la tubería en grados")]
[property: Name("Ángulo")]
private float angulo = 0f;
[Category("🔧 Tubería Hidráulica")]
[Description("Longitud de la tubería en metros")]
[Name("Longitud (m)")]
public double Length
{
get => _length;
set => SetProperty(ref _length, Math.Max(0.1, value));
}
[Category("🔧 Tubería Hidráulica")]
[Description("Diámetro interno de la tubería en metros")]
[Name("Diámetro (m)")]
public double Diameter
{
get => _diameter;
set => SetProperty(ref _diameter, Math.Max(0.001, value));
}
[Category("🔧 Tubería Hidráulica")]
[Description("Rugosidad del material en metros")]
[Name("Rugosidad (m)")]
public double Roughness
{
get => _roughness;
set => SetProperty(ref _roughness, Math.Max(1e-6, value));
}
[ObservableProperty]
[property: Category("🔗 Conexiones")]
[property: Description("Primer componente hidráulico conectado")]
[property: Name("Componente A")]
[property: ItemsSource(typeof(osBaseItemsSource<IHydraulicComponent>))]
private string id_ComponenteA = "";
[ObservableProperty]
[property: Category("🔗 Conexiones")]
[property: Description("Segundo componente hidráulico conectado")]
[property: Name("Componente B")]
[property: ItemsSource(typeof(osBaseItemsSource<IHydraulicComponent>))]
private string id_ComponenteB = "";
[Category("📊 Estado")]
[Description("Flujo actual a través de la tubería en m³/s")]
[Name("Flujo Actual (m³/s)")]
[ReadOnly(true)]
public double CurrentFlow
{
get => _currentFlow;
set => SetProperty(ref _currentFlow, value);
}
[Category("📊 Estado")]
[Description("Pérdida de presión en la tubería en Pa")]
[Name("Pérdida Presión (Pa)")]
[ReadOnly(true)]
public double PressureDrop
{
get => _pressureDrop;
set => SetProperty(ref _pressureDrop, value);
}
// Propiedades adicionales para información de fluido
private FluidProperties _currentFluid = new FluidProperties(FluidType.Air);
[Category("🧪 Fluido Actual")]
[DisplayName("Tipo de fluido")]
[Description("Tipo de fluido que atraviesa la tubería")]
[ReadOnly(true)]
public FluidType CurrentFluidType
{
get => _currentFluid.Type;
private set
{
if (_currentFluid.Type != value)
{
_currentFluid.Type = value;
OnPropertyChanged();
OnPropertyChanged(nameof(CurrentFluidDescription));
}
}
}
[Category("🧪 Fluido Actual")]
[DisplayName("Descripción")]
[Description("Descripción completa del fluido actual")]
[ReadOnly(true)]
public string CurrentFluidDescription => _currentFluid.Description;
[Category("🧪 Fluido Actual")]
[DisplayName("Temperatura (°C)")]
[Description("Temperatura del fluido en grados Celsius")]
[ReadOnly(true)]
[JsonProperty]
public double CurrentFluidTemperature => _currentFluid.Temperature;
[Category("🧪 Fluido Actual")]
[DisplayName("Brix (%)")]
[Description("Concentración en grados Brix (para jarabes)")]
[ReadOnly(true)]
[JsonProperty]
public double CurrentFluidBrix => _currentFluid.Brix;
[Category("🧪 Fluido Actual")]
[DisplayName("Concentración (%)")]
[Description("Concentración en porcentaje (para químicos)")]
[ReadOnly(true)]
[JsonProperty]
public double CurrentFluidConcentration => _currentFluid.Concentration;
[Category("🧪 Fluido Actual")]
[DisplayName("Densidad (kg/L)")]
[Description("Densidad del fluido en kg/L")]
[ReadOnly(true)]
[JsonProperty]
public double CurrentFluidDensity => _currentFluid.Density;
[Category("🧪 Fluido Actual")]
[DisplayName("Viscosidad (cP)")]
[Description("Viscosidad dinámica en centipoise")]
[ReadOnly(true)]
[JsonProperty]
public double CurrentFluidViscosity => _currentFluid.Viscosity;
[Category("🧪 Fluido Actual")]
[DisplayName("Velocidad (m/s)")]
[Description("Velocidad del fluido en la tubería")]
[ReadOnly(true)]
[JsonProperty]
public double FluidVelocity
{
get
{
if (Diameter <= 0) return 0;
var area = Math.PI * Math.Pow(Diameter / 2, 2); // m²
return Math.Abs(CurrentFlow) / area; // m/s
}
}
[Category("🧪 Fluido Actual")]
[DisplayName("Número de Reynolds")]
[Description("Número de Reynolds (Re = ρ × v × D / μ) - Caracteriza el régimen de flujo")]
[ReadOnly(true)]
[JsonProperty]
public double ReynoldsNumber
{
get
{
if (CurrentFluidViscosity <= 0 || Diameter <= 0) return 0;
// Re = (ρ × v × D) / μ
// ρ: densidad (kg/m³) - convertir de kg/L
// v: velocidad (m/s)
// D: diámetro (m)
// μ: viscosidad dinámica (Pa·s) - convertir de cP
var density_kg_m3 = CurrentFluidDensity * 1000; // kg/L → kg/m³
var viscosity_pa_s = CurrentFluidViscosity * 0.001; // cP → Pa·s
return (density_kg_m3 * FluidVelocity * Diameter) / viscosity_pa_s;
}
}
[Category("🧪 Fluido Actual")]
[DisplayName("Régimen de Flujo")]
[Description("Tipo de régimen basado en Reynolds (Laminar < 2300 < Turbulento)")]
[ReadOnly(true)]
[JsonProperty]
public string FlowRegime
{
get
{
var re = ReynoldsNumber;
return re switch
{
< 2300 => $"Laminar (Re = {re:F0})",
> 4000 => $"Turbulento (Re = {re:F0})",
_ => $"Transición (Re = {re:F0})"
};
}
}
// Método para exponer propiedades detalladas en serialización JSON
[JsonProperty("DetailedFluidProperties")]
[Browsable(false)]
public Dictionary<string, object> DetailedFluidProperties => new Dictionary<string, object>
{
["CurrentFluidTemperature"] = CurrentFluidTemperature,
["CurrentFluidBrix"] = CurrentFluidBrix,
["CurrentFluidConcentration"] = CurrentFluidConcentration,
["CurrentFluidDensity"] = CurrentFluidDensity,
["CurrentFluidViscosity"] = CurrentFluidViscosity,
["FluidVelocity"] = FluidVelocity,
["ReynoldsNumber"] = ReynoldsNumber,
["FlowRegime"] = FlowRegime
};
[Category("📊 Estado")]
[DisplayName("Flujo (L/min)")]
[Description("Flujo actual en litros por minuto")]
[ReadOnly(true)]
public double CurrentFlowLMin => Math.Abs(CurrentFlow) * 60000.0; // Conversión de m³/s a L/min
[Category("📊 Estado")]
[DisplayName("Dirección")]
[Description("Dirección del flujo")]
[ReadOnly(true)]
public string FlowDirection => CurrentFlow > 0 ? $"{Id_ComponenteA} → {Id_ComponenteB}" :
CurrentFlow < 0 ? $"{Id_ComponenteB} → {Id_ComponenteA}" : "Sin flujo";
[JsonIgnore]
public SolidColorBrush FluidColor
{
get
{
try
{
var colorHex = _currentFluid.Color;
return new SolidColorBrush((Color)ColorConverter.ConvertFromString(colorHex));
}
catch
{
return Brushes.Gray;
}
}
}
private string pipeId = "";
public string PipeId
{
get => pipeId;
set => SetProperty(ref pipeId, value);
}
// Component References
[JsonIgnore]
private IHydraulicComponent ComponenteA = null;
[JsonIgnore]
private IHydraulicComponent ComponenteB = null;
[JsonIgnore]
private PropertyChangedEventHandler componenteAPropertyChangedHandler;
[JsonIgnore]
private PropertyChangedEventHandler componenteBPropertyChangedHandler;
[JsonIgnore]
public Action<float, float> ActualizarTamaño { get; set; }
// IHydraulicPipe Implementation
// Ya implementadas las propiedades Length, Diameter, Roughness arriba
// IHydraulicComponent Implementation
public bool HasHydraulicComponents => true;
public List<HydraulicNodeDefinition> GetHydraulicNodes()
{
var nodes = new List<HydraulicNodeDefinition>();
// Las tuberías conectan componentes existentes, no crean nodos propios
// Los nodos son creados por los componentes conectados (tanques, bombas, etc.)
// La tubería solo actúa como elemento que conecta nodos existentes
return nodes;
}
public List<HydraulicElementDefinition> GetHydraulicElements()
{
var elements = new List<HydraulicElementDefinition>();
// Solo crear elemento si ambos componentes están conectados
if (!string.IsNullOrEmpty(Id_ComponenteA) && !string.IsNullOrEmpty(Id_ComponenteB))
{
// Obtener los nombres de nodos correctos para cada componente
string fromNodeName = GetNodeNameForComponent(Id_ComponenteA, true); // true = es el nodo origen
string toNodeName = GetNodeNameForComponent(Id_ComponenteB, false); // false = es el nodo destino
if (!string.IsNullOrEmpty(fromNodeName) && !string.IsNullOrEmpty(toNodeName))
{
// Crear el elemento Pipe según la documentación
var pipeElement = new Pipe(Length, Diameter, Roughness);
elements.Add(new HydraulicElementDefinition(
name: $"{Nombre}_Pipe",
fromNode: fromNodeName,
toNode: toNodeName,
element: pipeElement,
description: $"Tubería {Nombre} - L:{Length:F2}m, D:{Diameter*1000:F0}mm ({fromNodeName} → {toNodeName})"
));
}
}
return elements;
}
/// <summary>
/// Obtiene el nombre del nodo correcto para un componente dado
/// </summary>
/// <param name="componentName">Nombre del componente</param>
/// <param name="isSource">True si es el nodo origen de la tubería, False si es destino</param>
/// <returns>Nombre del nodo o string vacío si no se encuentra</returns>
private string GetNodeNameForComponent(string componentName, bool isSource)
{
if (string.IsNullOrEmpty(componentName) || _mainViewModel == null)
return "";
// Buscar el componente hidráulico
var component = _mainViewModel.ObjetosSimulables
.FirstOrDefault(s => s is IHydraulicComponent comp &&
((osBase)comp).Nombre == componentName) as IHydraulicComponent;
if (component == null)
return "";
// Obtener los nodos que crea este componente
var nodes = component.GetHydraulicNodes();
if (nodes == null || !nodes.Any())
return "";
// Determinar qué nodo usar según el tipo de componente
var componentType = component.GetType();
// Para tanques: siempre usar su único nodo
if (componentType.Name.Contains("Tank"))
{
return nodes.FirstOrDefault()?.Name ?? "";
}
// Para bombas: usar nodo de entrada o salida según corresponda
if (componentType.Name.Contains("Pump"))
{
if (isSource)
{
// Si la tubería sale desde este componente, usar el nodo de salida de la bomba
return nodes.FirstOrDefault(n => n.Name.EndsWith("_Out"))?.Name ?? "";
}
else
{
// Si la tubería llega a este componente, usar el nodo de entrada de la bomba
return nodes.FirstOrDefault(n => n.Name.EndsWith("_In"))?.Name ?? "";
}
}
// Para otros tipos de componentes, usar el primer nodo disponible
return nodes.FirstOrDefault()?.Name ?? "";
}
public void UpdateHydraulicProperties()
{
// Actualizar propiedades antes de la simulación si es necesario
// Por ejemplo, cambios dinámicos en diámetro o rugosidad
}
public void ApplyHydraulicResults(Dictionary<string, double> flows, Dictionary<string, double> pressures)
{
Debug.WriteLine($"[PIPE] ApplyHydraulicResults for {Nombre}");
Debug.WriteLine($"[PIPE] Available flow keys: {string.Join(", ", flows.Keys)}");
// Solo procesar si ambos componentes están conectados
if (!string.IsNullOrEmpty(Id_ComponenteA) && !string.IsNullOrEmpty(Id_ComponenteB))
{
// Obtener los nombres de nodos correctos
string fromNodeName = GetNodeNameForComponent(Id_ComponenteA, true);
string toNodeName = GetNodeNameForComponent(Id_ComponenteB, false);
Debug.WriteLine($"[PIPE] fromNodeName: '{fromNodeName}', toNodeName: '{toNodeName}'");
if (!string.IsNullOrEmpty(fromNodeName) && !string.IsNullOrEmpty(toNodeName))
{
string branchKey = $"{fromNodeName}->{toNodeName}";
Debug.WriteLine($"[PIPE] Looking for branchKey: '{branchKey}'");
if (flows.TryGetValue(branchKey, out double flow))
{
CurrentFlow = flow;
Debug.WriteLine($"[PIPE] Found flow: {flow}");
}
else
{
Debug.WriteLine($"[PIPE] Flow not found for key '{branchKey}'");
// Try alternative patterns
string[] alternativeKeys = {
$"{toNodeName}->{fromNodeName}",
$"{Id_ComponenteA}_{Id_ComponenteB}_Pipe",
$"{Id_ComponenteB}_{Id_ComponenteA}_Pipe",
$"Pipe_{Id_ComponenteA}_{Id_ComponenteB}",
$"Pipe_{Id_ComponenteB}_{Id_ComponenteA}",
$"{Nombre}_Pipe",
$"Branch_{Nombre}"
};
bool foundFlow = false;
foreach (string altKey in alternativeKeys)
{
if (flows.TryGetValue(altKey, out double altFlow))
{
CurrentFlow = altFlow;
Debug.WriteLine($"[PIPE] Found flow with alternative key '{altKey}': {altFlow}");
foundFlow = true;
break;
}
}
// If still no flow found, try to get flow from connected pump
if (!foundFlow)
{
Debug.WriteLine($"[PIPE] No direct flow found, checking connected components");
// Check if connected to a pump
string[] pumpKeys = {
$"{Id_ComponenteA}_Pump",
$"{Id_ComponenteB}_Pump"
};
foreach (string pumpKey in pumpKeys)
{
if (flows.TryGetValue(pumpKey, out double pumpFlow))
{
CurrentFlow = pumpFlow;
Debug.WriteLine($"[PIPE] Using pump flow from '{pumpKey}': {pumpFlow}");
foundFlow = true;
break;
}
}
}
}
// Calcular pérdida de presión entre nodos
if (pressures.TryGetValue(fromNodeName, out double pressureA) &&
pressures.TryGetValue(toNodeName, out double pressureB))
{
PressureDrop = pressureA - pressureB;
}
}
}
}
// IHydraulicFlowReceiver Implementation
public void SetFlow(double flow)
{
CurrentFlow = flow;
}
public double GetFlow()
{
return CurrentFlow;
}
// IHydraulicPressureReceiver Implementation
public void SetPressure(double pressure)
{
// Para tuberías, la presión se maneja a través de los nodos
}
public double GetPressure()
{
return PressureDrop;
}
// Connection Management
partial void OnId_ComponenteAChanged(string value)
{
if (ComponenteA != null && componenteAPropertyChangedHandler != null)
((INotifyPropertyChanged)ComponenteA).PropertyChanged -= componenteAPropertyChangedHandler;
if (_mainViewModel != null && !string.IsNullOrEmpty(value))
{
ComponenteA = (IHydraulicComponent)_mainViewModel.ObjetosSimulables
.FirstOrDefault(s => s is IHydraulicComponent comp &&
((osBase)comp).Nombre == value);
if (ComponenteA != null)
{
componenteAPropertyChangedHandler = (sender, e) =>
{
if (e.PropertyName == nameof(osBase.Nombre))
{
Id_ComponenteA = ((osBase)sender).Nombre;
}
};
((INotifyPropertyChanged)ComponenteA).PropertyChanged += componenteAPropertyChangedHandler;
}
}
}
partial void OnId_ComponenteBChanged(string value)
{
if (ComponenteB != null && componenteBPropertyChangedHandler != null)
((INotifyPropertyChanged)ComponenteB).PropertyChanged -= componenteBPropertyChangedHandler;
if (_mainViewModel != null && !string.IsNullOrEmpty(value))
{
ComponenteB = (IHydraulicComponent)_mainViewModel.ObjetosSimulables
.FirstOrDefault(s => s is IHydraulicComponent comp &&
((osBase)comp).Nombre == value);
if (ComponenteB != null)
{
componenteBPropertyChangedHandler = (sender, e) =>
{
if (e.PropertyName == nameof(osBase.Nombre))
{
Id_ComponenteB = ((osBase)sender).Nombre;
}
};
((INotifyPropertyChanged)ComponenteB).PropertyChanged += componenteBPropertyChangedHandler;
}
}
}
// osBase Implementation
public static string NombreCategoria() => "Componentes Hidráulicos";
public static string NombreClase() => "Tubería Hidráulica";
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 override void AltoChanged(float value)
{
ActualizarGeometrias();
}
public override void AnchoChanged(float value)
{
ActualizarGeometrias();
}
public void Start()
{
// Las tuberías no participan en la simulación física Bepu
// Solo en el sistema hidráulico
}
public override void UpdateGeometryStart()
{
// En el nuevo sistema unificado, el HydraulicSimulationManager se encarga
// de registrar automáticamente los objetos que implementan IHydraulicComponent
// No necesitamos crear objetos simHydraulic* separados
Debug.WriteLine($"[DEBUG] {Nombre}: UpdateGeometryStart() - ComponenteA: '{Id_ComponenteA}', ComponenteB: '{Id_ComponenteB}'");
ActualizarGeometrias();
}
public override void UpdateGeometryStep()
{
// Los objetos hidráulicos actualizan sus resultados
// a través de ApplyHydraulicResults() desde HydraulicSimulationManager
}
// Método para actualizar fluido desde componente fuente
public void UpdateFluidFromSource()
{
if (_mainViewModel?.ObjetosSimulables == null) return;
// Determinar componente fuente basado en dirección del flujo
string sourceComponentName = CurrentFlow >= 0 ? Id_ComponenteA : Id_ComponenteB;
var sourceComponent = _mainViewModel.ObjetosSimulables
.OfType<IHydraulicComponent>()
.FirstOrDefault(c => c is osBase osb && osb.Nombre == sourceComponentName);
if (sourceComponent is osHydTank tank)
{
var sourceFluid = tank.CurrentOutputFluid;
if (sourceFluid != null)
{
_currentFluid = sourceFluid.Clone();
OnPropertyChanged(nameof(CurrentFluidType));
OnPropertyChanged(nameof(CurrentFluidDescription));
OnPropertyChanged(nameof(FluidColor));
}
}
// Actualizar color visual
if (Math.Abs(CurrentFlow) > 1e-6) // Hay flujo
{
ColorButton_oculto = FluidColor;
}
else
{
ColorButton_oculto = Brushes.Gray; // Sin flujo
}
}
public override void UpdateControl(int elapsedMilliseconds)
{
// En el nuevo sistema unificado, las propiedades se actualizan
// directamente a través de ApplyHydraulicResults()
// CurrentFlow y PressureDrop ya están actualizados por el solver
// Actualizar propiedades del fluido cada ciclo
UpdateFluidFromSource();
// Actualizar propiedades de UI
OnPropertyChanged(nameof(CurrentFlowLMin));
OnPropertyChanged(nameof(FlowDirection));
// Actualizar propiedades de fluido para serialización JSON
OnPropertyChanged(nameof(CurrentFluidTemperature));
OnPropertyChanged(nameof(CurrentFluidBrix));
OnPropertyChanged(nameof(CurrentFluidConcentration));
OnPropertyChanged(nameof(CurrentFluidDensity));
OnPropertyChanged(nameof(CurrentFluidViscosity));
OnPropertyChanged(nameof(FluidVelocity));
OnPropertyChanged(nameof(ReynoldsNumber));
OnPropertyChanged(nameof(FlowRegime));
}
public override void ucLoaded()
{
// En el nuevo sistema unificado, no necesitamos crear objetos separados
base.ucLoaded();
}
public override void ucUnLoaded()
{
// En el nuevo sistema unificado, el HydraulicSimulationManager
// maneja automáticamente el registro/desregistro de objetos
base.ucUnLoaded();
}
public override void UpdatePLC(PLCViewModel plc, int elapsedMilliseconds)
{
// Las tuberías no tienen control PLC directo
}
private void ActualizarGeometrias()
{
try
{
// Actualizar geometría visual
if (this.ActualizarTamaño != null)
this.ActualizarTamaño(Ancho, Alto);
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Error actualizando geometría: {ex.Message}");
}
}
public void Inicializar(int valorInicial)
{
PipeId = $"Pipe_{Nombre}_{valorInicial}";
OnId_ComponenteAChanged(Id_ComponenteA);
OnId_ComponenteBChanged(Id_ComponenteB);
}
public void Disposing()
{
if (ComponenteA != null && componenteAPropertyChangedHandler != null)
((INotifyPropertyChanged)ComponenteA).PropertyChanged -= componenteAPropertyChangedHandler;
if (ComponenteB != null && componenteBPropertyChangedHandler != null)
((INotifyPropertyChanged)ComponenteB).PropertyChanged -= componenteBPropertyChangedHandler;
}
// Constructor
public osHydPipe()
{
PipeId = Guid.NewGuid().ToString();
IsVisFilter = true; // Asegurar que el componente hidráulico sea visible en filtros
}
}
}