Compare commits

...

11 Commits

Author SHA1 Message Date
Miguel af839f4201 Antes de MCP Server 2025-09-06 13:24:28 +02:00
Miguel 04565d21d0 Implement NPSH verification system in hydraulic simulation
- Added NPSH verification logic to PumpHQ and osHydPump classes.
- Introduced properties for NPSH available, cavitation factor, and operational status.
- Implemented NPSH testing on application startup in MainWindow.xaml.cs.
- Created a console application (NPSHTestConsole) for testing NPSH functionalities.
- Developed NPSHTestExample class to demonstrate original user problem and solutions.
- Enhanced osHydTank and osHydPump classes to support NPSH calculations and conditions.
- Added comprehensive logging for debugging and verification purposes.
2025-09-06 10:32:42 +02:00
Miguel e06efacaaf refactor: Update hydraulic components to implement IHydraulicComponent interface 2025-09-06 03:09:09 +02:00
Miguel e6a8bb8cbe feat: Enhance hydraulic simulation components and integrate new object management
- Updated UserControlFactory to support hydraulic simulation manager in AssignDatos method.
- Introduced new classes for hydraulic simulation objects: simHydraulicBase, simHydraulicPump, simHydraulicTank, and simHydraulicPipe.
- Implemented methods for updating properties and applying simulation results in hydraulic components.
- Enhanced osHydPipe, osHydPump, and osHydTank classes to manage hydraulic simulation objects and their properties.
- Added methods for creating, removing, and clearing hydraulic simulation objects in HydraulicSimulationManager.
- Improved error handling and logging in various components to facilitate debugging.
- Refactored XAML and code-behind files to align with new namespace structure for hydraulic components.
2025-09-06 02:58:18 +02:00
Miguel 8e6d457047 Refactor hydraulic components: Updated osHydPipe and osHydPump to improve code clarity by changing region comments to regular comments. Enhanced osHydTank with a new OnMove method and improved initialization in ucHydTank.xaml. Updated StateSerializer to include default values during serialization. 2025-09-06 01:47:57 +02:00
Miguel 0147e010d3 Refactor hydraulic components: Updated osHydPump and added osHydTank class with dynamic level and pressure management. Enhanced XAML for tank visualization and implemented converters for UI binding. Improved error handling in converters and added tests for hydraulic component detection. 2025-09-05 15:41:17 +02:00
Miguel 70bc74fa7d Fix hydraulic simulation issues: prevent double registration of objects, correct MaxFlow unit conversion, enable verbose output for debugging, and ensure proper convergence of the hydraulic solver. 2025-09-04 18:52:46 +02:00
Miguel de4193dee3 Add unit test for hydraulic component type assignability
- Created a new test class TestItemsSource in test_osBaseItemsSource.cs.
- Implemented TestTypeAssignability method to verify that osHydPipe, osHydDischargeTank, and osHydPump correctly implement the IHydraulicComponent interface.
- Added console output to display results of type checks and comparisons.
2025-09-04 18:25:31 +02:00
Miguel 42c9ab9449 Refactor hydraulic components: removed ucPumpExample, added ucHydDischargeTank and ucHydPipe with corresponding logic and XAML. Introduced new converters for UI bindings and created a simple hydraulic system example with testing capabilities. 2025-09-04 17:29:20 +02:00
Miguel d968aa9a2f Mejorado de HydPump 2025-09-04 15:54:14 +02:00
Miguel 3f21061524 Agregada base Hidraulica 2025-09-04 15:14:47 +02:00
34 changed files with 5644 additions and 19 deletions

View File

@ -1,11 +1,14 @@
<Application x:Class="CtrEditor.App" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:CtrEditor"
xmlns:osExtraccion="clr-namespace:CtrEditor.ObjetosSim.Extraccion_Datos"
xmlns:os="clr-namespace:CtrEditor.ObjetosSim" StartupUri="MainWindow.xaml">
xmlns:os="clr-namespace:CtrEditor.ObjetosSim"
xmlns:converters="clr-namespace:CtrEditor.Converters"
StartupUri="MainWindow.xaml">
<Application.Resources>
<local:MeterToPixelConverter x:Key="MeterToPixelConverter" />
<local:LevelToHeightMultiConverter x:Key="LevelToHeightMultiConverter" />
<local:PercentageToHeightConverter x:Key="PercentageToHeightConverter" />
<local:WidthPercentageConverter x:Key="WidthPercentageConverter" />
<local:DistanceToMarginConverter x:Key="DistanceToMarginConverter" />
<local:MarginConverter x:Key="MarginConverter" />
@ -17,11 +20,20 @@
<local:ColorToBrushConverter x:Key="ColorToBrushConverter" />
<local:StringToBrushConverter x:Key="StringToBrushConverter" />
<local:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
<local:BoolToColorConverter x:Key="BoolToColorConverter" />
<local:SubclassFilterConverter x:Key="SubclassFilterConverterosBuscarCoincidencias"
TargetType="{x:Type osExtraccion:osBuscarCoincidencias}" />
<local:SubclassFilterConverter x:Key="SubclassFilterConverterosVMMotor" TargetType="{x:Type os:osVMmotorSim}" />
<local:UnsavedChangesConverter x:Key="UnsavedChangesConverter"/>
<local:BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/>
<converters:TankLevelToHeightConverter x:Key="TankLevelToHeightConverter"/>
<!-- DropShadowEffect para efectos de sombra en texto -->
<DropShadowEffect x:Key="DropShadowEffect"
Color="Black"
BlurRadius="3"
ShadowDepth="1"
Opacity="0.5"/>
<!-- Estilo global para TreeViewItem para evitar errores de binding -->
<Style TargetType="TreeViewItem">

View File

@ -193,11 +193,20 @@ namespace CtrEditor.Controls
var newFilter = new TypeFilterItem(type)
{
DisplayName = GetTypeDisplayName(type),
IsSelected = true
IsSelected = true // Nuevos tipos empiezan seleccionados por defecto
};
SubscribeToTypeFilter(newFilter);
TypeFilters.Add(newFilter);
}
else
{
// Asegurar que los tipos existentes de componentes hidráulicos estén seleccionados
var existingFilter = TypeFilters.FirstOrDefault(tf => tf.Type == type);
if (existingFilter != null && IsHydraulicComponentType(type) && !existingFilter.IsSelected)
{
existingFilter.IsSelected = true;
}
}
}
}
@ -286,6 +295,17 @@ namespace CtrEditor.Controls
methodInfo.Invoke(null, null)?.ToString() :
type.Name;
}
/// <summary>
/// Verifica si un tipo es un componente hidráulico
/// </summary>
private bool IsHydraulicComponentType(Type type)
{
// Verificar si el tipo implementa IHydraulicComponent o está en el namespace de componentes hidráulicos
return type.Namespace != null &&
(type.Namespace.Contains("HydraulicComponents") ||
type.GetInterfaces().Any(i => i.Name.Contains("Hydraulic")));
}
}
/// <summary>

View File

@ -0,0 +1,121 @@
using System;
using System.Diagnostics;
using System.Globalization;
using System.Windows.Data;
namespace CtrEditor.Converters
{
/// <summary>
/// Converter para calcular la altura del nivel de líquido en un tanque
/// basado en el porcentaje de llenado y el tamaño del tanque
/// </summary>
public class TankLevelToHeightConverter : IMultiValueConverter
{
public TankLevelToHeightConverter()
{
Console.WriteLine("TankLevelToHeightConverter: Constructor called");
}
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
// Debug: Registrar valores de entrada
Console.WriteLine($"TankLevelToHeightConverter.Convert: values.Length={values?.Length ?? 0}");
if (values != null)
{
for (int i = 0; i < values.Length; i++)
{
Console.WriteLine($" values[{i}]: {values[i]} (Type: {values[i]?.GetType().Name ?? "null"})");
}
}
if (values.Length != 2)
{
Console.WriteLine("TankLevelToHeightConverter.Convert: Returning 0.0 - wrong values count");
return 0.0;
}
// Convertir flexiblemente los valores a double
if (!TryConvertToDouble(values[0], out double fillPercentage) ||
!TryConvertToDouble(values[1], out double tankSize))
{
Console.WriteLine("TankLevelToHeightConverter.Convert: Conversion failed - unable to convert values to double");
return 0.0;
}
Console.WriteLine($"TankLevelToHeightConverter.Convert: fillPercentage={fillPercentage}, tankSize={tankSize}");
try
{
// Calcular altura basada en el porcentaje de llenado
// tankSize está en metros, convertir a píxeles (aproximación: 100 píxeles por metro)
var tankHeightPixels = tankSize * 100.0;
// Altura del líquido = porcentaje de llenado * altura total del tanque
var liquidHeight = (fillPercentage / 100.0) * tankHeightPixels;
// Aplicar margen interno (restar 8 píxeles para los márgenes del XAML)
var availableHeight = tankHeightPixels - 8.0; // 4px arriba + 4px abajo
var adjustedHeight = (fillPercentage / 100.0) * availableHeight;
// Asegurar que la altura sea positiva y no exceda el contenedor
var result = Math.Max(0.0, Math.Min(adjustedHeight, availableHeight));
Console.WriteLine($"TankLevelToHeightConverter.Convert: Returning {result}");
return result;
}
catch (Exception ex)
{
// En caso de error, retornar 0
Console.WriteLine($"TankLevelToHeightConverter.Convert: Exception - {ex.Message}");
return 0.0;
}
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException("TankLevelToHeightConverter no soporta conversión inversa");
}
/// <summary>
/// Convierte un valor a double de manera flexible
/// </summary>
private static bool TryConvertToDouble(object value, out double result)
{
result = 0.0;
if (value == null)
return false;
if (value is double d)
{
result = d;
return true;
}
if (value is float f)
{
result = f;
return true;
}
if (value is int i)
{
result = i;
return true;
}
if (value is decimal dec)
{
result = (double)dec;
return true;
}
// Intentar conversión de string
if (value is string str && double.TryParse(str, out result))
{
return true;
}
return false;
}
}
}

View File

@ -74,6 +74,9 @@
<None Remove="imagenes\gear.png" />
<None Remove="imagenes\motorNegro.png" />
<None Remove="imagenes\motorVerde.png" />
<None Remove="imagenes\pump.png" />
<None Remove="imagenes\pump_run.png" />
<None Remove="imagenes\pump_stop.png" />
<None Remove="Images\base.png" />
<None Remove="motor2.png" />
<None Remove="tank.png" />
@ -150,6 +153,8 @@
<Resource Include="imagenes\gear.png" />
<Resource Include="imagenes\motorNegro.png" />
<Resource Include="imagenes\motorVerde.png" />
<Resource Include="imagenes\pump_run.png" />
<Resource Include="imagenes\pump_stop.png" />
<Resource Include="imagenes\tank.png" />
<EmbeddedResource Include="Images\base.png" />
</ItemGroup>
@ -188,7 +193,6 @@
</ItemGroup>
<ItemGroup>
<Folder Include="ObjetosSim\HydraulicSimulator\" />
<Folder Include="paddleocr\cls\inference\" />
<Folder Include="paddleocr\det\inference\" />
<Folder Include="paddleocr\keys\" />

View File

@ -275,7 +275,7 @@ namespace CtrEditor
if (userControl != null)
{
UserControlFactory.AssignDatos(userControl, osObjeto, _mainViewModel.simulationManager);
UserControlFactory.AssignDatos(userControl, osObjeto, _mainViewModel.simulationManager, _mainViewModel.hydraulicSimulationManager);
osObjeto._mainViewModel = _mainViewModel;
if (osObjeto.Id == null)

View File

@ -0,0 +1,374 @@
# Sistema de Componentes Hidráulicos - CtrEditor
## 📋 Resumen del Sistema
Este documento describe todos los componentes necesarios para implementar un sistema hidráulico completo en CtrEditor. El sistema está diseñado con una arquitectura modular que permite simular circuitos hidráulicos industriales complejos.
---
## 🎯 Componentes Existentes
### ✅ **Tanque Hidráulico** - `osHydTank`
- **Archivo**: `osHydTank.cs` / `ucHydTank.xaml`
- **Descripción**: Tanque hidráulico con gestión dinámica de nivel y presión configurable
- **Tipos**: Suction, Intermediate, Storage, Process
- **Características**:
- Múltiples tipos de tanque via enum `HydraulicTankType`
- Presión fija o variable (`IsFixedPressure`)
- Gestión completa de flujos entrada/salida
- Cálculos dinámicos de nivel y volumen
- Propiedades visuales para UI (colores, indicadores)
- Conexiones configurables de entrada y salida
### ✅ **Tubería Hidráulica** - `osHydPipe`
- **Archivo**: `osHydPipe.cs` / `ucHydPipe.xaml`
- **Descripción**: Tubería para transporte de fluido hidráulico
- **Características**:
- Cálculo de pérdidas de carga por fricción
- Diferentes materiales y rugosidades
- Diámetros configurables
- Longitud variable
- Resistencia hidráulica calculada
### ✅ **Bomba Hidráulica** - `osHydPump`
- **Archivo**: `osHydPump.cs` / `ucHydPump.xaml`
- **Descripción**: Bomba para generar presión y caudal en el sistema
- **Tipos**: Centrifugal, Positive Displacement, Variable Displacement
- **Características**:
- Curvas características configurables
- Control de velocidad variable
- Eficiencia energética
- Protección contra cavitación
- Control PID integrado
---
## 🔧 Componentes de Control (Por Implementar)
### **Válvulas** - `osHydValve`
- **Archivo**: `osHydValve.cs` / `ucHydValve.xaml`
- **Descripción**: Válvula general configurable para control de flujo
- **Tipos**:
- `Ball` - Válvula de bola (on/off)
- `Gate` - Válvula de compuerta
- `Globe` - Válvula de globo
- `Check` - Válvula de retención
- `Relief` - Válvula de alivio
- `Throttle` - Válvula de estrangulación
- **Estados**: `Open`, `Closed`, `Partial`
- **Propiedades**:
- Coeficiente de flujo (Cv)
- Pérdida de presión
- Posición del actuador
- Tiempo de operación
### **Actuadores Hidráulicos** - `osHydActuator`
- **Archivo**: `osHydActuator.cs` / `ucHydActuator.xaml`
- **Descripción**: Cilindros y actuadores hidráulicos
- **Tipos**:
- `SingleAction` - Cilindro de simple efecto
- `DoubleAction` - Cilindro de doble efecto
- `Rotary` - Actuador rotativo
- **Propiedades**:
- Diámetro del pistón
- Carrera (stroke)
- Fuerza desarrollada
- Velocidad de actuación
- Posición actual
---
## 📊 Instrumentación (Por Implementar)
### **Sensor de Presión** - `osHydPressureSensor`
- **Archivo**: `osHydPressureSensor.cs` / `ucHydPressureSensor.xaml`
- **Descripción**: Medición de presión en el sistema
- **Propiedades**:
- Rango de medición (0-1000 bar)
- Precisión (±0.1%)
- Tiempo de respuesta
- Señal de salida (4-20mA, 0-10V)
- Calibración automática
### **Sensor de Caudal** - `osHydFlowSensor`
- **Archivo**: `osHydFlowSensor.cs` / `ucHydFlowSensor.xaml`
- **Descripción**: Medición de caudal volumétrico
- **Tipos**: Electromagnetic, Turbine, Vortex, Ultrasonic
- **Propiedades**:
- Rango de caudal (L/min)
- Pérdida de presión
- Precisión de medición
- Compensación de temperatura
### **Sensor de Nivel** - `osHydLevelSensor`
- **Archivo**: `osHydLevelSensor.cs` / `ucHydLevelSensor.xaml`
- **Descripción**: Medición de nivel en tanques
- **Tipos**: Ultrasonic, Radar, Capacitive, Float
- **Propiedades**:
- Altura de medición
- Zona muerta
- Material del tanque
- Constante dieléctrica del fluido
### **Sensor de Temperatura** - `osHydTemperatureSensor`
- **Archivo**: `osHydTemperatureSensor.cs` / `ucHydTemperatureSensor.xaml`
- **Descripción**: Medición de temperatura del fluido
- **Tipos**: Thermocouple, RTD, Thermistor
- **Propiedades**:
- Rango de temperatura (-40°C a +200°C)
- Tiempo de respuesta térmica
- Exactitud (±0.1°C)
- Inmersión requerida
---
## 🔀 Conexiones y Distribución (Por Implementar)
### **Conexión en T** - `osHydTee`
- **Archivo**: `osHydTee.cs` / `ucHydTee.xaml`
- **Descripción**: Conexión de tres vías para dividir flujo
- **Propiedades**:
- Diámetro de entrada
- Diámetros de salida
- Coeficiente de pérdida
- Material de construcción
### **Codo 90°** - `osHydElbow`
- **Archivo**: `osHydElbow.cs` / `ucHydElbow.xaml`
- **Descripción**: Cambio de dirección a 90 grados
- **Propiedades**:
- Radio de curvatura
- Ángulo (45°, 90°, custom)
- Factor de pérdida K
- Diámetro interno
### **Conexión Cruzada** - `osHydCross`
- **Archivo**: `osHydCross.cs` / `ucHydCross.xaml`
- **Descripción**: Conexión de cuatro vías
- **Propiedades**:
- Configuración de flujos
- Pérdidas por turbulencia
- Diámetros de conexión
### **Reductor de Diámetro** - `osHydReducer`
- **Archivo**: `osHydReducer.cs` / `ucHydReducer.xaml`
- **Descripción**: Cambio gradual de diámetro de tubería
- **Tipos**: Concentric, Eccentric
- **Propiedades**:
- Diámetro de entrada
- Diámetro de salida
- Ángulo de reducción
- Pérdida por expansión/contracción
### **Distribuidor Multiple** - `osHydManifold`
- **Archivo**: `osHydManifold.cs` / `ucHydManifold.xaml`
- **Descripción**: Distribuidor de múltiples salidas
- **Propiedades**:
- Número de salidas (2-12)
- Distribución de caudal
- Válvulas integradas opcionales
- Presión de entrada común
---
## 🎛️ Control Avanzado (Por Implementar)
### **Regulador de Presión** - `osHydPressureRegulator`
- **Archivo**: `osHydPressureRegulator.cs` / `ucHydPressureRegulator.xaml`
- **Descripción**: Control automático de presión
- **Propiedades**:
- Presión de consigna (setpoint)
- Banda proporcional
- Control PID integrado
- Rango de regulación
- Precisión de control
### **Regulador de Caudal** - `osHydFlowRegulator`
- **Archivo**: `osHydFlowRegulator.cs` / `ucHydFlowRegulator.xaml`
- **Descripción**: Control automático de caudal
- **Propiedades**:
- Caudal de consigna
- Compensación de presión
- Tiempo de respuesta
- Estabilidad de flujo
### **Válvula de Alivio** - `osHydPressureRelief`
- **Archivo**: `osHydPressureRelief.cs` / `ucHydPressureRelief.xaml`
- **Descripción**: Protección contra sobrepresión
- **Propiedades**:
- Presión de apertura
- Presión de cierre
- Capacidad de descarga
- Tipo de pilotaje
---
## 🌡️ Gestión Térmica (Por Implementar)
### **Filtro Hidráulico** - `osHydFilter`
- **Archivo**: `osHydFilter.cs` / `ucHydFilter.xaml`
- **Descripción**: Filtración de partículas y contaminantes
- **Tipos**:
- `Suction` - Filtro de aspiración
- `Return` - Filtro de retorno
- `Pressure` - Filtro de presión
- `Bypass` - Filtro de bypass
- **Propiedades**:
- Grado de filtración (micrones)
- Capacidad de retención
- Pérdida de presión
- Indicador de saturación
### **Enfriador** - `osHydCooler`
- **Archivo**: `osHydCooler.cs` / `ucHydCooler.xaml`
- **Descripción**: Enfriamiento del fluido hidráulico
- **Tipos**: Air-cooled, Water-cooled
- **Propiedades**:
- Capacidad de enfriamiento (kW)
- Temperatura de entrada/salida
- Caudal de fluido refrigerante
- Eficiencia térmica
### **Calentador** - `osHydHeater`
- **Archivo**: `osHydHeater.cs` / `ucHydHeater.xaml`
- **Descripción**: Calentamiento del fluido hidráulico
- **Propiedades**:
- Potencia de calentamiento (kW)
- Temperatura objetivo
- Control de temperatura
- Protección contra sobrecalentamiento
### **Intercambiador de Calor** - `osHydHeatExchanger`
- **Archivo**: `osHydHeatExchanger.cs` / `ucHydHeatExchanger.xaml`
- **Descripción**: Intercambio térmico entre fluidos
- **Tipos**: Plate, Shell-and-tube, Coaxial
- **Propiedades**:
- Coeficiente de transferencia térmica
- Área de intercambio
- Configuración de flujos
- Eficiencia térmica
---
## 🔄 Componentes Especializados (Por Implementar)
### **Acumulador Hidráulico** - `osHydAccumulator`
- **Archivo**: `osHydAccumulator.cs` / `ucHydAccumulator.xaml`
- **Descripción**: Almacenamiento de energía hidráulica
- **Tipos**:
- `Bladder` - Membrana elastomérica
- `Piston` - Pistón separador
- `Diaphragm` - Diafragma metálico
- **Propiedades**:
- Volumen total
- Presión de precarga
- Volumen útil
- Tiempo de descarga
### **Motor Hidráulico** - `osHydMotor`
- **Archivo**: `osHydMotor.cs` / `ucHydMotor.xaml`
- **Descripción**: Conversión de energía hidráulica a mecánica rotativa
- **Tipos**:
- `Gear` - Motor de engranajes
- `Vane` - Motor de paletas
- `Piston` - Motor de pistones
- `Radial` - Motor radial
- **Propiedades**:
- Cilindrada (cm³/rev)
- Velocidad nominal (rpm)
- Par motor (Nm)
- Eficiencia volumétrica/mecánica
---
## 📋 Plan de Implementación
### **Fase 1 - Componentes Básicos** ⭐⭐⭐
1. `osHydValve` (Ball, Check, Relief)
2. `osHydPressureSensor`
3. `osHydFlowSensor`
4. `osHydTee`
### **Fase 2 - Control Intermedio** ⭐⭐
5. `osHydActuator`
6. `osHydPressureRegulator`
7. `osHydFilter`
8. `osHydManifold`
### **Fase 3 - Componentes Avanzados**
9. `osHydAccumulator`
10. `osHydMotor`
11. `osHydCooler`
12. `osHydHeatExchanger`
### **Fase 4 - Instrumentación Completa**
13. `osHydLevelSensor`
14. `osHydTemperatureSensor`
15. `osHydFlowRegulator`
16. Componentes de conexión restantes
---
## 💡 Arquitectura de Interfaces
### **Interfaces Base**
```csharp
// Control de flujo
public interface IHydraulicFlowController
{
double FlowCoefficient { get; set; }
double PressureDrop { get; }
ValveState CurrentState { get; set; }
}
// Actuadores
public interface IHydraulicActuator
{
double Force { get; }
double Stroke { get; set; }
double Velocity { get; }
ActuatorType Type { get; }
}
// Sensores
public interface IHydraulicSensor
{
double MeasuredValue { get; }
string Units { get; }
bool IsCalibrated { get; set; }
double Accuracy { get; }
}
// Componentes térmicos
public interface IHydraulicThermalComponent
{
double ThermalCapacity { get; }
double HeatTransferCoefficient { get; }
double OperatingTemperature { get; set; }
}
```
---
## 🎯 Objetivos del Sistema
- **Modularidad**: Cada componente es independiente y reutilizable
- **Realismo**: Cálculos basados en principios de ingeniería hidráulica
- **Flexibilidad**: Configuración adaptable a diferentes aplicaciones
- **Integración**: Compatible con el sistema de simulación existente
- **Escalabilidad**: Fácil adición de nuevos componentes
- **Documentación**: Cada componente bien documentado y ejemplificado
---
## 📚 Referencias Técnicas
- ISO 1219-1: Símbolos gráficos para esquemas de circuitos hidráulicos
- ISO 4413: Transmisiones hidráulicas - Reglas generales y requisitos de seguridad
- NFPA T3.5.17: Hidráulica industrial - Estándares de filtración
- API 610: Bombas centrífugas para servicios de refinería petroquímica
---
*Documento generado automáticamente - CtrEditor v2024*
*Última actualización: Septiembre 2025*

View File

@ -118,3 +118,354 @@ if (velocity.Linear.Z > maxUpwardVelocity) {
- **Solución integral**: Resuelve tanto el problema de elevación como el de contactos múltiples simultáneos
- **Código más simple**: Elimina la complejidad del sistema de tracking de densidad de contactos
# Sistema Hidráulico Integrado con HydraulicSimulationManager
## Arquitectura del Sistema Hidráulico
Se ha implementado un sistema de simulación hidráulica completo que funciona en paralelo con la simulación física BEPU. El sistema está basado en el `HydraulicSimulationManager` que actúa como puente entre los objetos gráficos derivados de `osBase` y el motor de simulación hidráulica.
### Componentes Principales
#### HydraulicSimulationManager
- **Función**: Gestiona el ciclo de vida y la comunicación entre objetos hidráulicos y el solver
- **Integración**: Se ejecuta en paralelo con BEPU en el timer loop principal de MainViewModel
- **Responsabilidades**:
- Registro automático de objetos que implementan `IHydraulicComponent`
- Construcción dinámica de la red hidráulica
- Aplicación de resultados a los objetos tras cada iteración del solver
#### Interfaces Hidráulicas
- **IHydraulicComponent**: Interfaz base para todos los objetos hidráulicos
- **IHydraulicPump**: Para bombas con curvas H-Q
- **IHydraulicPipe**: Para tuberías con pérdidas por fricción
- **IHydraulicTank**: Para tanques con cálculo de nivel dinámico
- **IHydraulicValve**: Para válvulas con coeficiente Kv
## Mejoras Implementadas en Objetos Hidráulicos
### 1. Sistema de Conexiones Mejorado en osHydPipe
**Problema Original**: El sistema de conexiones era limitado y no seguía el patrón establecido en otros objetos del sistema.
**Solución Implementada**:
```csharp
// Antes - Sistema limitado
[ObservableProperty] string id_InletComponent = "";
[ObservableProperty] string id_OutletComponent = "";
// Después - Patrón consistente
[ItemsSource(typeof(osBaseItemsSource<IHydraulicComponent>))]
[ObservableProperty] string id_ComponenteA = "";
[ItemsSource(typeof(osBaseItemsSource<IHydraulicComponent>))]
[ObservableProperty] string id_ComponenteB = "";
```
**Beneficios**:
- **Filtrado automático**: Solo muestra objetos que implementan `IHydraulicComponent` en el PropertyGrid
- **Consistencia**: Sigue el mismo patrón usado en `ucTransporteGuias` con `Motor`
- **Mantenimiento automático**: PropertyChangedHandlers mantienen sincronizados los nombres
- **Validación**: Solo crea elementos hidráulicos cuando ambos componentes están conectados
### 2. Lógica de Nodos Corregida
**Problema Original**: Las tuberías intentaban crear sus propios nodos, generando duplicación y conflictos.
**Solución Implementada**:
```csharp
// Las tuberías no crean nodos propios
public List<HydraulicNodeDefinition> GetHydraulicNodes()
{
// Los nodos son creados por los componentes conectados (tanques, bombas, etc.)
return new List<HydraulicNodeDefinition>();
}
// Solo conectan nodos existentes
public List<HydraulicElementDefinition> GetHydraulicElements()
{
if (!string.IsNullOrEmpty(Id_ComponenteA) && !string.IsNullOrEmpty(Id_ComponenteB))
{
var pipeElement = new Pipe(Length, Diameter, Roughness);
// Conecta directamente usando nombres de componentes como nodos
return new List<HydraulicElementDefinition> {
new HydraulicElementDefinition($"{Nombre}_Pipe", Id_ComponenteA, Id_ComponenteB, pipeElement)
};
}
return new List<HydraulicElementDefinition>();
}
```
### 3. Integración Completa con ItemsSource
**En osHydDischargeTank**:
```csharp
[Category("🔗 Conexiones")]
[Name("Componente Entrada")]
[ItemsSource(typeof(osBaseItemsSource<IHydraulicComponent>))]
[ObservableProperty] string id_InletComponent = "";
```
## Flujo de Simulación Hidráulica
### 1. Registro Automático
- Los objetos que implementan `IHydraulicComponent` se registran automáticamente en `HydraulicSimulationManager`
- El sistema detecta cuando hay cambios y marca la red para reconstrucción (`_networkNeedsRebuild = true`)
### 2. Construcción de Red
- **BuildNodesFromObjects()**: Crea nodos basándose en las definiciones de cada objeto
- **BuildBranchesFromObjects()**: Crea elementos (tuberías, bombas, válvulas) que conectan los nodos
- La red resultante es compatible con el solver `HydraulicNetwork.Solve()`
### 3. Resolución y Aplicación
- **UpdateObjectProperties()**: Actualiza propiedades antes del solver (apertura de válvulas, velocidad de bombas, etc.)
- **Network.Solve()**: Resuelve el sistema de ecuaciones usando el algoritmo iterativo
- **ApplyResultsToObjects()**: Aplica caudales y presiones calculados a cada objeto
## Patrón de Conexión Establecido
El sistema establecido para conexiones sigue este patrón consistente:
```csharp
// 1. Propiedad string con ItemsSource filtrado
[ItemsSource(typeof(osBaseItemsSource<IInterfaceEspecifica>))]
[ObservableProperty] string id_ComponenteConectado = "";
// 2. Referencia privada al objeto real
[JsonIgnore] private IInterfaceEspecifica ComponenteConectado = null;
// 3. PropertyChangedHandler para mantener sincronización
[JsonIgnore] private PropertyChangedEventHandler componentePropertyChangedHandler;
// 4. Método OnChanged que maneja la conexión
partial void OnId_ComponenteConectadoChanged(string value)
{
// Desconectar anterior si existe
if (ComponenteConectado != null && componentePropertyChangedHandler != null)
((INotifyPropertyChanged)ComponenteConectado).PropertyChanged -= componentePropertyChangedHandler;
// Buscar y conectar nuevo componente
if (_mainViewModel != null && !string.IsNullOrEmpty(value))
{
ComponenteConectado = (IInterfaceEspecifica)_mainViewModel.ObjetosSimulables
.FirstOrDefault(s => s is IInterfaceEspecifica comp &&
((osBase)comp).Nombre == value);
// Establecer handler para sincronización de nombres
if (ComponenteConectado != null)
{
componentePropertyChangedHandler = (sender, e) =>
{
if (e.PropertyName == nameof(osBase.Nombre))
Id_ComponenteConectado = ((osBase)sender).Nombre;
};
((INotifyPropertyChanged)ComponenteConectado).PropertyChanged += componentePropertyChangedHandler;
}
}
}
```
## Beneficios del Sistema Integrado
- **Doble simulación**: Simulación física (BEPU) y hidráulica funcionan en paralelo sin interferencia
- **Interfaces unificadas**: Todos los objetos hidráulicos implementan interfaces consistentes
- **Red dinámica**: La red hidráulica se reconstruye automáticamente cuando cambian las conexiones
- **Resultados bidireccionales**: Los resultados hidráulicos pueden influir en la simulación física y viceversa
- **Patrón consistente**: Todas las conexiones siguen el mismo patrón establecido
- **Mantenimiento automático**: Los nombres y referencias se mantienen sincronizados automáticamente
## Resolución de Problemas de Simulación Hidráulica - Bomba con Caudal Cero
### Problema Identificado
La bomba hidráulica mostraba velocidad=1.0 y funcionando=true pero registraba 0 caudal y 0 presión constantemente.
### Causas Encontradas
1. **Doble registro de objetos**: Los objetos hidráulicos se registraban dos veces - una al cargar cada objeto individualmente y otra al ejecutar `RegisterLoadedHydraulicObjects()` después de cargar el proyecto completo.
2. **Unidades incorrectas en MaxFlow**: La configuración JSON tenía `MaxFlow: 10.0` pero el código manejaba valores en m³/s. El valor 10.0 se interpretaba como 10 m³/s (36,000 m³/h) en lugar de 10 m³/h.
3. **Falta de información de debugging**: El `VerboseOutput` estaba desactivado, ocultando información crítica sobre la construcción de la red y convergencia del solver.
### Soluciones Implementadas
#### 1. Prevención de Doble Registro
Modificado `RegisterHydraulicObjectIfNeeded()` en `MainViewModel.cs` para verificar si el objeto ya está registrado antes de agregarlo:
```csharp
if (!hydraulicSimulationManager.HydraulicObjects.Contains(obj))
{
hydraulicSimulationManager.RegisterHydraulicObject(obj);
}
```
#### 2. Corrección de Unidades MaxFlow
Implementado en `osHydPump.cs` conversión automática de unidades para `MaxFlow`:
- Si el valor > 1.0, se asume que está en m³/h y se convierte a m³/s (dividiendo por 3600)
- Se agregó logging detallado mostrando ambas unidades para verificación
#### 3. Activación de Verbose Output
- Habilitado `VerboseOutput = true` por defecto en `HydraulicSimulationManager`
- Agregado logging detallado de construcción de red mostrando todos los nodos y ramas
- Implementado logging periódico de resultados (cada 5 segundos aprox.) mostrando flujos y presiones
- Mejorado logging de errores de convergencia con detalles de iteraciones y residual
#### 4. Debugging Mejorado
Los logs ahora muestran:
- Detalles completos de nodos (presión fija/libre, valores de presión)
- Ramas con elementos y tipos de componentes
- Resultados de flujos en m³/s y m³/h
- Presiones en Pa y bar
- Estado de convergencia del solver con detalles de error
### Corrección Final: Convergencia del Solver Hidráulico
#### Problema de Convergencia
El solver no convergía porque el sistema carecía de suficientes nodos de presión fija:
- La bomba tenía nodos de entrada y salida libres
- Solo el tanque de descarga tenía presión fija
- El solver necesita al menos dos puntos de referencia para sistemas complejos
#### Solución Implementada
**1. Nodo de Succión en la Bomba**
```csharp
// En GetHydraulicNodes() de osHydPump
double suctionPressure = 101325.0; // Pa (1 atm)
nodes.Add(new HydraulicNodeDefinition($"{Nombre}_In", true, suctionPressure, "Entrada de la bomba (succión)"));
```
**2. Parámetros Optimizados del Solver**
- MaxIterations: 200 → 300
- Tolerance: 1e-4 → 1e-3 (más relajada para convergencia inicial)
- RelaxationFactor: 0.8 (mantiene estabilidad)
**3. Sistema de Referencias Hidráulicas**
- Nodo de succión: 101325 Pa (presión atmosférica)
- Nodo de descarga: 101325 + TankPressure Pa (atmosférica + hidrostática)
- Esto simula un sistema real con tanque de succión y descarga
### Corrección Final: Problema del DataContext en UI
#### Problema Identificado
Aunque la simulación funcionaba correctamente y los valores se calculaban bien, estos no se mostraban en el UserControl de la bomba porque:
- El UserControl tenía su propio `DataContext` definido en el XAML
- Esto creaba una instancia separada de `osHydPump` solo para el UI
- Los valores se actualizaban en el objeto real, pero el UI mostraba el objeto falso
#### Solución Implementada
```xml
<!-- Eliminado el DataContext problemático -->
<!-- <UserControl.DataContext>
<vm:osHydPump ImageSource_oculta="/imagenes/pump_run.png" />
</UserControl.DataContext> -->
<!-- DataContext se establece desde el objeto padre, no aquí -->
```
### Resultado Final del Sistema Hidráulico
Sistema hidráulico completamente funcional:
- ✅ Convergencia estable del solver con residuales < 1e-3
- ✅ Registro único de objetos (sin duplicados)
- ✅ Conversión automática de unidades (m³/h ↔ m³/s)
- ✅ Logging detallado para monitoreo y debugging
- ✅ Bomba genera flujo y presión reales según configuración
- ✅ **UserControl muestra valores actualizados en tiempo real**
## Nuevo Tanque Hidráulico Avanzado (osHydTank)
### Arquitectura Modular Implementada
Se siguió el enfoque arquitectónico de **"Tanques como Terminales + Pipes como Conectores"** para crear un sistema más realista y mantenible:
```
[Tanque Succión] → [Pipe] → [Bomba] → [Pipe] → [Válvula] → [Pipe] → [Tanque Descarga]
```
### Características del osHydTank
**Gestión Dinámica de Nivel:**
- Nivel calculado en base a flujos de entrada y salida
- Seguimiento de volumen actual vs. máximo/mínimo
- Indicadores visuales de estado del tanque
- Detección automática de desbordamiento y vaciado
**Presión Configurable:**
- Presión independiente del nivel (simulando PID externo)
- Opción de presión fija o variable
- Diferentes tipos de tanque: Succión, Intermedio, Almacenamiento, Proceso
**Interfaces Hidráulicas:**
- `IHydraulicComponent`: Integración con red hidráulica
- `IHydraulicFlowReceiver`: Recepción de flujos calculados
- `IHydraulicPressureReceiver`: Actualización de presiones
**UserControl Avanzado:**
- Visualización en tiempo real del nivel de líquido
- Indicadores de tipo de tanque y conexiones
- Balance de flujo con códigos de color
- Conversión automática de TankLevelToHeightConverter
## Solución de Problemas de Visualización osHydTank
### Problema
El componente `osHydTank` no se mostraba correctamente en el canvas a pesar de registrarse exitosamente en el sistema hidráulico. Los logs mostraban errores de binding en el MultiBinding del rectángulo de nivel del líquido.
### Análisis
1. **Error de Converter**: El XAML usaba `TankLevelToHeightConverter` que tenía incompatibilidades de tipos
2. **Falta de inicialización**: El UserControl `ucHydTank` no llamaba a `ucLoaded()`/`ucUnLoaded()` como otros componentes
3. **Binding incorrecto**: No se convertía `Tamano` a píxeles antes del MultiBinding
### Solución Implementada
1. **Cambio de Converter**: Reemplazamos `TankLevelToHeightConverter` por `LevelToHeightMultiConverter` (que funciona en otros componentes)
2. **Conversión a Píxeles**: Agregamos `MeterToPixelConverter` a `Tamano` antes del MultiBinding
3. **Ciclo de Vida**: Implementamos las llamadas correctas a `ucLoaded()`/`ucUnLoaded()` en `ucHydTank.xaml.cs`
### Cambios Técnicos
- **XAML**: `<Binding Path="Tamano" Converter="{StaticResource MeterToPixelConverter}"/>`
- **UserControl**: Agregado `Unloaded += OnUserControlUnloaded` y llamadas a `ucLoaded()`/`ucUnLoaded()`
- **Converter Corregido**: `LevelToHeightMultiConverter` ahora usa `System.Convert.ToDouble()` para flexibilidad de tipos
- **Patrón consistente** con `ucHydPump`, `ucHydDischargeTank` y `ucHydPipe`
### Detalle del Error del Converter
El `LevelToHeightMultiConverter` esperaba tipos específicos (`float` y `double`) pero recibía tipos invertidos:
- `FillPercentage`: `double` (esperaba `float`)
- `MeterToPixelConverter` output: `float` (esperaba `double`)
**Solución**: Implementar función `TryConvertToDouble()` robusta que maneja:
- Valores especiales de WPF (`DependencyProperty.UnsetValue`, `MS.Internal.NamedObject`)
- Múltiples tipos numéricos (`double`, `float`, `int`, `decimal`)
- Conversión segura de strings
- Verificación `IConvertible` antes de usar `System.Convert.ToDouble()`
### Validación de Arquitectura
El sistema ahora requiere que:
- Todo circuito hidráulico **inicie con un tanque**
- Todo circuito hidráulico **termine con un tanque**
- Los `pipes` sean los únicos conectores entre componentes
- Cada componente tenga **responsabilidad única**
### Beneficios del Nuevo Diseño
**Realismo Físico:**
- Refleja sistemas hidráulicos reales donde tanques son puntos de referencia
- Presión independiente del nivel (como en sistemas industriales)
- Gestión de volumen dinámica
**Escalabilidad:**
- Patrón consistente para nuevos componentes hidráulicos
- Fácil conexión de válvulas, filtros, intercambiadores de calor
- Sistema preparado para redes complejas
**Mantenibilidad:**
- Principio de responsabilidad única por componente
- Interfaces claras y bien definidas
- Debugging simplificado con logging detallado
**Flexibilidad:**
- Diferentes tipos de tanques para diferentes funciones
- Configuración de presión independiente
- Conectividad modular via pipes
La bomba ahora debe mostrar correctamente:
- **Presión actual** en bar (ej: "1.0 bar")
- **Caudal actual** en L/min (ej: "165.8 L/min")

View File

@ -0,0 +1,950 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using HydraulicSimulator.Models;
using CtrEditor.ObjetosSim;
namespace CtrEditor.HydraulicSimulator
{
/// <summary>
/// Gestiona la simulación hidráulica similar a SimulationManagerBEPU
/// </summary>
public class HydraulicSimulationManager : IDisposable
{
#region Properties and Fields
/// <summary>
/// Red hidráulica principal que contiene todos los nodos y ramas
/// </summary>
public HydraulicNetwork Network { get; private set; }
/// <summary>
/// Lista de objetos simulables que tienen componentes hidráulicos
/// </summary>
public List<osBase> HydraulicObjects { get; private set; }
/// <summary>
/// Fluido utilizado en la simulación (agua por defecto)
/// </summary>
public Fluid SimulationFluid { get; set; }
/// <summary>
/// Resultado de la última simulación
/// </summary>
public SolutionResult LastSolutionResult { get; private set; }
/// <summary>
/// Tiempo global de la simulación hidráulica
/// </summary>
public float GlobalTime { get; private set; }
/// <summary>
/// Indica si la simulación hidráulica está habilitada
/// </summary>
public bool IsHydraulicSimulationEnabled { get; set; } = true;
/// <summary>
/// Indica si la verificación de NPSH está habilitada
/// </summary>
public bool EnableNPSHVerification { get; set; } = true;
/// <summary>
/// Parámetros del solver
/// </summary>
public int MaxIterations { get; set; } = 300;
public double Tolerance { get; set; } = 1e-3; // Tolerancia más relajada para convergencia inicial
public double RelaxationFactor { get; set; } = 0.8;
public bool VerboseOutput { get; set; } = false; // Activado para debugging
/// <summary>
/// Contador de pasos de simulación para optimizaciones
/// </summary>
private int _stepCount = 0;
/// <summary>
/// Cronómetro para medir tiempo de ejecución
/// </summary>
private Stopwatch _stopwatch;
/// <summary>
/// Diccionario para mapear IDs de objetos a elementos hidráulicos
/// </summary>
private Dictionary<string, osBase> _objectMapping;
/// <summary>
/// Indica si hay cambios pendientes en la red que requieren reconstrucción
/// </summary>
private bool _networkNeedsRebuild = true;
#endregion
#region Constructor
/// <summary>
/// Constructor del gestor de simulación hidráulica
/// </summary>
public HydraulicSimulationManager()
{
// Inicializar componentes
SimulationFluid = Fluid.Water20C; // Agua a 20°C por defecto
Network = new HydraulicNetwork(SimulationFluid);
HydraulicObjects = new List<osBase>();
_objectMapping = new Dictionary<string, osBase>();
_stopwatch = new Stopwatch();
// Inicializar resultado vacío
LastSolutionResult = new SolutionResult(false)
{
ErrorMessage = "Simulación no ejecutada"
};
GlobalTime = 0.0f;
Debug.WriteLine("HydraulicSimulationManager inicializado");
}
#endregion
#region Object Management
/// <summary>
/// Registra un objeto simulable que tiene componentes hidráulicos
/// </summary>
/// <param name="hydraulicObject">Objeto con componentes hidráulicos</param>
public void RegisterHydraulicObject(osBase hydraulicObject)
{
if (hydraulicObject == null)
return;
if (!HydraulicObjects.Contains(hydraulicObject))
{
HydraulicObjects.Add(hydraulicObject);
_objectMapping[hydraulicObject.Nombre] = hydraulicObject;
_networkNeedsRebuild = true;
// Debug.WriteLine($"Objeto hidráulico registrado: {hydraulicObject.Nombre}");
}
}
/// <summary>
/// Desregistra un objeto simulable hidráulico
/// </summary>
/// <param name="hydraulicObject">Objeto a desregistrar</param>
public void UnregisterHydraulicObject(osBase hydraulicObject)
{
if (hydraulicObject == null)
return;
if (HydraulicObjects.Remove(hydraulicObject))
{
_objectMapping.Remove(hydraulicObject.Nombre);
_networkNeedsRebuild = true;
// Debug.WriteLine($"Objeto hidráulico desregistrado: {hydraulicObject.Nombre}");
}
}
/// <summary>
/// Limpia todos los objetos hidráulicos registrados
/// </summary>
public void ClearHydraulicObjects()
{
HydraulicObjects.Clear();
_objectMapping.Clear();
_networkNeedsRebuild = true;
// Debug.WriteLine("Todos los objetos hidráulicos han sido limpiados");
}
#endregion
#region Network Building
/// <summary>
/// Reconstruye la red hidráulica basada en los objetos registrados
/// </summary>
private void RebuildNetwork()
{
if (!_networkNeedsRebuild)
return;
// Debug.WriteLine("Reconstruyendo red hidráulica...");
// Crear nueva red
Network = new HydraulicNetwork(SimulationFluid);
// Agregar nodos y elementos basados en los objetos hidráulicos
BuildNodesFromObjects();
BuildBranchesFromObjects();
_networkNeedsRebuild = false;
// Debug.WriteLine($"Red reconstruida: {Network.Nodes.Count} nodos, {Network.Branches.Count} ramas");
// Verbose output deshabilitado para mejorar rendimiento
/*
if (VerboseOutput)
{
Debug.WriteLine("=== DETALLES DE LA RED HIDRÁULICA ===");
Debug.WriteLine("Nodos:");
foreach (var node in Network.Nodes)
{
string pressureInfo = node.Value.FixedP ? $"Presión fija: {node.Value.P:F0} Pa" : "Presión libre";
Debug.WriteLine($" - {node.Key}: {pressureInfo}");
}
Debug.WriteLine("Ramas:");
foreach (var branch in Network.Branches)
{
Debug.WriteLine($" - {branch.Name}: {branch.N1} -> {branch.N2} ({branch.Elements.Count} elementos)");
foreach (var element in branch.Elements)
{
Debug.WriteLine($" * {element.GetType().Name}");
}
}
Debug.WriteLine("=== FIN DETALLES RED ===");
}
*/
}
/// <summary>
/// Construye los nodos de la red basándose en los objetos hidráulicos
/// </summary>
private void BuildNodesFromObjects()
{
foreach (var obj in HydraulicObjects)
{
// Verificar si el objeto implementa la interfaz hidráulica
if (obj is IHydraulicComponent hydraulicComponent && hydraulicComponent.HasHydraulicComponents)
{
// Obtener nodos definidos por el objeto
var nodeDefinitions = hydraulicComponent.GetHydraulicNodes();
foreach (var nodeDef in nodeDefinitions)
{
// Agregar nodo a la red
Network.AddNode(nodeDef.Name, nodeDef.IsFixedPressure ? nodeDef.Pressure : null);
// Verbose output deshabilitado para mejorar rendimiento
/*
if (VerboseOutput)
{
Debug.WriteLine($"Nodo agregado: {nodeDef.Name} " +
$"(Presión fija: {nodeDef.IsFixedPressure}, " +
$"Presión: {nodeDef.Pressure?.ToString() ?? "libre"})");
}
*/
}
}
else
{
// Objeto sin interfaz hidráulica - crear nodos básicos por compatibilidad
string nodeName = $"Node_{obj.Nombre}";
bool isFixedPressure = DetermineIfFixedPressure(obj);
double? pressure = isFixedPressure ? GetObjectPressure(obj) : null;
Network.AddNode(nodeName, pressure);
}
}
}
/// <summary>
/// Construye las ramas (conexiones) entre nodos
/// </summary>
private void BuildBranchesFromObjects()
{
foreach (var obj in HydraulicObjects)
{
// Verificar si el objeto implementa la interfaz hidráulica
if (obj is IHydraulicComponent hydraulicComponent && hydraulicComponent.HasHydraulicComponents)
{
// Obtener elementos definidos por el objeto
var elementDefinitions = hydraulicComponent.GetHydraulicElements();
foreach (var elemDef in elementDefinitions)
{
// Crear rama con el elemento
var elements = new List<Element> { elemDef.Element };
Network.AddBranch(elemDef.FromNode, elemDef.ToNode, elements, elemDef.Name);
// Verbose output deshabilitado para mejorar rendimiento
/*
if (VerboseOutput)
{
Debug.WriteLine($"Rama agregada: {elemDef.Name} " +
$"({elemDef.FromNode} -> {elemDef.ToNode})");
}
*/
}
}
else
{
// Objeto sin interfaz hidráulica - crear elementos básicos por compatibilidad
var elements = CreateHydraulicElementsFromObject(obj);
if (elements.Any())
{
string fromNode = GetSourceNode(obj);
string toNode = GetTargetNode(obj);
if (!string.IsNullOrEmpty(fromNode) && !string.IsNullOrEmpty(toNode))
{
Network.AddBranch(fromNode, toNode, elements, $"Branch_{obj.Nombre}");
}
}
}
}
}
#endregion
#region Helper Methods for Object Analysis
/// <summary>
/// Determina si un objeto debe tener presión fija
/// </summary>
private bool DetermineIfFixedPressure(osBase obj)
{
// Implementación que será extendida con tipos específicos
// Por ejemplo: tanques, descargas atmosféricas = presión fija
// Uniones, conexiones = presión libre
return false; // Por defecto, presión libre
}
/// <summary>
/// Obtiene la presión de un objeto (si tiene presión fija)
/// </summary>
private double GetObjectPressure(osBase obj)
{
// Implementación que será extendida
return 0.0; // Por defecto, presión atmosférica
}
/// <summary>
/// Crea elementos hidráulicos basándose en un objeto
/// </summary>
private List<Element> CreateHydraulicElementsFromObject(osBase obj)
{
var elements = new List<Element>();
// Implementación que será extendida con tipos específicos
// Por ejemplo:
// - Si es una bomba: crear PumpHQ
// - Si es una tubería: crear Pipe
// - Si es una válvula: crear ValveKv
return elements;
}
/// <summary>
/// Obtiene el nodo fuente de un objeto
/// </summary>
private string GetSourceNode(osBase obj)
{
// Implementación que será extendida
return $"Node_{obj.Nombre}_In";
}
/// <summary>
/// Obtiene el nodo destino de un objeto
/// </summary>
private string GetTargetNode(osBase obj)
{
// Implementación que será extendida
return $"Node_{obj.Nombre}_Out";
}
#endregion
#region Simulation Step
/// <summary>
/// Ejecuta un paso de simulación hidráulica (equivalente al Step de BEPU)
/// </summary>
public void Step(float deltaTime = 0.016f) // ~60 FPS por defecto
{
if (!IsHydraulicSimulationEnabled)
return;
_stopwatch.Restart();
try
{
// Actualizar tiempo global
GlobalTime += deltaTime;
_stepCount++;
// Reconstruir red si es necesario
RebuildNetwork();
// Solo resolver si tenemos objetos hidráulicos
if (HydraulicObjects.Count > 0 && Network.Branches.Count > 0)
{
// Actualizar propiedades de objetos antes de resolver
UpdateObjectProperties();
// Resolver la red hidráulica
LastSolutionResult = Network.Solve(
MaxIterations,
Tolerance,
RelaxationFactor,
VerboseOutput
);
if (LastSolutionResult.Converged)
{
// Aplicar resultados a los objetos
ApplyResultsToObjects();
// Verbose output de resultados deshabilitado para mejorar rendimiento
/*
if (VerboseOutput && _stepCount % 300 == 0) // Log cada 5 segundos aprox
{
//Debug.WriteLine("=== RESULTADOS SIMULACIÓN HIDRÁULICA ===");
//Debug.WriteLine("Flujos:");
foreach (var flow in LastSolutionResult.Flows)
{
Debug.WriteLine($" {flow.Key}: {flow.Value:F6} m³/s ({flow.Value * 3600:F2} m³/h)");
}
Debug.WriteLine("Presiones:");
foreach (var pressure in LastSolutionResult.Pressures)
{
Debug.WriteLine($" {pressure.Key}: {pressure.Value:F0} Pa ({pressure.Value / 100000:F2} bar)");
}
Debug.WriteLine("=== FIN RESULTADOS ===");
}
*/
}
else
{
Debug.WriteLine($"❌ Simulación hidráulica no convergió: {LastSolutionResult.ErrorMessage}");
Debug.WriteLine($" Iteraciones: {LastSolutionResult.Iterations}, Residual: {LastSolutionResult.Residual:E6}");
Debug.WriteLine($" Tolerancia requerida: {Tolerance:E6}");
// Diagnóstico detallado deshabilitado para mejorar rendimiento
/*
if (VerboseOutput && _stepCount % 60 == 0) // Log detallado cada segundo aprox
{
Debug.WriteLine("=== DIAGNÓSTICO CONVERGENCIA ===");
Debug.WriteLine($"Nodos en red: {Network.Nodes.Count}");
Debug.WriteLine($"Ramas en red: {Network.Branches.Count}");
foreach (var node in Network.Nodes)
{
string info = node.Value.FixedP ? $"FIJA={node.Value.P:F0}Pa" : "LIBRE";
Debug.WriteLine($" Nodo {node.Key}: {info}");
}
Debug.WriteLine("=== FIN DIAGNÓSTICO ===");
}
*/
}
}
else
{
// Sin objetos hidráulicos, resultado exitoso pero vacío
LastSolutionResult = new SolutionResult(true);
}
}
catch (Exception ex)
{
LastSolutionResult = new SolutionResult(false)
{
ErrorMessage = $"Error en simulación hidráulica: {ex.Message}"
};
Debug.WriteLine($"Error en HydraulicSimulationManager.Step: {ex}");
}
finally
{
_stopwatch.Stop();
// Logging de tiempo deshabilitado para mejorar rendimiento
/*
if (VerboseOutput && _stepCount % 60 == 0) // Log cada segundo aprox
{
Debug.WriteLine($"Simulación hidráulica - Paso {_stepCount}: {_stopwatch.ElapsedMilliseconds}ms, " +
$"Objetos: {HydraulicObjects.Count}, Convergió: {LastSolutionResult.Converged}");
}
*/
}
}
/// <summary>
/// Actualiza las propiedades de los objetos antes de resolver la red
/// </summary>
private void UpdateObjectProperties()
{
// Actualizar objetos hidráulicos específicos
foreach (var simObj in HydraulicSimObjects)
{
simObj.UpdateProperties();
}
// Actualizar objetos hidráulicos generales (legacy)
foreach (var obj in HydraulicObjects)
{
// Aquí se implementará la actualización de propiedades específicas
// Por ejemplo: estado de válvulas, velocidad de bombas, etc.
UpdateObjectHydraulicProperties(obj);
}
}
/// <summary>
/// Actualiza las propiedades hidráulicas específicas de un objeto
/// </summary>
private void UpdateObjectHydraulicProperties(osBase obj)
{
// Actualizar propiedades basándose en las interfaces implementadas
if (obj is IHydraulicComponent hydraulicComponent && hydraulicComponent.HasHydraulicComponents)
{
// Llamar al método de actualización del objeto
hydraulicComponent.UpdateHydraulicProperties();
}
// Actualización específica por tipo de objeto
if (obj is IHydraulicPump pump)
{
// Verificar estado de la bomba, velocidad, etc.
UpdatePumpProperties(pump);
}
else if (obj is IHydraulicValve valve)
{
// Verificar apertura de la válvula
UpdateValveProperties(valve);
}
else if (obj is IHydraulicTank tank)
{
// Actualizar nivel y presión del tanque
UpdateTankProperties(tank);
}
}
/// <summary>
/// Actualiza propiedades específicas de bombas
/// </summary>
private void UpdatePumpProperties(IHydraulicPump pump)
{
// Aquí se pueden hacer validaciones y ajustes específicos de bombas
// Por ejemplo, limitar velocidad, verificar estado de operación, etc.
if (pump.SpeedRatio < 0.0) pump.SpeedRatio = 0.0;
if (pump.SpeedRatio > 1.0) pump.SpeedRatio = 1.0;
// Debug output deshabilitado para mejorar rendimiento
/*
if (VerboseOutput)
{
Debug.WriteLine($"Bomba {pump.GetType().Name}: Velocidad={pump.SpeedRatio:F2}, " +
$"Funcionando={pump.IsRunning}, Dirección={pump.PumpDirection}");
}
*/
}
/// <summary>
/// Actualiza propiedades específicas de válvulas
/// </summary>
private void UpdateValveProperties(IHydraulicValve valve)
{
// Validar apertura de válvula
if (valve.Opening < 0.0) valve.Opening = 0.0;
if (valve.Opening > 1.0) valve.Opening = 1.0;
// Debug output deshabilitado para mejorar rendimiento
/*
if (VerboseOutput)
{
Debug.WriteLine($"Válvula {valve.GetType().Name}: Apertura={valve.Opening:F2}, " +
$"Cerrada={valve.IsClosed}, Abierta={valve.IsFullyOpen}");
}
*/
}
/// <summary>
/// Actualiza propiedades específicas de tanques
/// </summary>
private void UpdateTankProperties(IHydraulicTank tank)
{
// Calcular presión basada en nivel si no es presión fija
if (!tank.IsFixedPressure)
{
// P = ρgh + P_atmosferica
double pressureFromLevel = SimulationFluid.Rho * 9.80665 * tank.Level; // + presión atmosférica
tank.TankPressure = pressureFromLevel;
}
// Debug output deshabilitado para mejorar rendimiento
/*
if (VerboseOutput)
{
Debug.WriteLine($"Tanque {tank.GetType().Name}: Nivel={tank.Level:F2}m, " +
$"Presión={tank.TankPressure:F0}Pa, PresionFija={tank.IsFixedPressure}");
}
*/
}
/// <summary>
/// Aplica los resultados de la simulación a los objetos
/// </summary>
private void ApplyResultsToObjects()
{
// Aplicar resultados a objetos hidráulicos específicos
foreach (var simObj in HydraulicSimObjects)
{
ApplyResultsToSimObject(simObj);
simObj.ApplySimulationResults();
}
// Aplicar resultados a objetos hidráulicos generales (legacy)
foreach (var obj in HydraulicObjects)
{
// Aplicar caudales y presiones calculados al objeto
ApplyResultsToObject(obj);
}
}
/// <summary>
/// Aplica los resultados de la simulación a un objeto específico
/// </summary>
private void ApplyResultsToSimObject(simHydraulicBase simObj)
{
// Aplicar resultados específicos según el tipo de objeto
if (simObj is simHydraulicPump pump)
{
// Buscar flujo y presión de la bomba en los resultados
string pumpBranchName = $"{pump.Nombre}_Pump";
if (LastSolutionResult.Flows.TryGetValue(pumpBranchName, out double flow))
{
pump.CurrentFlow = flow;
}
string inletNodeName = $"{pump.Nombre}_In";
if (LastSolutionResult.Pressures.TryGetValue(inletNodeName, out double pressure))
{
pump.CurrentPressure = pressure;
}
}
else if (simObj is simHydraulicTank tank)
{
// Buscar flujos de entrada y salida del tanque
// Esto requeriría mapeo de tuberías conectadas
// Por ahora, aplicar presión del tanque
string tankNodeName = $"{tank.Nombre}_Tank";
if (LastSolutionResult.Pressures.TryGetValue(tankNodeName, out double pressure))
{
tank.CurrentPressure = pressure;
}
}
else if (simObj is simHydraulicPipe pipe)
{
// Buscar flujo a través de la tubería
string pipeBranchName = $"{pipe.Nombre}_Pipe";
if (LastSolutionResult.Flows.TryGetValue(pipeBranchName, out double flow))
{
pipe.CurrentFlow = flow;
}
// Calcular pérdida de presión
string fromNode = pipe.ComponenteAId;
string toNode = pipe.ComponenteBId;
if (LastSolutionResult.Pressures.TryGetValue(fromNode, out double pressureA) &&
LastSolutionResult.Pressures.TryGetValue(toNode, out double pressureB))
{
pipe.PressureDrop = pressureA - pressureB;
}
}
}
/// <summary>
/// Aplica los resultados a un objeto específico
/// </summary>
private void ApplyResultsToObject(osBase obj)
{
// Aplicar resultados usando las interfaces hidráulicas
if (obj is IHydraulicComponent hydraulicComponent && hydraulicComponent.HasHydraulicComponents)
{
// Llamar al método de aplicación de resultados del objeto
hydraulicComponent.ApplyHydraulicResults(LastSolutionResult.Flows, LastSolutionResult.Pressures);
}
// Aplicación específica por tipo de interfaz
if (obj is IHydraulicFlowReceiver flowReceiver)
{
// Buscar caudal asociado al objeto
string branchName = FindBranchNameForObject(obj);
if (!string.IsNullOrEmpty(branchName) && LastSolutionResult.Flows.ContainsKey(branchName))
{
double flow = LastSolutionResult.Flows[branchName];
flowReceiver.SetFlow(flow);
}
}
if (obj is IHydraulicPressureReceiver pressureReceiver)
{
// Buscar presión asociada al objeto
string nodeName = FindNodeNameForObject(obj);
if (!string.IsNullOrEmpty(nodeName) && LastSolutionResult.Pressures.ContainsKey(nodeName))
{
double pressure = LastSolutionResult.Pressures[nodeName];
pressureReceiver.SetPressure(pressure);
}
}
}
/// <summary>
/// Encuentra el nombre de rama asociado a un objeto
/// </summary>
private string FindBranchNameForObject(osBase obj)
{
// Buscar en las ramas de la red
foreach (var branch in Network.Branches)
{
if (branch.Name.Contains(obj.Nombre))
{
return branch.Name;
}
}
// Fallback a nombre por defecto
return $"Branch_{obj.Nombre}";
}
/// <summary>
/// Encuentra el nombre de nodo asociado a un objeto
/// </summary>
private string FindNodeNameForObject(osBase obj)
{
// Buscar en los nodos de la red
foreach (var nodeKvp in Network.Nodes)
{
if (nodeKvp.Key.Contains(obj.Nombre))
{
return nodeKvp.Key;
}
}
// Fallback a nombre por defecto
return $"Node_{obj.Nombre}";
}
/// <summary>
/// Establece el caudal en un objeto
/// </summary>
private void SetObjectFlow(osBase obj, double flow)
{
if (obj is IHydraulicFlowReceiver flowReceiver)
{
flowReceiver.SetFlow(flow);
}
}
/// <summary>
/// Establece la presión en un objeto
/// </summary>
private void SetObjectPressure(osBase obj, double pressure)
{
if (obj is IHydraulicPressureReceiver pressureReceiver)
{
pressureReceiver.SetPressure(pressure);
}
}
#endregion
#region Public Methods
/// <summary>
/// Fuerza la reconstrucción de la red en el próximo paso
/// </summary>
public void InvalidateNetwork()
{
_networkNeedsRebuild = true;
}
/// <summary>
/// Obtiene estadísticas de la simulación
/// </summary>
public string GetSimulationStats()
{
return $"Objetos hidráulicos: {HydraulicObjects.Count}\n" +
$"Nodos: {Network.Nodes.Count}\n" +
$"Ramas: {Network.Branches.Count}\n" +
$"Última simulación convergió: {LastSolutionResult.Converged}\n" +
$"Iteraciones: {LastSolutionResult.Iterations}\n" +
$"Residual: {LastSolutionResult.Residual:E3}\n" +
$"Tiempo global: {GlobalTime:F2}s\n" +
$"Pasos: {_stepCount}";
}
/// <summary>
/// Reinicia la simulación
/// </summary>
public void Reset()
{
GlobalTime = 0.0f;
_stepCount = 0;
_networkNeedsRebuild = true;
// Reinicializar resultado
LastSolutionResult = new SolutionResult(false)
{
ErrorMessage = "Simulación reiniciada"
};
Debug.WriteLine("Simulación hidráulica reiniciada");
}
/// <summary>
/// Configura los parámetros de verificación de NPSH
/// </summary>
public void ConfigureNPSHSettings(bool enableNPSH, double npshRequired = 3.0, double vaporPressure = 2337.0, double suctionLosses = 0.5)
{
EnableNPSHVerification = enableNPSH;
// Actualizar todas las bombas existentes con los nuevos parámetros
foreach (var obj in HydraulicObjects)
{
if (obj is osHydPump pump)
{
// Los parámetros se aplicarán cuando se reconstruya la red
InvalidateNetwork();
}
}
Debug.WriteLine($"Verificación NPSH {(enableNPSH ? "habilitada" : "deshabilitada")}: NPSH_req={npshRequired}m, P_vapor={vaporPressure}Pa");
}
#endregion
#region IDisposable
private bool _disposed = false;
/// <summary>
/// Libera recursos del gestor de simulación
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
// Limpiar recursos gestionados
HydraulicObjects?.Clear();
_objectMapping?.Clear();
Network = null;
_stopwatch?.Stop();
Debug.WriteLine("HydraulicSimulationManager disposed");
}
_disposed = true;
}
}
#endregion
#region Hydraulic Object Creation
/// <summary>
/// Lista de objetos hidráulicos específicos del simulador
/// </summary>
public List<simHydraulicBase> HydraulicSimObjects { get; private set; } = new List<simHydraulicBase>();
/// <summary>
/// Crea una bomba hidráulica en la simulación
/// </summary>
public simHydraulicPump AddPump(double pumpHead, double maxFlow, double speedRatio, bool isRunning, int direction)
{
var pump = new simHydraulicPump(this)
{
PumpHead = pumpHead,
MaxFlow = maxFlow,
SpeedRatio = speedRatio,
IsRunning = isRunning,
PumpDirection = direction
};
HydraulicSimObjects.Add(pump);
_networkNeedsRebuild = true;
Debug.WriteLine($"Bomba hidráulica creada: H={pumpHead}m, Q={maxFlow}m³/s");
return pump;
}
/// <summary>
/// Crea un tanque hidráulico en la simulación
/// </summary>
public simHydraulicTank AddTank(double pressure, double currentLevel, double maxLevel, double minLevel, double crossSectionalArea, bool isFixedPressure)
{
var tank = new simHydraulicTank(this)
{
TankPressure = pressure,
CurrentLevel = currentLevel,
MaxLevel = maxLevel,
MinLevel = minLevel,
CrossSectionalArea = crossSectionalArea,
IsFixedPressure = isFixedPressure
};
HydraulicSimObjects.Add(tank);
_networkNeedsRebuild = true;
Debug.WriteLine($"Tanque hidráulico creado: P={pressure}Pa, Nivel={currentLevel}m");
return tank;
}
/// <summary>
/// Crea una tubería hidráulica en la simulación
/// </summary>
public simHydraulicPipe AddPipe(double length, double diameter, double roughness, string componenteAId, string componenteBId)
{
var pipe = new simHydraulicPipe(this)
{
Length = length,
Diameter = diameter,
Roughness = roughness,
ComponenteAId = componenteAId,
ComponenteBId = componenteBId
};
HydraulicSimObjects.Add(pipe);
_networkNeedsRebuild = true;
Debug.WriteLine($"Tubería hidráulica creada: L={length}m, D={diameter}m");
return pipe;
}
/// <summary>
/// Remueve un objeto hidráulico de la simulación
/// </summary>
public void Remove(simHydraulicBase hydraulicObject)
{
if (hydraulicObject != null && HydraulicSimObjects.Contains(hydraulicObject))
{
HydraulicSimObjects.Remove(hydraulicObject);
_networkNeedsRebuild = true;
Debug.WriteLine($"Objeto hidráulico removido: {hydraulicObject.SimObjectType}");
}
}
/// <summary>
/// Limpia todos los objetos hidráulicos específicos
/// </summary>
public void ClearHydraulicSimObjects()
{
HydraulicSimObjects.Clear();
_networkNeedsRebuild = true;
Debug.WriteLine("Todos los objetos hidráulicos específicos han sido limpiados");
}
#endregion
}
}

View File

@ -0,0 +1,301 @@
using HydraulicSimulator.Models;
using System.Collections.Generic;
namespace CtrEditor.HydraulicSimulator
{
/// <summary>
/// Interfaz base para objetos que tienen componentes hidráulicos
/// </summary>
public interface IHydraulicComponent
{
/// <summary>
/// Indica si este objeto tiene componentes hidráulicos activos
/// </summary>
bool HasHydraulicComponents { get; }
/// <summary>
/// Obtiene los nodos hidráulicos asociados a este objeto
/// </summary>
/// <returns>Lista de definiciones de nodos</returns>
List<HydraulicNodeDefinition> GetHydraulicNodes();
/// <summary>
/// Obtiene los elementos hidráulicos asociados a este objeto
/// </summary>
/// <returns>Lista de definiciones de elementos hidráulicos</returns>
List<HydraulicElementDefinition> GetHydraulicElements();
/// <summary>
/// Actualiza las propiedades hidráulicas del objeto antes de la simulación
/// </summary>
void UpdateHydraulicProperties();
/// <summary>
/// Aplica los resultados de la simulación hidráulica al objeto
/// </summary>
/// <param name="flows">Caudales calculados</param>
/// <param name="pressures">Presiones calculadas</param>
void ApplyHydraulicResults(Dictionary<string, double> flows, Dictionary<string, double> pressures);
}
/// <summary>
/// Interfaz para objetos que pueden recibir información de caudal
/// </summary>
public interface IHydraulicFlowReceiver
{
/// <summary>
/// Establece el caudal actual del objeto
/// </summary>
/// <param name="flow">Caudal en m³/s</param>
void SetFlow(double flow);
/// <summary>
/// Obtiene el caudal actual del objeto
/// </summary>
/// <returns>Caudal en m³/s</returns>
double GetFlow();
}
/// <summary>
/// Interfaz para objetos que pueden recibir información de presión
/// </summary>
public interface IHydraulicPressureReceiver
{
/// <summary>
/// Establece la presión actual del objeto
/// </summary>
/// <param name="pressure">Presión en Pa</param>
void SetPressure(double pressure);
/// <summary>
/// Obtiene la presión actual del objeto
/// </summary>
/// <returns>Presión en Pa</returns>
double GetPressure();
}
/// <summary>
/// Interfaz para objetos que son bombas hidráulicas
/// </summary>
public interface IHydraulicPump : IHydraulicComponent, IHydraulicFlowReceiver, IHydraulicPressureReceiver
{
/// <summary>
/// Cabeza de la bomba a caudal cero (m)
/// </summary>
double PumpHead { get; set; }
/// <summary>
/// Caudal máximo de la bomba (m³/s)
/// </summary>
double MaxFlow { get; set; }
/// <summary>
/// Velocidad relativa de la bomba (0.0 a 1.0)
/// </summary>
double SpeedRatio { get; set; }
/// <summary>
/// Indica si la bomba está encendida
/// </summary>
bool IsRunning { get; set; }
/// <summary>
/// Dirección de la bomba (+1 o -1)
/// </summary>
int PumpDirection { get; set; }
}
/// <summary>
/// Interfaz para objetos que son válvulas hidráulicas
/// </summary>
public interface IHydraulicValve : IHydraulicComponent, IHydraulicFlowReceiver, IHydraulicPressureReceiver
{
/// <summary>
/// Coeficiente Kv de la válvula completamente abierta (m³/h/√bar)
/// </summary>
double KvFull { get; set; }
/// <summary>
/// Apertura actual de la válvula (0.0 a 1.0)
/// </summary>
double Opening { get; set; }
/// <summary>
/// Indica si la válvula está completamente cerrada
/// </summary>
bool IsClosed { get; }
/// <summary>
/// Indica si la válvula está completamente abierta
/// </summary>
bool IsFullyOpen { get; }
}
/// <summary>
/// Interfaz para objetos que son tuberías hidráulicas
/// </summary>
public interface IHydraulicPipe : IHydraulicComponent, IHydraulicFlowReceiver, IHydraulicPressureReceiver
{
/// <summary>
/// Longitud de la tubería (m)
/// </summary>
double Length { get; set; }
/// <summary>
/// Diámetro interno de la tubería (m)
/// </summary>
double Diameter { get; set; }
/// <summary>
/// Rugosidad absoluta de la tubería (m)
/// </summary>
double Roughness { get; set; }
}
/// <summary>
/// Interfaz para objetos que son tanques hidráulicos
/// </summary>
public interface IHydraulicTank : IHydraulicComponent, IHydraulicPressureReceiver
{
/// <summary>
/// Presión del tanque (Pa)
/// </summary>
double TankPressure { get; set; }
/// <summary>
/// Nivel actual del tanque (m)
/// </summary>
double Level { get; set; }
/// <summary>
/// Área de la sección transversal del tanque (m²)
/// </summary>
double CrossSectionalArea { get; set; }
/// <summary>
/// Indica si el tanque tiene presión fija
/// </summary>
bool IsFixedPressure { get; }
}
/// <summary>
/// Definición de un nodo hidráulico
/// </summary>
public class HydraulicNodeDefinition
{
/// <summary>
/// Nombre único del nodo
/// </summary>
public string Name { get; set; }
/// <summary>
/// Indica si el nodo tiene presión fija
/// </summary>
public bool IsFixedPressure { get; set; }
/// <summary>
/// Presión del nodo (Pa), si es de presión fija
/// </summary>
public double? Pressure { get; set; }
/// <summary>
/// Descripción del nodo
/// </summary>
public string Description { get; set; } = "";
public HydraulicNodeDefinition(string name, bool isFixedPressure = false, double? pressure = null, string description = "")
{
Name = name;
IsFixedPressure = isFixedPressure;
Pressure = pressure;
Description = description;
}
}
/// <summary>
/// Definición de un elemento hidráulico
/// </summary>
public class HydraulicElementDefinition
{
/// <summary>
/// Nombre único del elemento
/// </summary>
public string Name { get; set; }
/// <summary>
/// Nodo de origen
/// </summary>
public string FromNode { get; set; }
/// <summary>
/// Nodo de destino
/// </summary>
public string ToNode { get; set; }
/// <summary>
/// Elemento hidráulico
/// </summary>
public Element Element { get; set; }
/// <summary>
/// Descripción del elemento
/// </summary>
public string Description { get; set; } = "";
public HydraulicElementDefinition(string name, string fromNode, string toNode, Element element, string description = "")
{
Name = name;
FromNode = fromNode;
ToNode = toNode;
Element = element;
Description = description;
}
}
/// <summary>
/// Tipos de elementos hidráulicos
/// </summary>
public enum HydraulicElementType
{
Pipe,
Pump,
Valve,
MinorLoss,
Tank,
Junction
}
/// <summary>
/// Configuración de un elemento hidráulico
/// </summary>
public class HydraulicElementConfiguration
{
public HydraulicElementType Type { get; set; }
public Dictionary<string, object> Parameters { get; set; } = new Dictionary<string, object>();
public HydraulicElementConfiguration(HydraulicElementType type)
{
Type = type;
}
/// <summary>
/// Establece un parámetro de configuración
/// </summary>
public void SetParameter(string key, object value)
{
Parameters[key] = value;
}
/// <summary>
/// Obtiene un parámetro de configuración
/// </summary>
public T GetParameter<T>(string key, T defaultValue = default(T))
{
if (Parameters.ContainsKey(key) && Parameters[key] is T)
{
return (T)Parameters[key];
}
return defaultValue;
}
}
}

View File

@ -68,6 +68,29 @@ namespace HydraulicSimulator.Models
Branches.Add(new Branch(n1, n2, elements, name));
}
/// <summary>
/// Actualiza las presiones en bombas con verificación de NPSH
/// </summary>
private void UpdatePumpPressures()
{
var currentPressures = new Dictionary<string, double>();
foreach (var kvp in Nodes)
{
currentPressures[kvp.Key] = kvp.Value.P;
}
foreach (var branch in Branches)
{
foreach (var element in branch.Elements)
{
if (element is PumpHQWithSuctionCheck pumpWithCheck)
{
pumpWithCheck.UpdatePressures(currentPressures);
}
}
}
}
public SolutionResult Solve(int maxIterations = 100, double tolerance = 1e-3,
double relaxationFactor = 0.1, bool verbose = false)
{
@ -102,6 +125,9 @@ namespace HydraulicSimulator.Models
// Iteración global sobre presiones nodales
for (it = 0; it < maxIterations; it++)
{
// Actualizar presiones en bombas con verificación de NPSH
UpdatePumpPressures();
// 1) con presiones actuales, resolvés q de cada rama
foreach (var b in Branches)
{
@ -150,8 +176,9 @@ namespace HydraulicSimulator.Models
}
normR = R.Length > 0 ? R.Max(Math.Abs) : 0.0;
if (verbose)
Console.WriteLine($"it {it}: |R|_inf={normR:E3}");
// Console output deshabilitado para mejorar rendimiento
// if (verbose)
// Console.WriteLine($"it {it}: |R|_inf={normR:E3}");
if (normR < tolerance)
break;
@ -294,6 +321,8 @@ namespace HydraulicSimulator.Models
/// </summary>
public void Report()
{
// Reporte deshabilitado para mejorar rendimiento
/*
Console.WriteLine("== Nodos (Pa) ==");
foreach (var kvp in Nodes)
{
@ -307,6 +336,7 @@ namespace HydraulicSimulator.Models
{
Console.WriteLine($"{b.Name,15}: {b.Q,10:E6}");
}
*/
}
}
}

View File

@ -4,6 +4,7 @@ namespace HydraulicSimulator.Models
{
/// <summary>
/// Bomba con curva H(Q)=H0*(1-(Q/Q0)²) y ley de afinidad con velocidad relativa
/// Incluye verificación de NPSH y condiciones de succión
/// </summary>
public class PumpHQ : Element
{
@ -11,6 +12,15 @@ namespace HydraulicSimulator.Models
public double Q0 { get; set; } // m³/s, caudal a cabeza cero, vel nominal
public double SpeedRel { get; set; } = 1.0; // n / n_nominal
public int Direction { get; set; } = 1; // +1 si impulsa de i->j, -1 si al revés
// Propiedades para verificación de NPSH
public double NPSHRequired { get; set; } = 3.0; // m, NPSH requerido típico
public double VaporPressure { get; set; } = 2337.0; // Pa, presión de vapor del agua a 20°C
public double SuctionLosses { get; set; } = 0.5; // m, pérdidas en la succión
// Referencias a las presiones de los nodos para verificación
public string InletNodeName { get; set; }
public string OutletNodeName { get; set; }
public PumpHQ(double h0, double q0, double speedRel = 1.0, int direction = 1)
{
@ -29,22 +39,215 @@ namespace HydraulicSimulator.Models
}
}
/// <summary>
/// Calcula el NPSH disponible basado en la presión de succión
/// </summary>
public double CalculateNPSHAvailable(double suctionPressure, Fluid fluid)
{
// NPSH disponible = (Presión absoluta de succión - Presión de vapor) / (ρ * g) - Pérdidas
var npshAvailable = (suctionPressure - VaporPressure) / (fluid.Rho * 9.80665) - SuctionLosses;
return Math.Max(0, npshAvailable); // No puede ser negativo
}
/// <summary>
/// Verifica si la bomba puede operar sin cavitación
/// </summary>
public bool CanOperateWithoutCavitation(double suctionPressure, Fluid fluid)
{
var npshAvailable = CalculateNPSHAvailable(suctionPressure, fluid);
return npshAvailable >= NPSHRequired;
}
/// <summary>
/// Calcula el factor de reducción por cavitación (0 = cavitación total, 1 = sin cavitación)
/// </summary>
public double GetCavitationFactor(double suctionPressure, Fluid fluid)
{
var npshAvailable = CalculateNPSHAvailable(suctionPressure, fluid);
var ratio = npshAvailable / NPSHRequired;
if (ratio >= 1.0) return 1.0; // Sin cavitación
if (ratio <= 0.1) return 0.0; // Cavitación severa
// Transición suave entre 0.1 y 1.0
return Math.Pow(ratio, 2); // Curva cuadrática para transición suave
}
/// <summary>
/// Verifica si la bomba puede superar la presión de descarga
/// </summary>
public bool CanOvercomeDischargePressure(double suctionPressure, double dischargePressure, Fluid fluid)
{
var (h0s, _) = Scaled;
var maxPressureRise = h0s * fluid.Rho * 9.80665; // Máxima presión que puede agregar la bomba
var requiredPressureRise = dischargePressure - suctionPressure;
return maxPressureRise >= requiredPressureRise;
}
public override double Dp(double q, Fluid fluid)
{
var (h0s, q0s) = Scaled;
// Si la velocidad es muy baja o la bomba está apagada, no genera presión
if (SpeedRel < 0.01)
return 0.0;
// Limitamos fuera de rango para estabilidad
var qq = Math.Max(-q0s * 0.999, Math.Min(q0s * 0.999, q));
var h = h0s * (1.0 - Math.Pow(qq / q0s, 2));
// Calcular presión diferencial base
var dpBase = -Direction * fluid.Rho * 9.80665 * h;
// Aplicar factor de cavitación si tenemos información de presión de succión
// Nota: Esto requiere que el simulador pase las presiones de los nodos
// Por ahora, asumimos operación normal, pero el factor se aplicará en el simulador
return dpBase;
}
/// <summary>
/// Versión mejorada de Dp que considera presiones de succión y descarga
/// </summary>
public double DpWithSuctionCheck(double q, Fluid fluid, double suctionPressure, double dischargePressure)
{
var (h0s, q0s) = Scaled;
// Si la velocidad es muy baja o la bomba está apagada, no genera presión
if (SpeedRel < 0.01)
return 0.0;
// Verificar si puede superar la presión de descarga
if (!CanOvercomeDischargePressure(suctionPressure, dischargePressure, fluid))
{
// La bomba no puede vencer la presión de descarga, flujo cero o negativo
return 0.0;
}
// Verificar NPSH y aplicar factor de cavitación
var cavitationFactor = GetCavitationFactor(suctionPressure, fluid);
if (cavitationFactor < 0.1)
{
// Cavitación severa, bomba no puede operar efectivamente
return 0.0;
}
// Limitamos fuera de rango para estabilidad
var qq = Math.Max(-q0s * 0.999, Math.Min(q0s * 0.999, q));
var h = h0s * (1.0 - Math.Pow(qq / q0s, 2));
// Aplicar factor de cavitación
h *= cavitationFactor;
var dp = -Direction * fluid.Rho * 9.80665 * h;
// dp es negativo si la bomba agrega presión en el sentido de la rama
return dp;
}
public override double DdpDq(double q, Fluid fluid)
{
var (h0s, q0s) = Scaled;
// Si la velocidad es muy baja, derivada es cero
if (SpeedRel < 0.01)
return 1e-12;
var dhDq = -2.0 * h0s * q / (q0s * q0s);
return -Direction * fluid.Rho * 9.80665 * dhDq + 1e-12;
}
/// <summary>
/// Versión mejorada de DdpDq que considera cavitación
/// </summary>
public double DdpDqWithSuctionCheck(double q, Fluid fluid, double suctionPressure, double dischargePressure)
{
var (h0s, q0s) = Scaled;
// Si la velocidad es muy baja, derivada es cero
if (SpeedRel < 0.01)
return 1e-12;
// Verificar condiciones de operación
if (!CanOvercomeDischargePressure(suctionPressure, dischargePressure, fluid))
return 1e-12;
var cavitationFactor = GetCavitationFactor(suctionPressure, fluid);
if (cavitationFactor < 0.1)
return 1e-12;
var dhDq = -2.0 * h0s * q / (q0s * q0s);
dhDq *= cavitationFactor; // Aplicar factor de cavitación
return -Direction * fluid.Rho * 9.80665 * dhDq + 1e-12;
}
}
/// <summary>
/// Extensión de PumpHQ que considera verificaciones de NPSH durante la simulación
/// </summary>
public class PumpHQWithSuctionCheck : PumpHQ
{
private readonly Dictionary<string, double> _pressures;
private bool _npshCheckEnabled;
public PumpHQWithSuctionCheck(double h0, double q0, double speedRel = 1.0, int direction = 1,
Dictionary<string, double> pressures = null, bool enableNpshCheck = true)
: base(h0, q0, speedRel, direction)
{
_pressures = pressures ?? new Dictionary<string, double>();
_npshCheckEnabled = enableNpshCheck;
}
public void UpdatePressures(Dictionary<string, double> pressures)
{
_pressures.Clear();
if (pressures != null)
{
foreach (var kvp in pressures)
{
_pressures[kvp.Key] = kvp.Value;
}
}
}
public override double Dp(double q, Fluid fluid)
{
// Si no hay verificación de NPSH habilitada, usar el comportamiento base
if (!_npshCheckEnabled || _pressures == null || string.IsNullOrEmpty(InletNodeName) || string.IsNullOrEmpty(OutletNodeName))
{
return base.Dp(q, fluid);
}
// Obtener presiones de succión y descarga
if (_pressures.TryGetValue(InletNodeName, out double suctionPressure) &&
_pressures.TryGetValue(OutletNodeName, out double dischargePressure))
{
return DpWithSuctionCheck(q, fluid, suctionPressure, dischargePressure);
}
// Si no tenemos presiones, usar comportamiento base
return base.Dp(q, fluid);
}
public override double DdpDq(double q, Fluid fluid)
{
// Si no hay verificación de NPSH habilitada, usar el comportamiento base
if (!_npshCheckEnabled || _pressures == null || string.IsNullOrEmpty(InletNodeName) || string.IsNullOrEmpty(OutletNodeName))
{
return base.DdpDq(q, fluid);
}
// Obtener presiones de succión y descarga
if (_pressures.TryGetValue(InletNodeName, out double suctionPressure) &&
_pressures.TryGetValue(OutletNodeName, out double dischargePressure))
{
return DdpDqWithSuctionCheck(q, fluid, suctionPressure, dischargePressure);
}
// Si no tenemos presiones, usar comportamiento base
return base.DdpDq(q, fluid);
}
}
}

View File

@ -0,0 +1,179 @@
using System.Numerics;
using System.ComponentModel;
using CtrEditor.ObjetosSim;
namespace CtrEditor.HydraulicSimulator
{
/// <summary>
/// Clase base para objetos hidráulicos en la simulación
/// Similar a simBase pero para el sistema hidráulico
/// </summary>
public class simHydraulicBase
{
public string SimObjectType { get; set; } = "";
public osBase WpfObject { get; set; }
public string Nombre { get; set; }
public bool Descartar { get; set; } = false;
protected HydraulicSimulationManager _hydraulicManager;
public simHydraulicBase(HydraulicSimulationManager hydraulicManager)
{
_hydraulicManager = hydraulicManager;
}
/// <summary>
/// Método virtual para actualizar propiedades específicas del objeto
/// </summary>
public virtual void UpdateProperties()
{
// Implementación base - puede ser sobrescrita
}
/// <summary>
/// Método virtual para aplicar resultados de la simulación al objeto WPF
/// </summary>
public virtual void ApplySimulationResults()
{
// Implementación base - puede ser sobrescrita
}
}
/// <summary>
/// Representa una bomba hidráulica en la simulación
/// </summary>
public class simHydraulicPump : simHydraulicBase
{
public double PumpHead { get; set; }
public double MaxFlow { get; set; }
public double SpeedRatio { get; set; }
public bool IsRunning { get; set; }
public int PumpDirection { get; set; }
// Resultados de la simulación
public double CurrentFlow { get; set; }
public double CurrentPressure { get; set; }
public simHydraulicPump(HydraulicSimulationManager hydraulicManager) : base(hydraulicManager)
{
SimObjectType = "HydraulicPump";
}
public override void UpdateProperties()
{
// Actualizar propiedades específicas de la bomba
if (WpfObject is IHydraulicPump pump)
{
PumpHead = pump.PumpHead;
MaxFlow = pump.MaxFlow;
SpeedRatio = pump.SpeedRatio;
IsRunning = pump.IsRunning;
PumpDirection = pump.PumpDirection;
}
}
public override void ApplySimulationResults()
{
// Aplicar resultados al objeto WPF
if (WpfObject is IHydraulicPump pump)
{
pump.SetFlow(CurrentFlow);
pump.SetPressure(CurrentPressure);
}
}
}
/// <summary>
/// Representa un tanque hidráulico en la simulación
/// </summary>
public class simHydraulicTank : simHydraulicBase
{
public double TankPressure { get; set; }
public double CurrentLevel { get; set; }
public double MaxLevel { get; set; }
public double MinLevel { get; set; }
public double CrossSectionalArea { get; set; }
public bool IsFixedPressure { get; set; }
// Resultados de la simulación
public double InletFlow { get; set; }
public double OutletFlow { get; set; }
public double CurrentPressure { get; set; }
public simHydraulicTank(HydraulicSimulationManager hydraulicManager) : base(hydraulicManager)
{
SimObjectType = "HydraulicTank";
}
public override void UpdateProperties()
{
// Actualizar propiedades específicas del tanque
if (WpfObject is osHydTank tank)
{
TankPressure = tank.TankPressure;
CurrentLevel = tank.CurrentLevel;
MaxLevel = tank.MaxLevel;
MinLevel = tank.MinLevel;
CrossSectionalArea = tank.CrossSectionalArea;
IsFixedPressure = tank.IsFixedPressure;
}
}
public override void ApplySimulationResults()
{
// Aplicar resultados al objeto WPF
if (WpfObject is IHydraulicFlowReceiver flowReceiver)
{
flowReceiver.SetFlow(InletFlow - OutletFlow); // Flujo neto
}
if (WpfObject is IHydraulicPressureReceiver pressureReceiver)
{
pressureReceiver.SetPressure(CurrentPressure);
}
}
}
/// <summary>
/// Representa una tubería hidráulica en la simulación
/// </summary>
public class simHydraulicPipe : simHydraulicBase
{
public double Length { get; set; }
public double Diameter { get; set; }
public double Roughness { get; set; }
public string ComponenteAId { get; set; }
public string ComponenteBId { get; set; }
// Resultados de la simulación
public double CurrentFlow { get; set; }
public double PressureDrop { get; set; }
public simHydraulicPipe(HydraulicSimulationManager hydraulicManager) : base(hydraulicManager)
{
SimObjectType = "HydraulicPipe";
}
public override void UpdateProperties()
{
// Actualizar propiedades específicas de la tubería
if (WpfObject is IHydraulicPipe pipe)
{
Length = pipe.Length;
Diameter = pipe.Diameter;
Roughness = pipe.Roughness;
// Obtener IDs de componentes conectados (esto se manejará en el manager)
}
}
public override void ApplySimulationResults()
{
// Aplicar resultados al objeto WPF
if (WpfObject is IHydraulicPipe pipe)
{
pipe.SetFlow(CurrentFlow);
pipe.SetPressure(PressureDrop); // En tuberías, "presión" es la pérdida de presión
}
}
}
}

View File

@ -11,6 +11,7 @@ using System.IO;
using Newtonsoft.Json;
using System.Windows;
using CtrEditor.Simulacion;
using CtrEditor.HydraulicSimulator; // Nuevo using para el simulador hidráulico
using System.Diagnostics;
using System.Reflection;
using System.Linq;
@ -58,6 +59,7 @@ namespace CtrEditor
private bool Debug_SimulacionCreado = false;
public SimulationManagerBEPU simulationManager = new SimulationManagerBEPU();
public HydraulicSimulationManager hydraulicSimulationManager = new HydraulicSimulationManager();
private readonly System.Timers.Timer _timerSimulacion; // Cambiado a System.Timers.Timer para mejor precisión
private readonly System.Timers.Timer _timerPLCUpdate; // Cambiado a System.Timers.Timer para mejor precisión
@ -737,6 +739,9 @@ namespace CtrEditor
{
_stateSerializer.LoadState(SelectedImage);
// Registrar objetos hidráulicos cargados
RegisterLoadedHydraulicObjects();
// Aplicar los filtros actuales a los objetos recién cargados
if (MainWindow?.VisFilter?.FilterViewModel != null)
{
@ -787,6 +792,10 @@ namespace CtrEditor
{
// Añadir el nuevo osBase a la colección de objetos simulables
ObjetosSimulables.Add(NuevoOsBase);
// Registrar en el simulador hidráulico si tiene componentes hidráulicos
RegisterHydraulicObjectIfNeeded(NuevoOsBase);
HasUnsavedChanges = true;
}
}
@ -804,7 +813,7 @@ namespace CtrEditor
if (userControl != null)
{
// Asignar los datos al UserControl
UserControlFactory.AssignDatos(userControl, osObjeto, simulationManager);
UserControlFactory.AssignDatos(userControl, osObjeto, simulationManager, hydraulicSimulationManager);
osObjeto._mainViewModel = this;
if (osObjeto.Id == null) // Para los objetos salvados antes de usar UniqueID
osObjeto.Id = new UniqueId().ObtenerNuevaID();
@ -820,6 +829,9 @@ namespace CtrEditor
{
if (osObjeto != null && ObjetosSimulables.Contains(osObjeto))
{
// Desregistrar del simulador hidráulico si estaba registrado
UnregisterHydraulicObjectIfNeeded(osObjeto);
ObjetosSimulables.Remove(osObjeto);
if (osObjeto.VisualRepresentation != null)
MainWindow.EliminarUserControlDelCanvas(osObjeto.VisualRepresentation);
@ -925,6 +937,10 @@ namespace CtrEditor
NuevoObjetoDuplicado.Top += OffsetY;
ObjetosSimulables.Add(NuevoObjetoDuplicado);
CrearUserControlDesdeObjetoSimulable(NuevoObjetoDuplicado);
// Registrar en el simulador hidráulico si tiene componentes hidráulicos
RegisterHydraulicObjectIfNeeded(NuevoObjetoDuplicado);
HasUnsavedChanges = true;
}
}
@ -1113,6 +1129,9 @@ namespace CtrEditor
foreach (var objetoSimulable in ObjetosSimulables)
objetoSimulable.SimulationStop();
// Reiniciar simulador hidráulico al detener la simulación
ResetHydraulicSimulation();
if (Debug_SimulacionCreado)
{
Debug_SimulacionCreado = false;
@ -1158,7 +1177,11 @@ namespace CtrEditor
objetoSimulable.UpdateGeometryStep();
}
// Ejecutar simulación física BEPU
simulationManager.Step();
// Ejecutar simulación hidráulica
hydraulicSimulationManager.Step((float)(timeBetweenCalls / 1000.0)); // Convertir ms a segundos
// ✅ NUEVO: Solo crear la copia si hay objetos para procesar
if (ObjetosSimulables?.Count > 0)
@ -1642,6 +1665,97 @@ namespace CtrEditor
}
}
#region Hydraulic Simulation Management
/// <summary>
/// Registra un objeto en el simulador hidráulico si tiene componentes hidráulicos
/// </summary>
private void RegisterHydraulicObjectIfNeeded(osBase obj)
{
if (obj is IHydraulicComponent hydraulicComponent && hydraulicComponent.HasHydraulicComponents)
{
// Evitar doble registro - solo registrar si no está ya registrado
if (!hydraulicSimulationManager.HydraulicObjects.Contains(obj))
{
hydraulicSimulationManager.RegisterHydraulicObject(obj);
Debug.WriteLine($"Objeto hidráulico registrado: {obj.Nombre}");
}
else
{
Debug.WriteLine($"Objeto hidráulico ya registrado (omitido): {obj.Nombre}");
}
}
}
/// <summary>
/// Desregistra un objeto del simulador hidráulico si estaba registrado
/// </summary>
private void UnregisterHydraulicObjectIfNeeded(osBase obj)
{
// Desregistrar independientemente de si implementa la interfaz actualmente
// (podría haber cambiado desde que se registró)
hydraulicSimulationManager.UnregisterHydraulicObject(obj);
Debug.WriteLine($"Objeto desregistrado del simulador hidráulico: {obj.Nombre}");
}
/// <summary>
/// Reinicia el simulador hidráulico
/// </summary>
public void ResetHydraulicSimulation()
{
hydraulicSimulationManager.Reset();
Debug.WriteLine("Simulador hidráulico reiniciado");
}
/// <summary>
/// Obtiene estadísticas del simulador hidráulico
/// </summary>
public string GetHydraulicSimulationStats()
{
return hydraulicSimulationManager.GetSimulationStats();
}
/// <summary>
/// Habilita/deshabilita la simulación hidráulica
/// </summary>
public void SetHydraulicSimulationEnabled(bool enabled)
{
hydraulicSimulationManager.IsHydraulicSimulationEnabled = enabled;
Debug.WriteLine($"Simulación hidráulica {(enabled ? "habilitada" : "deshabilitada")}");
}
/// <summary>
/// Registra todos los objetos hidráulicos existentes después de cargar un proyecto
/// </summary>
private void RegisterLoadedHydraulicObjects()
{
// Limpiar registros previos
hydraulicSimulationManager.ClearHydraulicObjects();
// Crear una lista temporal para evitar duplicados durante la carga
var objectsToRegister = new HashSet<osBase>();
// Registrar todos los objetos cargados que tengan componentes hidráulicos
foreach (var obj in ObjetosSimulables)
{
if (obj is IHydraulicComponent hydraulicComponent && hydraulicComponent.HasHydraulicComponents)
{
objectsToRegister.Add(obj);
}
}
// Registrar los objetos únicos
foreach (var obj in objectsToRegister)
{
hydraulicSimulationManager.RegisterHydraulicObject(obj);
Debug.WriteLine($"Objeto hidráulico registrado en carga: {obj.Nombre}");
}
Debug.WriteLine($"Registrados {hydraulicSimulationManager.HydraulicObjects.Count} objetos hidráulicos únicos tras cargar proyecto");
}
#endregion
}
public class SimulationData

View File

@ -134,7 +134,7 @@
<RowDefinition Height="Auto" />
<RowDefinition Height="3*" />
<RowDefinition Height="5" />
<RowDefinition Height="2*" />
<RowDefinition Height="0" />
</Grid.RowDefinitions>
<ToolBarTray Grid.Row="0">

View File

@ -1,5 +1,6 @@
using CtrEditor.ObjetosSim;
using CtrEditor.Simulacion;
using CtrEditor.HydraulicSimulator;
using System.Diagnostics;
using System.Globalization;
using System.Windows;
@ -971,7 +972,37 @@ namespace CtrEditor
try
{
var serializedData = JsonConvert.SerializeObject(original, settings);
Debug.WriteLine($"CreateCopyUsingExistingLogic - Serialized data: {serializedData}");
copy = JsonConvert.DeserializeObject<osBase>(serializedData, settings);
if (copy == null)
{
Debug.WriteLine($"CreateCopyUsingExistingLogic - Deserialización devolvió null para tipo: {original.GetType().Name}");
}
else
{
Debug.WriteLine($"CreateCopyUsingExistingLogic - Copia exitosa de tipo: {copy.GetType().Name}");
}
}
catch (JsonSerializationException jsonEx)
{
Debug.WriteLine($"CreateCopyUsingExistingLogic - Error de serialización JSON: {jsonEx.Message}");
Debug.WriteLine($"CreateCopyUsingExistingLogic - StackTrace: {jsonEx.StackTrace}");
if (jsonEx.InnerException != null)
{
Debug.WriteLine($"CreateCopyUsingExistingLogic - Inner Exception: {jsonEx.InnerException.Message}");
}
}
catch (Exception ex)
{
Debug.WriteLine($"CreateCopyUsingExistingLogic - Error general: {ex.Message}");
Debug.WriteLine($"CreateCopyUsingExistingLogic - Tipo de excepción: {ex.GetType().Name}");
Debug.WriteLine($"CreateCopyUsingExistingLogic - StackTrace: {ex.StackTrace}");
if (ex.InnerException != null)
{
Debug.WriteLine($"CreateCopyUsingExistingLogic - Inner Exception: {ex.InnerException.Message}");
}
}
finally
{
@ -1220,5 +1251,6 @@ namespace CtrEditor
else
return new ValidationResult(false, "Ingrese un número válido.");
}
}
}

12
NPSHTestConsole.csproj Normal file
View File

@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0-windows8.0</TargetFramework>
<Nullable>enable</Nullable>
<UseWPF>true</UseWPF>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="CtrEditor.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,501 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
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
{
// Referencia al objeto de simulación hidráulica específico
private simHydraulicPipe SimHydraulicPipe;
// 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);
}
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)
{
// 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);
if (!string.IsNullOrEmpty(fromNodeName) && !string.IsNullOrEmpty(toNodeName))
{
string branchKey = $"{fromNodeName}->{toNodeName}";
if (flows.TryGetValue(branchKey, out double flow))
{
CurrentFlow = flow;
}
// 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()
{
// Se llama cuando inicia la simulación - crear objeto hidráulico si no existe
if (SimHydraulicPipe == null && !string.IsNullOrEmpty(Id_ComponenteA) && !string.IsNullOrEmpty(Id_ComponenteB))
{
SimHydraulicPipe = hydraulicSimulationManager.AddPipe(Length, Diameter, Roughness, Id_ComponenteA, Id_ComponenteB);
SimHydraulicPipe.SimObjectType = "HydraulicPipe";
SimHydraulicPipe.WpfObject = this;
SimHydraulicPipe.Nombre = Nombre;
}
else if (SimHydraulicPipe != null)
{
// Actualizar propiedades si el objeto ya existe
SimHydraulicPipe.Length = Length;
SimHydraulicPipe.Diameter = Diameter;
SimHydraulicPipe.Roughness = Roughness;
SimHydraulicPipe.ComponenteAId = Id_ComponenteA;
SimHydraulicPipe.ComponenteBId = Id_ComponenteB;
}
ActualizarGeometrias();
}
public override void UpdateGeometryStep()
{
// Los objetos hidráulicos actualizan sus resultados
// a través de ApplySimulationResults() desde HydraulicSimulationManager
}
public override void UpdateControl(int elapsedMilliseconds)
{
// Actualizar propiedades desde la simulación hidráulica
if (SimHydraulicPipe != null)
{
CurrentFlow = SimHydraulicPipe.CurrentFlow;
PressureDrop = SimHydraulicPipe.PressureDrop;
}
}
public override void ucLoaded()
{
// Objeto hidráulico se crea en UpdateGeometryStart cuando inicia la simulación
base.ucLoaded();
}
public override void ucUnLoaded()
{
// Remover objeto hidráulico de la simulación
if (SimHydraulicPipe != null)
{
hydraulicSimulationManager.Remove(SimHydraulicPipe);
SimHydraulicPipe = null;
}
}
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
}
}
}

View File

@ -0,0 +1,613 @@
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
{
/// <summary>
/// Bomba hidráulica que implementa las interfaces del simulador hidráulico
/// </summary>
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;
}
}
// 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
}
public override void UpdateControl(int elapsedMilliseconds)
{
// Actualizar propiedades desde la simulación hidráulica
if (SimHydraulicPump != null)
{
CurrentFlow = SimHydraulicPump.CurrentFlow;
CurrentPressure = SimHydraulicPump.CurrentPressure;
// Actualizar el color según el estado
if (IsRunning)
{
if (hydraulicSimulationManager.EnableNPSHVerification)
{
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 if (!CanOperateWithoutCavitation)
ColorButton_oculto = Brushes.Yellow; // Condiciones límite
else
ColorButton_oculto = Brushes.Green; // Funcionamiento normal
}
else
{
ColorButton_oculto = Brushes.Green; // Sin verificación NPSH
}
}
else
{
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<HydraulicNodeDefinition> GetHydraulicNodes()
{
var nodes = new List<HydraulicNodeDefinition>();
// 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<HydraulicElementDefinition> GetHydraulicElements()
{
var elements = new List<HydraulicElementDefinition>();
// 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;
// Verificar condiciones de NPSH si está habilitada la verificación
if (hydraulicSimulationManager.EnableNPSHVerification && IsRunning)
{
var npshAvailable = NPSHAvailable;
var cavitationFactor = CavitationFactor;
if (cavitationFactor < 0.5)
{
Debug.WriteLine($"⚠️ Bomba {Nombre}: Riesgo de cavitación - Factor={cavitationFactor:F2}, NPSH_disponible={npshAvailable:F2}m");
}
if (!CanOperateWithoutCavitation)
{
Debug.WriteLine($"❌ Bomba {Nombre}: NO puede operar sin cavitación - NPSH insuficiente");
}
}
//Debug.WriteLine($"Bomba {Nombre}: Velocidad={SpeedRatio:F2}, Funcionando={IsRunning}");
}
public void ApplyHydraulicResults(Dictionary<string, double> flows, Dictionary<string, double> 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
/// <summary>
/// Verifica si la bomba tiene componentes conectados a través de tuberías
/// </summary>
private bool HasConnectedComponents()
{
if (_mainViewModel == null) return false;
// Buscar tuberías que conecten esta bomba con otros componentes
var connectedPipes = _mainViewModel.ObjetosSimulables
.OfType<osHydPipe>()
.Where(pipe => pipe.Id_ComponenteA == Nombre || pipe.Id_ComponenteB == Nombre)
.ToList();
return connectedPipes.Any();
}
/// <summary>
/// Obtiene los nombres de nodos correctos para la bomba basándose en las conexiones
/// </summary>
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<osHydPipe>()
.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();
}
}
/// <summary>
/// Proveedor de items para la dirección de la bomba
/// </summary>
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;
}
}
}

View File

@ -0,0 +1,986 @@
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>
/// Tanque hidráulico con gestión dinámica de nivel y presión configurable
/// </summary>
public partial class osHydTank : osBase, IHydraulicComponent, IHydraulicFlowReceiver, IHydraulicPressureReceiver, IosBase
{
// Referencia al objeto de simulación hidráulica específico
private simHydraulicTank SimHydraulicTank;
public static string NombreCategoria() => "Componentes Hidráulicos";
public static string NombreClase() => "Tanque Hidráulico";
// Private Fields
private double _tankPressure = 101325.0; // Pa (1 atm por defecto)
private double _currentLevel = 1.0; // m
private double _maxLevel = 2.0; // m
private double _minLevel = 0.1; // m
private double _crossSectionalArea = 1.0; // m²
private double _currentVolume = 1.0; // m³
private double _maxVolume = 2.0; // m³
private double _inletFlow = 0.0; // m³/s
private double _outletFlow = 0.0; // m³/s
private double _currentPressure = 101325.0; // Pa
private bool _isFixedPressure = true;
private HydraulicTankType _tankType = HydraulicTankType.Intermediate;
private string _connectedInletPipe = "";
private string _connectedOutletPipe = "";
private double _lastUpdateTime = 0.0;
[JsonIgnore]
private object _levelLock = new object();
private PropertyChangedEventHandler? inletPropertyChangedHandler;
private PropertyChangedEventHandler? outletPropertyChangedHandler;
// Enums
public enum HydraulicTankType
{
[Description("Tanque de succión (inicio del sistema)")]
Suction,
[Description("Tanque intermedio (buffer)")]
Intermediate,
[Description("Tanque de almacenamiento")]
Storage,
[Description("Tanque de proceso")]
Process
}
// Properties
[ObservableProperty]
[property: Category("🎨 Apariencia")]
[property: Description("Tamaño visual del tanque")]
[property: Name("Tamaño")]
public float tamano;
// Propiedades visuales específicas del tanque
[ObservableProperty]
[property: Category("🎨 Apariencia")]
[property: Description("Color visual del tanque")]
[property: Name("Color")]
private Brush colorButton_oculto = Brushes.LightBlue;
// Constructor
public osHydTank()
{
// Inicializar el lock primero para thread safety
EnsureLockInitialized();
try
{
Nombre = "Tanque Hidráulico";
Tamano = 1.0f; // Usar un tamaño mayor para mayor visibilidad
// Inicializar dimensiones usando las propiedades de osBase
Ancho = 0.30f;
Alto = 0.40f;
Angulo = 0f;
// Asegurar que el movimiento esté habilitado
Lock_movement = false;
// No cargar imagen aquí - se carga en ucLoaded()
// para evitar problemas de serialización
// Cálculos seguros
SafeUpdateVolumeCalculations();
IsVisFilter = true; // Asegurar que el componente hidráulico sea visible en filtros
SafeUpdateTankPressure();
// Debug: Confirmar que el constructor se ejecuta
Debug.WriteLine($"osHydTank Constructor: Nombre='{Nombre}', Tamaño={Tamano}, ZIndex={zIndex_fromFrames}, IsVisFilter={IsVisFilter}, Lock_movement={Lock_movement}");
}
catch (Exception ex)
{
Debug.WriteLine($"Error in osHydTank constructor: {ex.Message}");
// Inicializar valores por defecto mínimos
InitializeDefaults();
}
}
// 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 override void OnMove(float LeftPixels, float TopPixels)
{
// Método básico de movimiento como otros objetos
// Debug para verificar que se llama
Debug.WriteLine($"osHydTank.OnMove: LeftPixels={LeftPixels}, TopPixels={TopPixels}, Left={Left}, Top={Top}");
}
public override void OnResize(float Delta_Width, float Delta_Height)
{
Tamano += Delta_Width + Delta_Height;
}
public override void UpdateGeometryStart()
{
// Se llama cuando inicia la simulación - crear objeto hidráulico si no existe
if (SimHydraulicTank == null)
{
SimHydraulicTank = hydraulicSimulationManager.AddTank(TankPressure, CurrentLevel, MaxLevel, MinLevel, CrossSectionalArea, IsFixedPressure);
SimHydraulicTank.SimObjectType = "HydraulicTank";
SimHydraulicTank.WpfObject = this;
SimHydraulicTank.Nombre = Nombre;
}
else
{
// Actualizar propiedades si el objeto ya existe
SimHydraulicTank.TankPressure = TankPressure;
SimHydraulicTank.CurrentLevel = CurrentLevel;
SimHydraulicTank.MaxLevel = MaxLevel;
SimHydraulicTank.MinLevel = MinLevel;
SimHydraulicTank.CrossSectionalArea = CrossSectionalArea;
SimHydraulicTank.IsFixedPressure = IsFixedPressure;
}
}
public override void UpdateGeometryStep()
{
// Los objetos hidráulicos actualizan sus resultados
// a través de ApplySimulationResults() desde HydraulicSimulationManager
}
public override void UpdateControl(int elapsedMilliseconds)
{
// Actualizar propiedades desde la simulación hidráulica
if (SimHydraulicTank != null)
{
InletFlow = SimHydraulicTank.InletFlow;
OutletFlow = SimHydraulicTank.OutletFlow;
CurrentPressure = SimHydraulicTank.CurrentPressure;
// Actualizar nivel del tanque basado en el balance de flujo
double deltaTime = elapsedMilliseconds / 1000.0; // Convertir a segundos
if (deltaTime > 0)
{
UpdateLevelFromFlowBalance(deltaTime);
}
}
// Actualizar el color según el estado del tanque
var percentage = FillPercentage;
if (percentage < 20)
ColorButton_oculto = Brushes.Red;
else if (percentage < 50)
ColorButton_oculto = Brushes.Orange;
else if (percentage < 80)
ColorButton_oculto = Brushes.Yellow;
else
ColorButton_oculto = Brushes.LightBlue;
}
// Properties - Configuration
[Category("🏗️ Configuración del Tanque")]
[DisplayName("Tipo de tanque")]
[Description("Función del tanque en el sistema hidráulico")]
public HydraulicTankType TankType
{
get => _tankType;
set
{
if (SetProperty(ref _tankType, value))
{
UpdateTankConfiguration();
InvalidateHydraulicNetwork();
}
}
}
[Category("🏗️ Configuración del Tanque")]
[DisplayName("Área sección transversal")]
[Description("Área de la sección transversal del tanque (m²)")]
public double CrossSectionalArea
{
get => _crossSectionalArea;
set
{
if (SetProperty(ref _crossSectionalArea, Math.Max(0.1, value)))
{
SafeUpdateVolumeCalculations();
InvalidateHydraulicNetwork();
}
}
}
[Category("🏗️ Configuración del Tanque")]
[DisplayName("Nivel máximo")]
[Description("Nivel máximo permitido en el tanque (m)")]
public double MaxLevel
{
get => _maxLevel;
set
{
if (SetProperty(ref _maxLevel, Math.Max(0.2, value)))
{
if (_currentLevel > _maxLevel)
CurrentLevel = _maxLevel;
SafeUpdateVolumeCalculations();
}
}
}
[Category("🏗️ Configuración del Tanque")]
[DisplayName("Nivel mínimo")]
[Description("Nivel mínimo permitido en el tanque (m)")]
public double MinLevel
{
get => _minLevel;
set
{
if (SetProperty(ref _minLevel, Math.Max(0.0, Math.Min(value, _maxLevel - 0.1))))
{
if (_currentLevel < _minLevel)
CurrentLevel = _minLevel;
SafeUpdateVolumeCalculations();
}
}
}
// Properties - Pressure
[Category("🔧 Sistema de Presión")]
[DisplayName("Presión fija")]
[Description("Si está habilitado, la presión se mantiene constante")]
public bool IsFixedPressure
{
get => _isFixedPressure;
set
{
if (SetProperty(ref _isFixedPressure, value))
{
SafeUpdateTankPressure();
InvalidateHydraulicNetwork();
}
}
}
[Category("🔧 Sistema de Presión")]
[DisplayName("Presión del tanque")]
[Description("Presión mantenida en el tanque (Pa). En sistemas reales controlada por PID")]
public double TankPressure
{
get => _tankPressure;
set
{
if (SetProperty(ref _tankPressure, Math.Max(0, value)))
{
SafeUpdateTankPressure();
InvalidateHydraulicNetwork();
}
}
}
[Category("🔧 Sistema de Presión")]
[DisplayName("Presión (bar)")]
[Description("Presión del tanque en bar")]
[JsonIgnore]
public double TankPressureBar
{
get => TankPressure / 100000.0;
set => TankPressure = value * 100000.0;
}
// Properties - Current State
[Category("📊 Estado Actual")]
[DisplayName("Nivel actual")]
[Description("Nivel actual de líquido en el tanque (m)")]
[JsonIgnore]
public double CurrentLevel
{
get => _currentLevel;
private set
{
EnsureLockInitialized();
lock (_levelLock)
{
var clampedLevel = Math.Max(_minLevel, Math.Min(_maxLevel, value));
if (SetProperty(ref _currentLevel, clampedLevel))
{
SafeUpdateVolumeCalculations();
OnPropertyChanged(nameof(FillPercentage));
}
}
}
}
[Category("📊 Estado Actual")]
[DisplayName("Volumen actual")]
[Description("Volumen actual de líquido en el tanque (m³)")]
[JsonIgnore]
public double CurrentVolume
{
get => _currentVolume;
private set => SetProperty(ref _currentVolume, value);
}
[Category("📊 Estado Actual")]
[DisplayName("Volumen máximo")]
[Description("Volumen máximo del tanque (m³)")]
[JsonIgnore]
public double MaxVolume
{
get => _maxVolume;
private set => SetProperty(ref _maxVolume, value);
}
[Category("📊 Estado Actual")]
[DisplayName("Porcentaje llenado")]
[Description("Porcentaje de llenado del tanque")]
[JsonIgnore]
public double FillPercentage => (_currentLevel - _minLevel) / (_maxLevel - _minLevel) * 100.0;
[Category("📊 Estado Actual")]
[DisplayName("Flujo entrada")]
[Description("Flujo de entrada actual (m³/s)")]
[JsonIgnore]
public double InletFlow
{
get => _inletFlow;
private set => SetProperty(ref _inletFlow, value);
}
[Category("📊 Estado Actual")]
[DisplayName("Flujo salida")]
[Description("Flujo de salida actual (m³/s)")]
[JsonIgnore]
public double OutletFlow
{
get => _outletFlow;
private set => SetProperty(ref _outletFlow, value);
}
[Category("📊 Estado Actual")]
[DisplayName("Balance flujo")]
[Description("Balance de flujo (entrada - salida) (m³/s)")]
[JsonIgnore]
public double FlowBalance => InletFlow - OutletFlow;
[Category("📊 Estado Actual")]
[DisplayName("Presión actual")]
[Description("Presión actual en el tanque (Pa)")]
[JsonIgnore]
public double CurrentPressure
{
get => _currentPressure;
private set => SetProperty(ref _currentPressure, value);
}
[Category("📊 Estado Actual")]
[DisplayName("Presión actual (bar)")]
[Description("Presión actual en bar")]
[JsonIgnore]
public double CurrentPressureBar => CurrentPressure / 100000.0;
// Properties - Connections
[Category("🔗 Conexiones")]
[DisplayName("Tubería entrada")]
[Description("Nombre de la tubería conectada a la entrada")]
public string ConnectedInletPipe
{
get => _connectedInletPipe;
set
{
if (SetProperty(ref _connectedInletPipe, value ?? ""))
{
ManageInletConnection();
InvalidateHydraulicNetwork();
}
}
}
[Category("🔗 Conexiones")]
[DisplayName("Tubería salida")]
[Description("Nombre de la tubería conectada a la salida")]
public string ConnectedOutletPipe
{
get => _connectedOutletPipe;
set
{
if (SetProperty(ref _connectedOutletPipe, value ?? ""))
{
ManageOutletConnection();
InvalidateHydraulicNetwork();
}
}
}
// IHydraulicComponent Implementation
[JsonIgnore]
public bool HasHydraulicComponents => true;
public List<HydraulicNodeDefinition> GetHydraulicNodes()
{
var nodes = new List<HydraulicNodeDefinition>();
// El tanque siempre es un nodo de presión fija si está configurado así
if (IsFixedPressure)
{
nodes.Add(new HydraulicNodeDefinition(Nombre, true, TankPressure, GetTankDescription()));
//Debug.WriteLine($"Tanque {Nombre}: Nodo de presión fija creado - {TankPressure:F0} Pa ({TankPressureBar:F2} bar)");
}
else
{
nodes.Add(new HydraulicNodeDefinition(Nombre, false, null, GetTankDescription()));
//Debug.WriteLine($"Tanque {Nombre}: Nodo de presión libre creado");
}
return nodes;
}
public List<HydraulicElementDefinition> GetHydraulicElements()
{
// Los tanques no generan elementos hidráulicos, solo nodos
return new List<HydraulicElementDefinition>();
}
public void UpdateHydraulicProperties()
{
// Actualizar propiedades antes de la simulación
SafeUpdateTankPressure();
SafeUpdateVolumeCalculations();
}
public void ApplyHydraulicResults(Dictionary<string, double> flows, Dictionary<string, double> pressures)
{
var deltaTime = GetDeltaTime();
// Obtener flujos conectados
UpdateFlowsFromConnectedPipes(flows);
// Actualizar presión
if (pressures.ContainsKey(Nombre))
{
CurrentPressure = pressures[Nombre];
}
// Actualizar nivel basado en balance de flujo
UpdateLevelFromFlowBalance(deltaTime);
if (VerboseLogging())
{
Debug.WriteLine($"Tanque {Nombre}: Nivel={CurrentLevel:F2}m ({FillPercentage:F1}%), " +
$"Flujo_entrada={InletFlow:F6}m³/s, Flujo_salida={OutletFlow:F6}m³/s, " +
$"Balance={FlowBalance:F6}m³/s, Presión={CurrentPressure:F0}Pa");
}
}
// UI Properties
/// <summary>
/// Color del nivel de líquido
/// </summary>
[JsonIgnore]
public SolidColorBrush LevelColor
{
get
{
var percentage = FillPercentage;
if (percentage < 20) return new SolidColorBrush(Colors.Red);
if (percentage < 50) return new SolidColorBrush(Colors.Orange);
if (percentage < 80) return new SolidColorBrush(Colors.Yellow);
return new SolidColorBrush(Colors.LightBlue);
}
}
/// <summary>
/// Color del borde del nivel de líquido
/// </summary>
[JsonIgnore]
public SolidColorBrush LevelBorderColor
{
get
{
var percentage = FillPercentage;
if (percentage < 20) return new SolidColorBrush(Colors.DarkRed);
if (percentage < 50) return new SolidColorBrush(Colors.DarkOrange);
if (percentage < 80) return new SolidColorBrush(Colors.DarkGoldenrod);
return new SolidColorBrush(Colors.DarkBlue);
}
}
/// <summary>
/// Indica si hay conexión de entrada
/// </summary>
[JsonIgnore]
public bool HasInletConnection => !string.IsNullOrEmpty(ConnectedInletPipe);
/// <summary>
/// Indica si hay conexión de salida
/// </summary>
[JsonIgnore]
public bool HasOutletConnection => !string.IsNullOrEmpty(ConnectedOutletPipe);
/// <summary>
/// Indicador del tipo de tanque
/// </summary>
[JsonIgnore]
public string TankTypeIndicator
{
get
{
return TankType switch
{
HydraulicTankType.Suction => "S",
HydraulicTankType.Intermediate => "I",
HydraulicTankType.Storage => "A",
HydraulicTankType.Process => "P",
_ => "T"
};
}
}
/// <summary>
/// Balance de flujo en L/min
/// </summary>
[JsonIgnore]
public double FlowBalanceLMin => FlowBalance * 60000; // m³/s a L/min
/// <summary>
/// Color del balance de flujo
/// </summary>
[JsonIgnore]
public SolidColorBrush FlowBalanceColor
{
get
{
var balance = FlowBalance;
if (Math.Abs(balance) < 0.0001) return new SolidColorBrush(Colors.Gray);
return balance > 0 ? new SolidColorBrush(Colors.Green) : new SolidColorBrush(Colors.Red);
}
}
/// <summary>
/// Posición Y de la línea de nivel medio (50%)
/// </summary>
[JsonIgnore]
public double MidLevelY
{
get
{
var tankHeight = Tamano * 100; // Convertir a píxeles aproximados
return tankHeight * 0.5; // 50% de altura
}
}
/// <summary>
/// Posición Y de la línea de nivel mínimo
/// </summary>
[JsonIgnore]
public double MinLevelY
{
get
{
var tankHeight = Tamano * 100; // Convertir a píxeles aproximados
var minPercentage = (MinLevel - MinLevel) / (MaxLevel - MinLevel) * 100;
return tankHeight - (tankHeight * (minPercentage + 10) / 100); // 10% del nivel mínimo
}
}
// Private Methods
private void InitializeDefaults()
{
// Valores mínimos para que el objeto sea funcional
try
{
if (string.IsNullOrEmpty(nombre))
nombre = "Tanque Hidráulico";
if (Tamano <= 0)
Tamano = 1.0f;
if (Ancho <= 0)
Ancho = 0.30f;
if (Alto <= 0)
Alto = 0.40f;
SafeUpdateVolumeCalculations();
SafeUpdateTankPressure();
Debug.WriteLine("osHydTank: Initialized with default values");
}
catch (Exception ex)
{
Debug.WriteLine($"Error in InitializeDefaults: {ex.Message}");
}
}
private void SafeUpdateVolumeCalculations()
{
try
{
_currentVolume = _currentLevel * _crossSectionalArea;
_maxVolume = _maxLevel * _crossSectionalArea;
}
catch (Exception ex)
{
Debug.WriteLine($"Error in SafeUpdateVolumeCalculations: {ex.Message}");
// Usar valores por defecto seguros
_currentVolume = 1.0;
_maxVolume = 2.0;
}
}
private void SafeUpdateTankPressure()
{
try
{
if (IsFixedPressure)
{
CurrentPressure = TankPressure;
}
}
catch (Exception ex)
{
Debug.WriteLine($"Error in SafeUpdateTankPressure: {ex.Message}");
// Usar presión atmosférica por defecto
_currentPressure = 101325.0;
}
}
private string GetTankDescription()
{
return TankType switch
{
HydraulicTankType.Suction => "Tanque de succión",
HydraulicTankType.Intermediate => "Tanque intermedio",
HydraulicTankType.Storage => "Tanque de almacenamiento",
HydraulicTankType.Process => "Tanque de proceso",
_ => "Tanque hidráulico"
};
}
private void UpdateTankConfiguration()
{
// Configuración automática según el tipo de tanque
switch (TankType)
{
case HydraulicTankType.Suction:
// Tanques de succión típicamente a presión atmosférica
if (TankPressure < 50000) // Si no se ha configurado manualmente
TankPressure = 101325.0; // 1 atm
IsFixedPressure = true;
break;
case HydraulicTankType.Intermediate:
case HydraulicTankType.Storage:
case HydraulicTankType.Process:
// Estos pueden tener presión variable o fija según el proceso
break;
}
}
private void UpdateVolumeCalculations()
{
CurrentVolume = CurrentLevel * CrossSectionalArea;
MaxVolume = MaxLevel * CrossSectionalArea;
}
private void UpdateTankPressure()
{
if (IsFixedPressure)
{
CurrentPressure = TankPressure;
}
}
private void UpdateFlowsFromConnectedPipes(Dictionary<string, double> flows)
{
InletFlow = 0.0;
OutletFlow = 0.0;
// Buscar flujos en las ramas que involucren este tanque
foreach (var flow in flows)
{
var branchName = flow.Key;
var flowValue = flow.Value;
// Buscar si esta rama conecta este tanque
if (branchName.Contains("Pump") && HasPumpConnection(branchName, flowValue))
{
continue; // Ya manejado en HasPumpConnection
}
// Buscar otras conexiones directas al tanque por nombre
if (branchName.Contains(Nombre))
{
if (flowValue > 0)
{
// Flujo positivo hacia o desde el tanque
if (IsFlowTowardsTank(branchName))
{
InletFlow += flowValue;
}
else
{
OutletFlow += flowValue;
}
}
}
}
}
private bool HasPumpConnection(string branchName, double flowValue)
{
if (_mainViewModel == null) return false;
// Buscar si hay una bomba conectada que esté generando este flujo
var connectedPipes = _mainViewModel.ObjetosSimulables
.OfType<osHydPipe>()
.Where(pipe => pipe.Id_ComponenteA == Nombre || pipe.Id_ComponenteB == Nombre)
.ToList();
foreach (var pipe in connectedPipes)
{
if (pipe.Id_ComponenteA == Nombre)
{
// Esta conexión sale del tanque
if (branchName.Contains(pipe.Id_ComponenteB) && branchName.Contains("Pump"))
{
OutletFlow += Math.Abs(flowValue);
return true;
}
}
else if (pipe.Id_ComponenteB == Nombre)
{
// Esta conexión llega al tanque
if (branchName.Contains(pipe.Id_ComponenteA) && branchName.Contains("Pump"))
{
InletFlow += Math.Abs(flowValue);
return true;
}
}
}
return false;
}
private bool IsFlowTowardsTank(string branchName)
{
// Determinar la dirección del flujo basándose en el nombre de la rama
return branchName.EndsWith($" -> {Nombre}") || branchName.EndsWith($"_{Nombre}");
}
private void UpdateLevelFromFlowBalance(double deltaTime)
{
if (deltaTime <= 0 || Math.Abs(deltaTime) > 1.0) // Evitar deltas muy grandes
return;
// Balance de masa: cambio_volumen = (flujo_entrada - flujo_salida) * tiempo
var volumeChange = FlowBalance * deltaTime;
var levelChange = volumeChange / CrossSectionalArea;
// Actualizar nivel con límites
var newLevel = CurrentLevel + levelChange;
CurrentLevel = Math.Max(MinLevel, Math.Min(MaxLevel, newLevel));
// Debug para verificar cambios
if (VerboseLogging() && Math.Abs(levelChange) > 0.0001)
{
Debug.WriteLine($"Tanque {Nombre}: Δt={deltaTime:F3}s, ΔVolumen={volumeChange:F6}m³, ΔNivel={levelChange:F6}m, Nivel={CurrentLevel:F3}m");
}
}
private double GetDeltaTime()
{
var currentTime = Environment.TickCount64 / 1000.0; // segundos
var deltaTime = _lastUpdateTime > 0 ? currentTime - _lastUpdateTime : 0.0;
_lastUpdateTime = currentTime;
// Limitar delta time para evitar saltos grandes
return Math.Min(deltaTime, 0.1); // máximo 100ms
}
private bool VerboseLogging()
{
return _mainViewModel?.hydraulicSimulationManager?.VerboseOutput ?? false;
}
private void ManageInletConnection()
{
// Gestionar conexión de entrada
// Implementar lógica de conexión específica si es necesario
}
private void ManageOutletConnection()
{
// Gestionar conexión de salida
// Implementar lógica de conexión específica si es necesario
}
private void InvalidateHydraulicNetwork()
{
_mainViewModel?.hydraulicSimulationManager?.InvalidateNetwork();
}
// IHydraulicFlowReceiver Implementation
public void SetFlow(double flow)
{
// Este método puede ser llamado por tuberías conectadas
// La lógica de flujo se maneja en ApplyHydraulicResults
}
public double GetFlow()
{
return FlowBalance;
}
// IHydraulicPressureReceiver Implementation
public void SetPressure(double pressure)
{
if (!IsFixedPressure)
{
CurrentPressure = pressure;
}
}
public double GetPressure()
{
return CurrentPressure;
}
// ZIndex Implementation
public int zIndex_fromFrames { get; set; } = 2;
public ZIndexEnum ZIndex_Base()
{
return ZIndexEnum.Estaticos;
}
// IosBase Implementation
public void Start()
{
// Inicialización del tanque para la simulación
SafeUpdateVolumeCalculations();
SafeUpdateTankPressure();
}
public void Inicializar(int valorInicial)
{
// Inicialización con valor inicial
SafeUpdateVolumeCalculations();
SafeUpdateTankPressure();
}
public void Disposing()
{
// Desconectar event handlers si existen
if (inletPropertyChangedHandler != null)
{
// Aquí se desconectarían los handlers cuando se implementen las conexiones
// Similar a como lo hace osHydDischargeTank
}
if (outletPropertyChangedHandler != null)
{
// Desconectar outlet handler cuando se implemente
}
}
// Serialization Support
/// <summary>
/// Se llama antes de la serialización para asegurar que el lock esté disponible
/// </summary>
private void EnsureLockInitialized()
{
if (_levelLock == null)
_levelLock = new object();
}
// osBase Overrides
public override void ucLoaded()
{
// El UserControl ya se ha cargado y podemos obtener las coordenadas para
// crear el objeto de simulacion
base.ucLoaded();
Debug.WriteLine($"osHydTank.ucLoaded(): Componente hidráulico cargado correctamente");
// Inicialización específica del tanque hidráulico
SafeUpdateVolumeCalculations();
SafeUpdateTankPressure();
// Debug: Confirmar que el UserControl se está cargando
Debug.WriteLine($"osHydTank.ucLoaded(): Tanque '{Nombre}' cargado - Tamaño: {Tamano}, ZIndex: {zIndex_fromFrames}");
}
public override void ucUnLoaded()
{
// Remover objeto hidráulico de la simulación
if (SimHydraulicTank != null)
{
hydraulicSimulationManager.Remove(SimHydraulicTank);
SimHydraulicTank = null;
}
}
}
}

View File

@ -0,0 +1,78 @@
<UserControl x:Class="CtrEditor.ObjetosSim.ucHydPipe"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
xmlns:vm="clr-namespace:CtrEditor.ObjetosSim"
mc:Ignorable="d">
<UserControl.DataContext>
<vm:osHydPipe Ancho="2.0" Alto="0.2"/>
</UserControl.DataContext>
<Canvas RenderTransformOrigin="0,0">
<Canvas.RenderTransform>
<TransformGroup>
<ScaleTransform />
<SkewTransform />
<RotateTransform Angle="{Binding Angulo}" />
<TranslateTransform />
</TransformGroup>
</Canvas.RenderTransform>
<!-- Cuerpo de la tubería -->
<Rectangle x:Name="rectPipe"
Width="{Binding Ancho, Converter={StaticResource MeterToPixelConverter}}"
Height="{Binding Alto, Converter={StaticResource MeterToPixelConverter}}"
Fill="{Binding ColorButton_oculto}"
Stroke="Black"
StrokeThickness="2"
RadiusX="5"
RadiusY="5"/>
<!-- Etiqueta con Viewbox para escalado -->
<Viewbox Width="{Binding Ancho, Converter={StaticResource MeterToPixelConverter}}"
Height="{Binding Alto, Converter={StaticResource MeterToPixelConverter}}"
Stretch="Uniform">
<Label Content="{Binding Nombre}"
VerticalAlignment="Center"
HorizontalAlignment="Center"
FontWeight="Bold"
FontSize="18"
Opacity="0.9"
Foreground="White"/>
</Viewbox>
<!-- Indicador de conexión entrada -->
<Ellipse x:Name="ConnectionIn"
Fill="Green"
Width="8"
Height="8"
Canvas.Left="-4"
Canvas.Top="{Binding Alto, Converter={StaticResource MeterToPixelConverter}, ConverterParameter=0.5}"/>
<!-- Indicador de conexión salida -->
<Ellipse x:Name="ConnectionOut"
Fill="Red"
Width="8"
Height="8"
Canvas.Right="-4"
Canvas.Top="{Binding Alto, Converter={StaticResource MeterToPixelConverter}, ConverterParameter=0.5}"/>
<!-- Indicador de flujo -->
<Border x:Name="FlowIndicator"
Background="Yellow"
CornerRadius="2"
Visibility="Collapsed"
Canvas.Top="-20"
Canvas.Left="{Binding Ancho, Converter={StaticResource MeterToPixelConverter}, ConverterParameter=0.5}">
<TextBlock x:Name="FlowText"
Text="0.00"
FontSize="6"
Foreground="Black"
Margin="2"/>
</Border>
</Canvas>
</UserControl>

View File

@ -0,0 +1,70 @@
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows;
using CtrEditor.ObjetosSim;
using CtrEditor.FuncionesBase;
namespace CtrEditor.ObjetosSim
{
/// <summary>
/// Interaction logic for ucHydPipe.xaml
/// </summary>
public partial class ucHydPipe : UserControl, IDataContainer
{
public osBase? Datos { get; set; }
public int zIndex_fromFrames { get; set; } = 0;
private bool _isHighlighted = false;
public ucHydPipe()
{
InitializeComponent();
this.Loaded += OnLoaded;
this.Unloaded += OnUnloaded;
DataContextChanged += OnDataContextChanged;
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
Datos?.ucLoaded();
}
private void OnUnloaded(object sender, RoutedEventArgs e)
{
Datos?.ucUnLoaded();
}
private void OnDataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
if (DataContext is osHydPipe pipe)
{
Datos = pipe;
}
}
#region IDataContainer Implementation
public void Highlight(bool state)
{
_isHighlighted = state;
if (state)
{
rectPipe.Stroke = new SolidColorBrush(Colors.Yellow);
rectPipe.StrokeThickness = 3;
}
else
{
rectPipe.Stroke = new SolidColorBrush(Colors.DarkSlateGray);
rectPipe.StrokeThickness = 2;
}
}
#endregion
public ZIndexEnum ZIndex_Base()
{
return ZIndexEnum.Estaticos;
}
}
}

View File

@ -0,0 +1,39 @@
<UserControl x:Class="CtrEditor.ObjetosSim.ucHydPump"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:CtrEditor.ObjetosSim"
xmlns:vm="clr-namespace:CtrEditor.ObjetosSim"
mc:Ignorable="d">
<!-- DataContext se establece desde el objeto padre, no aquí -->
<Grid RenderTransformOrigin="0,0">
<Grid.RenderTransform>
<TransformGroup>
<RotateTransform Angle="{Binding Angulo}"/>
</TransformGroup>
</Grid.RenderTransform>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- Imagen de la bomba -->
<Image Grid.Row="0" Source="{Binding ImageSource_oculta}"
Width="{Binding Tamano, Converter={StaticResource MeterToPixelConverter}}"
Height="{Binding Tamano, Converter={StaticResource MeterToPixelConverter}}"
Stretch="Uniform"/>
<!-- Panel de información con presión y caudal -->
<StackPanel Grid.Row="1" Orientation="Horizontal" HorizontalAlignment="Center">
<TextBlock Text="{Binding CurrentPressureBar, StringFormat='{}{0:F1} bar'}"
Foreground="White" Background="Blue" Padding="2" Margin="1" FontSize="8"/>
<TextBlock Text="{Binding CurrentFlowLMin, StringFormat='{}{0:F1} L/min'}"
Foreground="White" Background="Green" Padding="2" Margin="1" FontSize="8"/>
</StackPanel>
</Grid>
</UserControl>

View File

@ -0,0 +1,53 @@
using System;
using System.Diagnostics;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using CtrEditor.ObjetosSim;
using CtrEditor.FuncionesBase;
namespace CtrEditor.ObjetosSim
{
/// <summary>
/// UserControl para la bomba hidráulica
/// </summary>
public partial class ucHydPump : UserControl, IDataContainer
{
public osBase? Datos { get; set; }
public int zIndex_fromFrames { get; set; } = 0;
private bool _isHighlighted = false;
public ucHydPump()
{
InitializeComponent();
this.Loaded += OnLoaded;
this.Unloaded += OnUnloaded;
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
Datos?.ucLoaded();
}
private void OnUnloaded(object sender, RoutedEventArgs e)
{
Datos?.ucUnLoaded();
}
#region IDataContainer Implementation
public void Highlight(bool state)
{
_isHighlighted = state;
// Aquí se podría agregar lógica de resaltado si fuera necesario
}
public ZIndexEnum ZIndex_Base()
{
return ZIndexEnum.Estaticos;
}
#endregion
}
}

View File

@ -0,0 +1,115 @@
<UserControl x:Class="CtrEditor.ObjetosSim.ucHydTank"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:CtrEditor.ObjetosSim"
xmlns:vm="clr-namespace:CtrEditor.ObjetosSim"
mc:Ignorable="d">
<!-- DataContext se establece desde el objeto padre, no aquí -->
<UserControl.DataContext>
<vm:osHydTank />
</UserControl.DataContext>
<Canvas RenderTransformOrigin="0,0">
<Canvas.RenderTransform>
<TransformGroup>
<ScaleTransform />
<SkewTransform />
<RotateTransform Angle="{Binding Angulo}" />
<TranslateTransform />
</TransformGroup>
</Canvas.RenderTransform>
<!-- Contenedor principal del tanque -->
<Grid Width="{Binding Tamano, Converter={StaticResource MeterToPixelConverter}}"
Height="{Binding Tamano, Converter={StaticResource MeterToPixelConverter}}">
<!-- Fondo del tanque (contenedor vacío) -->
<Rectangle x:Name="rectTankContainer"
Fill="LightGray"
Stroke="DarkGray"
StrokeThickness="2"
RadiusX="5"
RadiusY="5"/>
<!-- Nivel del líquido -->
<Rectangle x:Name="rectLevel"
Fill="{Binding LevelColor}"
Stroke="{Binding LevelBorderColor}"
StrokeThickness="1"
RadiusX="3"
RadiusY="3"
VerticalAlignment="Bottom"
Margin="4,4,4,4">
<Rectangle.Height>
<MultiBinding Converter="{StaticResource LevelToHeightMultiConverter}">
<Binding Path="FillPercentage"/>
<Binding Path="Tamano" Converter="{StaticResource MeterToPixelConverter}"/>
</MultiBinding>
</Rectangle.Height>
</Rectangle>
<!-- Líneas de nivel (marcas visuales) -->
<Canvas>
<!-- Línea de nivel máximo -->
<Line X1="0" Y1="8" X2="{Binding ActualWidth, ElementName=rectTankContainer}" Y2="8"
Stroke="Red" StrokeThickness="1" StrokeDashArray="2,2" Opacity="0.7"/>
<!-- Línea de nivel medio -->
<Line X1="0" Y1="{Binding MidLevelY}" X2="{Binding ActualWidth, ElementName=rectTankContainer}" Y2="{Binding MidLevelY}"
Stroke="Orange" StrokeThickness="1" StrokeDashArray="2,2" Opacity="0.5"/>
<!-- Línea de nivel mínimo -->
<Line X1="0" Y1="{Binding MinLevelY}" X2="{Binding ActualWidth, ElementName=rectTankContainer}" Y2="{Binding MinLevelY}"
Stroke="Red" StrokeThickness="1" StrokeDashArray="2,2" Opacity="0.7"/>
</Canvas>
<!-- Texto del porcentaje de llenado -->
<TextBlock Text="{Binding FillPercentage, StringFormat='{}{0:F0}%'}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Foreground="White"
FontWeight="Bold"
FontSize="10"
Effect="{StaticResource DropShadowEffect}"/>
<!-- Indicador de tipo de tanque -->
<TextBlock Text="{Binding TankTypeIndicator}"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Foreground="Navy"
FontWeight="Bold"
FontSize="8"
Margin="2,0,0,0"/>
</Grid>
<!-- Panel de información -->
<Grid Canvas.Top="{Binding Tamano, Converter={StaticResource MeterToPixelConverter}, ConverterParameter=1.05}"
Width="{Binding Tamano, Converter={StaticResource MeterToPixelConverter}}">
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Center"
Margin="0,2,0,0">
<!-- Presión actual -->
<TextBlock Text="{Binding CurrentPressureBar, StringFormat='{}{0:F1} bar'}"
Foreground="White" Background="Blue"
Padding="2" Margin="1" FontSize="8"/>
<!-- Nivel actual -->
<TextBlock Text="{Binding CurrentLevel, StringFormat='{}{0:F2} m'}"
Foreground="White" Background="Green"
Padding="2" Margin="1" FontSize="8"/>
<!-- Balance de flujo -->
<TextBlock Text="{Binding FlowBalanceLMin, StringFormat='{}{0:F1} L/min'}"
Foreground="White"
Background="{Binding FlowBalanceColor}"
Padding="2" Margin="1" FontSize="8"/>
</StackPanel>
</Grid>
</Canvas>
</UserControl>

View File

@ -0,0 +1,126 @@
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using CommunityToolkit.Mvvm.ComponentModel;
using Newtonsoft.Json;
using System.Diagnostics;
using CtrEditor.ObjetosSim;
using CtrEditor.FuncionesBase;
namespace CtrEditor.ObjetosSim
{
/// <summary>
/// UserControl para el tanque hidráulico osHydTank
/// </summary>
public partial class ucHydTank : UserControl, IDataContainer
{
#region IDataContainer Implementation
[JsonIgnore]
public osBase? Datos { get; set; }
public int zIndex_fromFrames { get; set; } = 0;
private bool _isHighlighted = false;
#endregion
public ucHydTank()
{
InitializeComponent();
Loaded += OnUserControlLoaded;
Unloaded += OnUserControlUnloaded;
DataContextChanged += OnDataContextChanged;
// Debug: Confirmar que el UserControl se está creando
Debug.WriteLine("ucHydTank Constructor: UserControl del tanque hidráulico creado");
}
private void OnUserControlUnloaded(object sender, RoutedEventArgs e)
{
Datos?.ucUnLoaded();
}
private void OnDataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
if (DataContext is osHydTank tank)
{
Datos = tank;
}
}
private void OnUserControlLoaded(object sender, RoutedEventArgs e)
{
// Debug: Confirmar que el UserControl se está cargando
Debug.WriteLine($"ucHydTank.OnUserControlLoaded: DataContext = {DataContext?.GetType().Name ?? "null"}");
// Llamar a ucLoaded() usando la propiedad Datos
Datos?.ucLoaded();
// Asegurar que el DataContext esté configurado correctamente
if (Datos is osHydTank tank)
{
Debug.WriteLine($"ucHydTank.OnUserControlLoaded: Tanque '{tank.Nombre}' conectado - Tamaño: {tank.Tamano}");
// Suscribirse a cambios de propiedades para actualizar la UI
tank.PropertyChanged += Tank_PropertyChanged;
UpdateVisualProperties();
}
else
{
Debug.WriteLine("ucHydTank.OnUserControlLoaded: ERROR - Datos no es osHydTank o es null");
}
}
private void Tank_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
// Actualizar propiedades visuales cuando cambien propiedades relevantes
if (e.PropertyName == nameof(osHydTank.FillPercentage) ||
e.PropertyName == nameof(osHydTank.CurrentLevel) ||
e.PropertyName == nameof(osHydTank.FlowBalance) ||
e.PropertyName == nameof(osHydTank.TankType) ||
e.PropertyName == nameof(osHydTank.ConnectedInletPipe) ||
e.PropertyName == nameof(osHydTank.ConnectedOutletPipe))
{
UpdateVisualProperties();
}
}
private void UpdateVisualProperties()
{
if (Datos is osHydTank tank)
{
// Forzar actualización del UI - las propiedades se actualizan automáticamente
// cuando cambian las propiedades base gracias al binding
this.InvalidateVisual();
}
}
#region IDataContainer Implementation Methods
public void Highlight(bool state)
{
_isHighlighted = state;
if (state)
{
// Resaltar el contenedor del tanque con borde amarillo y más grueso
rectTankContainer.Stroke = new SolidColorBrush(Colors.Yellow);
rectTankContainer.StrokeThickness = 4;
}
else
{
// Volver al estado normal
rectTankContainer.Stroke = new SolidColorBrush(Colors.DarkGray);
rectTankContainer.StrokeThickness = 2;
}
}
public ZIndexEnum ZIndex_Base()
{
return ZIndexEnum.Estaticos;
}
#endregion
}
}

View File

@ -1,5 +1,6 @@
using Newtonsoft.Json;
using System.Windows.Controls;
using CtrEditor.HydraulicSimulator;
using CtrEditor.Simulacion;
using System.Reflection;
using System;
@ -77,7 +78,7 @@ namespace CtrEditor.ObjetosSim
return instance;
}
public static void AssignDatos(UserControl userControl, osBase datos, SimulationManagerBEPU simulationManager)
public static void AssignDatos(UserControl userControl, osBase datos, SimulationManagerBEPU simulationManager, HydraulicSimulationManager hydraulicSimulationManager = null)
{
if (userControl is IDataContainer dataContainer)
{
@ -85,6 +86,7 @@ namespace CtrEditor.ObjetosSim
userControl.DataContext = datos;
datos.VisualRepresentation = userControl;
datos.simulationManager = simulationManager;
datos.hydraulicSimulationManager = hydraulicSimulationManager;
}
}

View File

@ -1,5 +1,6 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CtrEditor.FuncionesBase;
using CtrEditor.HydraulicSimulator;
using CtrEditor.Serialization;
using CtrEditor.Services;
using CtrEditor.Simulacion;
@ -45,19 +46,22 @@ namespace CtrEditor.ObjetosSim
private MainViewModel? _mainViewModel;
private UserControl? VisualRepresentation;
private SimulationManagerBEPU? simulationManager;
private HydraulicSimulationManager? hydraulicSimulationManager;
public DataSaveToSerialize(MainViewModel a, UserControl b, SimulationManagerBEPU c)
public DataSaveToSerialize(MainViewModel a, UserControl b, SimulationManagerBEPU c, HydraulicSimulationManager d = null)
{
_mainViewModel = a;
VisualRepresentation = b;
simulationManager = c;
hydraulicSimulationManager = d;
}
public void DataRestoreAfterSerialize(out MainViewModel a, out UserControl b, out SimulationManagerBEPU c)
public void DataRestoreAfterSerialize(out MainViewModel a, out UserControl b, out SimulationManagerBEPU c, out HydraulicSimulationManager d)
{
a = _mainViewModel;
b = VisualRepresentation;
c = simulationManager;
d = hydraulicSimulationManager;
}
}
@ -1054,16 +1058,23 @@ namespace CtrEditor.ObjetosSim
[JsonIgnore]
public SimulationManagerBEPU simulationManager;
/// <summary>
/// Link al Simulador hidráulico.
/// </summary>
[JsonIgnore]
public HydraulicSimulationManager hydraulicSimulationManager;
/// <summary>
/// Prepara la clase para ser serializable poniendo a null los objetos que tienen referencias circulares
/// Luego se debe llamar a RestaurarDatosNoSerializables para restaurar el estado original
/// </summary>
public void SalvarDatosNoSerializables()
{
DataSave = new DataSaveToSerialize(_mainViewModel, _visualRepresentation, simulationManager);
DataSave = new DataSaveToSerialize(_mainViewModel, _visualRepresentation, simulationManager, hydraulicSimulationManager);
_mainViewModel = null;
_visualRepresentation = null;
simulationManager = null;
hydraulicSimulationManager = null;
}
/// <summary>
@ -1072,7 +1083,7 @@ namespace CtrEditor.ObjetosSim
public void RestaurarDatosNoSerializables()
{
if (DataSave == null) return;
DataSave.DataRestoreAfterSerialize(out _mainViewModel, out _visualRepresentation, out simulationManager);
DataSave.DataRestoreAfterSerialize(out _mainViewModel, out _visualRepresentation, out simulationManager, out hydraulicSimulationManager);
}
/// <summary>
/// Se llama una unica vez al conectar con el PLC.
@ -1919,9 +1930,30 @@ namespace CtrEditor.ObjetosSim
items.Add("");
foreach (var obj in objetosSimulables)
{
if (obj.GetType() == typeof(T))
try
{
items.Add(obj.Nombre);
// Verificar si el tipo T es una interfaz
if (typeof(T).IsInterface)
{
// Para interfaces, verificar si el objeto implementa la interfaz
if (typeof(T).IsAssignableFrom(obj.GetType()))
{
items.Add(obj.Nombre);
}
}
else
{
// Para clases concretas, usar comparación directa de tipos
if (obj.GetType() == typeof(T))
{
items.Add(obj.Nombre);
}
}
}
catch (Exception ex)
{
// Log the exception but continue processing other objects
System.Diagnostics.Debug.WriteLine($"Error checking type compatibility for {obj?.GetType()?.Name}: {ex.Message}");
}
}

View File

@ -270,6 +270,7 @@ namespace CtrEditor.Serialization
{
Formatting = Formatting.Indented,
NullValueHandling = NullValueHandling.Ignore,
DefaultValueHandling = DefaultValueHandling.Include, // Incluir valores por defecto
TypeNameHandling = TypeNameHandling.Auto,
ObjectCreationHandling = ObjectCreationHandling.Replace,
ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor,

0
Simulacion/simHydPipe.cs Normal file
View File

0
Simulacion/simHydTank.cs Normal file
View File

View File

@ -0,0 +1,197 @@
using BepuPhysics.Collidables;
using BepuPhysics.Constraints;
using BepuPhysics;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Text;
using System.Threading.Tasks;
using CtrEditor.HydraulicSimulator;
namespace CtrEditor.Simulacion
{
public class simPumpExample : simBase
{
public float Width;
public float Height;
public float Depth;
private float _mass;
// Propiedades hidráulicas
public double PumpHead { get; set; } = 50.0;
public double MaxFlow { get; set; } = 0.01;
public double SpeedRatio { get; set; } = 1.0;
public bool IsRunning { get; set; } = true;
public int PumpDirection { get; set; } = 1;
// Estado actual de la simulación hidráulica
public double CurrentFlow { get; set; } = 0.0;
public double CurrentPressure { get; set; } = 0.0;
public simPumpExample(Simulation simulation, List<Action> deferredActions, Vector3 dimensions, Vector2 position, float mass)
{
_simulation = simulation;
Width = dimensions.X;
Height = dimensions.Y;
Depth = dimensions.Z;
_mass = mass;
// Usar COORDINATECONVERTER para conversión centralizada
var position3D = new Vector3(position.X, CoordinateConverter.WpfYToBepuY(position.Y), Depth / 2f + zPos_Transporte + zAltura_Transporte);
Create(position3D);
}
public float CenterX
{
get
{
return GetPosition().X;
}
set
{
var pos = GetPosition();
SetPosition(value, pos.Y, pos.Z);
}
}
public float CenterY
{
get
{
// Usar COORDINATECONVERTER para conversión centralizada
return CoordinateConverter.BepuYToWpfY(GetPosition().Y);
}
set
{
var pos = GetPosition();
// Usar COORDINATECONVERTER para conversión centralizada
SetPosition(pos.X, CoordinateConverter.WpfYToBepuY(value), pos.Z);
}
}
public Vector2 Center
{
get
{
var pos3D = GetPosition();
// Usar COORDINATECONVERTER para conversión centralizada
return CoordinateConverter.BepuVector3ToWpfVector2(pos3D);
}
set
{
// Mantener la Z actual, solo cambiar X, Y
var currentPos = GetPosition();
// Usar COORDINATECONVERTER para conversión centralizada
SetPosition(value.X, CoordinateConverter.WpfYToBepuY(value.Y), currentPos.Z);
}
}
public float Mass
{
get
{
if (_simulation != null && _simulation.Bodies.BodyExists(BodyHandle))
{
var bodyReference = _simulation.Bodies.GetBodyReference(BodyHandle);
return 1f / bodyReference.LocalInertia.InverseMass;
}
return _mass;
}
set
{
_mass = value;
if (_simulation != null && _simulation.Bodies.BodyExists(BodyHandle))
{
var box = new Box(Width, Height, Depth);
var inertia = box.ComputeInertia(_mass);
_simulation.Bodies.SetLocalInertia(BodyHandle, inertia);
}
}
}
private void Create(Vector3 position)
{
RemoverBody();
var box = new Box(Width, Height, Depth);
var shapeIndex = _simulation.Shapes.Add(box);
// Crear el cuerpo estático (las bombas no se mueven físicamente)
var inertia = box.ComputeInertia(_mass);
var bodyDescription = BodyDescription.CreateKinematic(
new RigidPose(position),
new CollidableDescription(shapeIndex, 0),
new BodyActivityDescription(-1f)
);
BodyHandle = _simulation.Bodies.Add(bodyDescription);
_bodyCreated = true;
// Registrar en el diccionario del SimulationManager
if (_simulationManager != null)
_simulationManager.CollidableData[BodyHandle.Value] = this;
}
public void SetDimensions(Vector3 dimensions)
{
Width = dimensions.X;
Height = dimensions.Y;
Depth = dimensions.Z;
if (_simulation != null && _simulation.Bodies.BodyExists(BodyHandle))
{
var box = new Box(Width, Height, Depth);
var shapeIndex = _simulation.Shapes.Add(box);
ChangeBodyShape(shapeIndex);
}
}
public void SetMass(float mass)
{
Mass = mass;
}
/// <summary>
/// Actualiza las propiedades hidráulicas de la bomba
/// </summary>
public void UpdateHydraulicProperties(double pumpHead, double maxFlow, double speedRatio, bool isRunning, int direction)
{
PumpHead = pumpHead;
MaxFlow = maxFlow;
SpeedRatio = speedRatio;
IsRunning = isRunning;
PumpDirection = direction;
}
/// <summary>
/// Aplica los resultados de la simulación hidráulica
/// </summary>
public void ApplyHydraulicResults(double flow, double pressure)
{
CurrentFlow = flow;
CurrentPressure = pressure;
}
/// <summary>
/// Obtiene el caudal actual de la bomba
/// </summary>
public double GetCurrentFlow()
{
return CurrentFlow;
}
/// <summary>
/// Obtiene la presión actual de la bomba
/// </summary>
public double GetCurrentPressure()
{
return CurrentPressure;
}
public new void RemoverBody()
{
base.RemoverBody();
}
}
}

View File

@ -380,14 +380,123 @@ namespace CtrEditor
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (values[0] is float level && values[1] is double containerHeight)
if (values?.Length >= 2)
{
return containerHeight * (level / 100);
// Verificar que los valores sean válidos antes de convertir
if (!TryConvertToDouble(values[0], out double level) ||
!TryConvertToDouble(values[1], out double containerHeight))
{
return 0.0; // Retornar 0 si no se pueden convertir los valores
}
var result = containerHeight * (level / 100.0);
return result;
}
return 0.0;
}
private static bool TryConvertToDouble(object value, out double result)
{
result = 0.0;
// Verificar valores nulos o especiales de WPF
if (value == null ||
value == DependencyProperty.UnsetValue ||
value.GetType().Name.Contains("NamedObject"))
{
return false;
}
// Intentar conversión directa para tipos numéricos
if (value is double d)
{
result = d;
return true;
}
if (value is float f)
{
result = f;
return true;
}
if (value is int i)
{
result = i;
return true;
}
if (value is decimal dec)
{
result = (double)dec;
return true;
}
// Intentar conversión de string
if (value is string str && double.TryParse(str, out result))
{
return true;
}
// Último intento: usar Convert.ToDouble solo si es IConvertible
if (value is IConvertible)
{
try
{
result = System.Convert.ToDouble(value, CultureInfo.InvariantCulture);
return true;
}
catch
{
return false;
}
}
return false;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
public class PercentageToHeightConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is float percentage)
{
// Assume the tank height is 100 pixels by default, or use parameter if provided
double tankHeight = 100;
if (parameter is double paramHeight)
tankHeight = paramHeight;
else if (parameter is string paramStr && double.TryParse(paramStr, out double parsed))
tankHeight = parsed;
return tankHeight * (percentage / 100.0);
}
return 0;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
public class BoolToColorConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is bool isTrue)
{
return isTrue ? new SolidColorBrush(Colors.Green) : new SolidColorBrush(Colors.Red);
}
return new SolidColorBrush(Colors.Gray);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}

BIN
imagenes/pump_run.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
imagenes/pump_stop.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB