Refactor TSNet Hydraulic Simulation Integration

- Implemented node name sanitization in TSNetINPGenerator to ensure compatibility with EPANET INP format.
- Enhanced TSNetSimulationManager to manage hydraulic components and elements more robustly, including improved error handling and logging.
- Replaced HydraulicSimulationManager with TSNetSimulationManager in MainViewModel, updating all relevant references and methods.
- Improved handling of IOException tracking in MainViewModel for better debugging and stability during simulation runs.
- Updated osHydPipe, osHydPump, and osHydTank classes to utilize sanitized node names for hydraulic nodes.
- Added new methods for resetting and clearing hydraulic objects in TSNetSimulationManager.
- Enhanced UserControlFactory to support the new TSNetSimulationManager without assigning the old hydraulic simulation manager.
This commit is contained in:
Miguel 2025-09-11 22:30:37 +02:00
parent 1b21f86886
commit 51d0f36187
10 changed files with 779 additions and 80 deletions

View File

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

View File

@ -228,14 +228,36 @@ namespace CtrEditor.HydraulicSimulator
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);
if (VerboseOutput)
try
{
Trace.WriteLine($"Rama agregada: {elemDef.Name} " +
$"({elemDef.FromNode} -> {elemDef.ToNode})");
// Validar que los nodos existan antes de agregar la rama
if (!Network.Nodes.ContainsKey(elemDef.FromNode))
{
Debug.WriteLine($"ERROR: Nodo '{elemDef.FromNode}' no existe. Nodos disponibles: {string.Join(", ", Network.Nodes.Keys)}");
continue;
}
if (!Network.Nodes.ContainsKey(elemDef.ToNode))
{
Debug.WriteLine($"ERROR: Nodo '{elemDef.ToNode}' no existe. Nodos disponibles: {string.Join(", ", Network.Nodes.Keys)}");
continue;
}
// Crear rama con el elemento
var elements = new List<Element> { elemDef.Element };
Network.AddBranch(elemDef.FromNode, elemDef.ToNode, elements, elemDef.Name);
if (VerboseOutput)
{
Trace.WriteLine($"Rama agregada: {elemDef.Name} " +
$"({elemDef.FromNode} -> {elemDef.ToNode})");
}
}
catch (Exception ex)
{
Debug.WriteLine($"Error agregando rama {elemDef.Name}: {ex.Message}");
Debug.WriteLine($" FromNode: '{elemDef.FromNode}', ToNode: '{elemDef.ToNode}'");
Debug.WriteLine($" Nodos disponibles: {string.Join(", ", Network.Nodes.Keys)}");
}
}
}

View File

@ -298,17 +298,43 @@ try:
# Cargar el modelo usando WNTR (TSNet usa WNTR internamente)
wn = wntr.network.WaterNetworkModel(r'{inpFilePath}')
# Configurar simulación transitoria
wn.set_time(duration=10.0, dt=0.01) # 10 segundos, dt=0.01s
# Configurar WNTR options para headloss formula
wn.options.hydraulic.headloss = 'D-W' # Darcy-Weisbach
wn.options.time.duration = 10.0 # 10 segundos de duración
wn.options.time.hydraulic_timestep = 0.01 # Paso de tiempo de 0.01 segundos
wn.options.time.report_timestep = 0.01 # Reportar cada 0.01 segundos
# Ejecutar simulación
results = tsnet.simulation.run_transient_simulation(wn, results_dir=r'{outputDir}')
# CORRECCIÓN: TSNet.TransientModel necesita el archivo INP, no el objeto WaterNetworkModel
# Convertir a modelo transient de TSNet usando el archivo INP directamente
tm = tsnet.network.TransientModel(r'{inpFilePath}')
# Ejecutar simulación usando la API correcta de TSNet
try:
# Método correcto de TSNet
results = tsnet.simulation.MOCSimulator(tm, results_obj='results', friction='steady')
print('Simulación TSNet completada exitosamente')
print(f'Resultados generados en modelo transient')
# Guardar resultados si es necesario
print('Directorio de resultados: ' + r'{outputDir}')
except Exception as tsnet_error:
print('Error en TSNet: ' + str(tsnet_error))
# Fallback a simulación básica con WNTR
try:
import wntr.sim
sim = wntr.sim.EpanetSimulator(wn)
results = sim.run_sim()
print('Ejecutada simulación básica WNTR (fallback)')
except Exception as wntr_error:
print('Error en WNTR fallback: ' + str(wntr_error))
raise tsnet_error
print('Simulación completada exitosamente')
print(r'Resultados guardados en: {outputDir}')
print('Resultados guardados en: ' + r'{outputDir}')
except Exception as e:
print(f'Error en simulación TSNet: {{e}}')
print('Error en simulación TSNet: ' + str(e))
raise
";

View File

@ -90,7 +90,8 @@ namespace CtrEditor.HydraulicSimulator.TSNet
{
var elevation = GetNodeElevation(node);
var demand = GetNodeDemand(node);
content.AppendLine($" {node.Name,-15}\t{elevation.ToString("F2", CultureInfo.InvariantCulture)} \t{demand.ToString("F2", CultureInfo.InvariantCulture)} \t;");
var sanitizedName = SanitizeNodeName(node.Name);
content.AppendLine($" {sanitizedName,-15}\t{elevation.ToString("F2", CultureInfo.InvariantCulture)} \t{demand.ToString("F2", CultureInfo.InvariantCulture)} \t;");
}
content.AppendLine();
@ -104,7 +105,8 @@ namespace CtrEditor.HydraulicSimulator.TSNet
foreach (var node in _network.Nodes.Values.Where(n => n.FixedP && !IsTank(n)))
{
var head = PressureToHead(node.P);
content.AppendLine($" {node.Name,-15}\t{head.ToString("F2", CultureInfo.InvariantCulture)} \t;");
var sanitizedName = SanitizeNodeName(node.Name);
content.AppendLine($" {sanitizedName,-15}\t{head.ToString("F2", CultureInfo.InvariantCulture)} \t;");
}
content.AppendLine();
@ -120,7 +122,8 @@ namespace CtrEditor.HydraulicSimulator.TSNet
foreach (var node in tankNodes)
{
var elevation = GetNodeElevation(node);
content.AppendLine($" {node.Name,-15}\t{elevation:F2} \t1.0 \t0.0 \t2.0 \t1.0 \t0 \t");
var sanitizedName = SanitizeNodeName(node.Name);
content.AppendLine($" {sanitizedName,-15}\t{elevation.ToString("F2", CultureInfo.InvariantCulture)} \t1.0 \t0.0 \t2.0 \t1.0 \t0 \t");
}
content.AppendLine();
@ -141,7 +144,10 @@ namespace CtrEditor.HydraulicSimulator.TSNet
var diameter = element.D * 1000; // Usar D en lugar de Diameter, convertir a mm
var roughness = element.Rough * 1000; // Usar Rough en lugar de Roughness, convertir a mm
content.AppendLine($" {id,-15}\t{branch.N1,-15}\t{branch.N2,-15}\t{length.ToString("F2", CultureInfo.InvariantCulture)} \t{diameter.ToString("F1", CultureInfo.InvariantCulture)} \t{roughness.ToString("F4", CultureInfo.InvariantCulture)} \t0 \tOpen");
var sanitizedN1 = SanitizeNodeName(branch.N1);
var sanitizedN2 = SanitizeNodeName(branch.N2);
content.AppendLine($" {id,-15}\t{sanitizedN1,-15}\t{sanitizedN2,-15}\t{length.ToString("F2", CultureInfo.InvariantCulture)} \t{diameter.ToString("F1", CultureInfo.InvariantCulture)} \t{roughness.ToString("F4", CultureInfo.InvariantCulture)} \t0 \tOpen");
}
}
@ -159,7 +165,10 @@ namespace CtrEditor.HydraulicSimulator.TSNet
foreach (var element in branch.Elements.OfType<PumpHQ>())
{
var id = $"PUMP{pumpId}";
content.AppendLine($" {id,-15}\t{branch.N1,-15}\t{branch.N2,-15}\tHEAD CURVE{pumpId}");
var sanitizedN1 = SanitizeNodeName(branch.N1);
var sanitizedN2 = SanitizeNodeName(branch.N2);
content.AppendLine($" {id,-15}\t{sanitizedN1,-15}\t{sanitizedN2,-15}\tHEAD CURVE{pumpId}");
pumpId++;
}
}
@ -213,9 +222,9 @@ namespace CtrEditor.HydraulicSimulator.TSNet
var maxHead = element.H0;
var maxFlow = element.H0 / 10; // Estimación simple
content.AppendLine($" CURVE{curveId} \t0 \t{maxHead:F2}");
content.AppendLine($" CURVE{curveId} \t{maxFlow/2:F2} \t{maxHead*0.8:F2}");
content.AppendLine($" CURVE{curveId} \t{maxFlow:F2} \t{maxHead*0.5:F2}");
content.AppendLine($" CURVE{curveId} \t0 \t{maxHead.ToString("F2", CultureInfo.InvariantCulture)}");
content.AppendLine($" CURVE{curveId} \t{(maxFlow/2).ToString("F2", CultureInfo.InvariantCulture)} \t{(maxHead*0.8).ToString("F2", CultureInfo.InvariantCulture)}");
content.AppendLine($" CURVE{curveId} \t{maxFlow.ToString("F2", CultureInfo.InvariantCulture)} \t{(maxHead*0.5).ToString("F2", CultureInfo.InvariantCulture)}");
curveId++;
}
@ -231,7 +240,8 @@ namespace CtrEditor.HydraulicSimulator.TSNet
foreach (var node in _network.Nodes.Values)
{
content.AppendLine($" {node.Name,-15}\t0.0");
var sanitizedName = SanitizeNodeName(node.Name);
content.AppendLine($" {sanitizedName,-15}\t0.0");
}
content.AppendLine();
@ -292,7 +302,8 @@ namespace CtrEditor.HydraulicSimulator.TSNet
int x = 0, y = 0;
foreach (var node in _network.Nodes.Values)
{
content.AppendLine($" {node.Name,-15}\t{x:F2} \t{y:F2}");
var sanitizedName = SanitizeNodeName(node.Name);
content.AppendLine($" {sanitizedName,-15}\t{x.ToString("F2", CultureInfo.InvariantCulture)} \t{y.ToString("F2", CultureInfo.InvariantCulture)}");
x += 1000;
if (x > 5000)
{
@ -341,6 +352,45 @@ namespace CtrEditor.HydraulicSimulator.TSNet
return Math.Sqrt(valve.KvFull / 100.0) * 0.1; // en metros
}
/// <summary>
/// Sanitiza nombres de nodos para compatibilidad con EPANET INP
/// </summary>
private string SanitizeNodeName(string nodeName)
{
if (string.IsNullOrEmpty(nodeName))
return "UnknownNode";
// Reemplazar espacios y caracteres especiales con guiones bajos
var sanitized = nodeName
.Replace(" ", "_")
.Replace("á", "a")
.Replace("é", "e")
.Replace("í", "i")
.Replace("ó", "o")
.Replace("ú", "u")
.Replace("ü", "u")
.Replace("ñ", "n")
.Replace("Á", "A")
.Replace("É", "E")
.Replace("Í", "I")
.Replace("Ó", "O")
.Replace("Ú", "U")
.Replace("Ü", "U")
.Replace("Ñ", "N");
// Remover caracteres no válidos para EPANET
var validChars = sanitized.Where(c => char.IsLetterOrDigit(c) || c == '_' || c == '-').ToArray();
var result = new string(validChars);
// Asegurar que empiece con una letra
if (!string.IsNullOrEmpty(result) && !char.IsLetter(result[0]))
{
result = "Node_" + result;
}
return string.IsNullOrEmpty(result) ? "UnknownNode" : result;
}
#endregion
}
}

View File

@ -52,6 +52,11 @@ namespace CtrEditor.HydraulicSimulator.TSNet
/// </summary>
public bool IsRunning { get; private set; }
/// <summary>
/// Indica si la red necesita ser reconstruida
/// </summary>
public bool NetworkNeedsRebuild { get; set; } = true;
/// <summary>
/// Evento disparado cuando se completa una simulación
/// </summary>
@ -224,12 +229,73 @@ namespace CtrEditor.HydraulicSimulator.TSNet
{
Network = new HydraulicNetwork();
// Procesar objetos IHydraulicComponent
foreach (var obj in HydraulicObjects)
var hydraulicComponents = HydraulicObjects.OfType<IHydraulicComponent>().ToList();
// Primera pasada: Agregar TODOS los nodos
Debug.WriteLine("TSNet: Primera pasada - agregando todos los nodos...");
foreach (var component in hydraulicComponents)
{
if (obj is IHydraulicComponent hydraulicComponent)
try
{
ProcessHydraulicComponent(hydraulicComponent);
var nodes = component.GetHydraulicNodes();
foreach (var nodeDefinition in nodes)
{
if (nodeDefinition.IsFixedPressure)
{
Network.AddNode(nodeDefinition.Name, nodeDefinition.Pressure);
}
else
{
Network.AddNode(nodeDefinition.Name);
}
}
}
catch (Exception ex)
{
Debug.WriteLine($"Error agregando nodos del componente {component}: {ex.Message}");
}
}
Debug.WriteLine($"TSNet: Nodos agregados - Total: {Network.Nodes.Count}");
Debug.WriteLine($"TSNet: Nodos disponibles: {string.Join(", ", Network.Nodes.Keys)}");
// Segunda pasada: Agregar TODOS los elementos
Debug.WriteLine("TSNet: Segunda pasada - agregando todos los elementos...");
foreach (var component in hydraulicComponents)
{
try
{
var elements = component.GetHydraulicElements();
foreach (var elementDefinition in elements)
{
try
{
// Verificar que los nodos existan antes de agregar el elemento
if (Network.Nodes.ContainsKey(elementDefinition.FromNode) &&
Network.Nodes.ContainsKey(elementDefinition.ToNode))
{
Network.AddElement(elementDefinition.Element,
elementDefinition.FromNode,
elementDefinition.ToNode,
elementDefinition.Name);
Debug.WriteLine($"Rama agregada: {elementDefinition.Name} ({elementDefinition.FromNode} -> {elementDefinition.ToNode})");
}
else
{
Debug.WriteLine($"ERROR: Nodo '{elementDefinition.FromNode}' o '{elementDefinition.ToNode}' no existe. " +
$"Nodos disponibles: {string.Join(", ", Network.Nodes.Keys)}");
}
}
catch (Exception elementEx)
{
Debug.WriteLine($"Error agregando elemento {elementDefinition.Name}: {elementEx.Message}");
}
}
}
catch (Exception ex)
{
Debug.WriteLine($"Error obteniendo elementos del componente {component}: {ex.Message}");
}
}
@ -616,8 +682,29 @@ namespace CtrEditor.HydraulicSimulator.TSNet
var elements = component.GetHydraulicElements();
foreach (var elementDefinition in elements)
{
// TODO: Convertir elementDefinition a Element y agregar a la red
// Por ahora creamos elementos básicos
try
{
// Verificar que los nodos existan antes de agregar el elemento
if (Network.Nodes.ContainsKey(elementDefinition.FromNode) &&
Network.Nodes.ContainsKey(elementDefinition.ToNode))
{
Network.AddElement(elementDefinition.Element,
elementDefinition.FromNode,
elementDefinition.ToNode,
elementDefinition.Name);
Debug.WriteLine($"Rama agregada: {elementDefinition.Name} ({elementDefinition.FromNode} -> {elementDefinition.ToNode})");
}
else
{
Debug.WriteLine($"ERROR: Nodo '{elementDefinition.FromNode}' o '{elementDefinition.ToNode}' no existe. " +
$"Nodos disponibles: {string.Join(", ", Network.Nodes.Keys)}");
}
}
catch (Exception elementEx)
{
Debug.WriteLine($"Error agregando elemento {elementDefinition.Name}: {elementEx.Message}");
}
}
}
catch (Exception ex)
@ -646,6 +733,55 @@ namespace CtrEditor.HydraulicSimulator.TSNet
#endregion
#region Additional Methods for Compatibility
/// <summary>
/// Reinicia el simulador hidráulico (equivalente a Reset del HydraulicSimulationManager)
/// </summary>
public void Reset()
{
ResetAllCalculatedValues();
Debug.WriteLine("TSNet: Simulador reiniciado");
}
/// <summary>
/// Obtiene estadísticas de la simulación
/// </summary>
public string GetSimulationStats()
{
return $"TSNet - Objetos: {HydraulicObjects.Count}, Tanques: {_tankAdapters.Count}, Bombas: {_pumpAdapters.Count}, Tuberías: {_pipeAdapters.Count}";
}
/// <summary>
/// Propiedad para habilitar/deshabilitar la simulación hidráulica
/// </summary>
public bool IsHydraulicSimulationEnabled { get; set; } = true;
/// <summary>
/// Limpia todos los objetos hidráulicos
/// </summary>
public void ClearHydraulicObjects()
{
HydraulicObjects.Clear();
_objectMapping.Clear();
_tankAdapters.Clear();
_pumpAdapters.Clear();
_pipeAdapters.Clear();
Network = new HydraulicNetwork();
Debug.WriteLine("TSNet: Todos los objetos hidráulicos limpiados");
}
/// <summary>
/// Invalida la red para forzar reconstrucción
/// </summary>
public void InvalidateNetwork()
{
NetworkNeedsRebuild = true;
Debug.WriteLine("TSNet: Red invalidada y marcada para reconstrucción");
}
#endregion
#region IDisposable
public void Dispose()

View File

@ -77,7 +77,7 @@ namespace CtrEditor
private bool Debug_SimulacionCreado = false;
public SimulationManagerBEPU simulationManager = new SimulationManagerBEPU();
public HydraulicSimulationManager hydraulicSimulationManager = new HydraulicSimulationManager();
// ELIMINADO: HydraulicSimulationManager - reemplazado por TSNetSimulationManager
public TSNetSimulationManager tsnetSimulationManager = new TSNetSimulationManager();
private readonly System.Timers.Timer _timerSimulacion; // Cambiado a System.Timers.Timer para mejor precisión
@ -86,6 +86,12 @@ namespace CtrEditor
private readonly DispatcherTimer _timer3DUpdate; // Nuevo timer para actualización 3D cuando simulación está detenida
private readonly System.Timers.Timer _timerDebugFlush; // Timer para flush automático del buffer de debug
private readonly System.Timers.Timer _timerTSNet; // Timer para TSNet automático cada 100ms
// Variables para tracking de IOException
private volatile bool _tsnetExecuting = false; // Flag para evitar ejecuciones simultáneas de TSNet
private DateTime _lastTSNetExecution = DateTime.MinValue; // Timestamp de última ejecución TSNet
private readonly TimeSpan _minTSNetInterval = TimeSpan.FromMilliseconds(50); // Intervalo mínimo entre ejecuciones
private int _ioExceptionCount = 0; // Contador de excepciones IOException para debugging
public Canvas MainCanvas;
@ -525,10 +531,46 @@ namespace CtrEditor
// Inicializar configuración del workspace
WorkspaceConfig = new Models.WorkspaceConfiguration();
// Configurar tracking de IOException
SetupIOExceptionTraking();
// Iniciar servidor MCP automáticamente
StartMcpServer();
}
/// <summary>
/// Configura el sistema de tracking de IOException mejorado para capturar excepciones de WPF
/// </summary>
private void SetupIOExceptionTraking()
{
// Manejador para excepciones de primera oportunidad (first-chance exceptions)
// Esto captura TODAS las IOException, incluso las que son manejadas internamente por WPF
AppDomain.CurrentDomain.FirstChanceException += (sender, e) =>
{
if (e.Exception is System.IO.IOException ioEx)
{
_ioExceptionCount++;
var timestamp = DateTime.Now.ToString("HH:mm:ss:fff");
System.Diagnostics.Debug.WriteLine($"[{timestamp}] 🔍 IOException #{_ioExceptionCount} detectada (First-Chance):");
System.Diagnostics.Debug.WriteLine($" Mensaje: {ioEx.Message}");
System.Diagnostics.Debug.WriteLine($" Source: {ioEx.Source}");
System.Diagnostics.Debug.WriteLine($" HResult: {ioEx.HResult}");
System.Diagnostics.Debug.WriteLine($" Simulación activa: {IsSimulationRunning}");
System.Diagnostics.Debug.WriteLine($" TSNet ejecutándose: {_tsnetExecuting}");
// Verificar si está relacionada con TSNet/Python
if (ioEx.StackTrace?.Contains("Python") == true ||
ioEx.StackTrace?.Contains("tsnet") == true ||
ioEx.Source?.Contains("Presentation") == true)
{
System.Diagnostics.Debug.WriteLine($" ⚠️ Posiblemente relacionada con TSNet/WPF");
}
}
};
System.Diagnostics.Debug.WriteLine("IOException tracking configurado - se capturarán todas las excepciones de E/S");
}
#region Workspace Configuration Management
/// <summary>
@ -869,8 +911,8 @@ namespace CtrEditor
if (userControl != null)
{
// Asignar los datos al UserControl
UserControlFactory.AssignDatos(userControl, osObjeto, simulationManager, hydraulicSimulationManager);
// Asignar los datos al UserControl - Usando TSNetSimulationManager
UserControlFactory.AssignDatos(userControl, osObjeto, simulationManager, tsnetSimulationManager);
osObjeto._mainViewModel = this;
if (osObjeto.Id == null) // Para los objetos salvados antes de usar UniqueID
osObjeto.Id = new UniqueId().ObtenerNuevaID();
@ -1254,8 +1296,8 @@ namespace CtrEditor
// Ejecutar simulación física BEPU
simulationManager.Step();
// Ejecutar simulación hidráulica
hydraulicSimulationManager.Step((float)(timeBetweenCalls / 1000.0)); // Convertir ms a segundos
// ELIMINADO: hydraulicSimulationManager.Step() - Ahora solo usamos TSNet
// La simulación hidráulica se ejecuta por separado en _timerTSNet
// ✅ NUEVO: Solo crear la copia si hay objetos para procesar
if (ObjetosSimulables?.Count > 0)
@ -1277,7 +1319,7 @@ namespace CtrEditor
AdaptSimulationTiming(executionStopwatch.Elapsed.TotalMilliseconds);
//Debug.WriteLine($"OnTickSimulacion execution time: {stopwatch.ElapsedMilliseconds} ms");
//Debug.WriteLine($"OnTickSimulacion execution time: {executionStopwatch.Elapsed.TotalMilliseconds:F2}ms | Timer interval: {timeBetweenCalls:F2}ms | Objects: {ObjetosSimulables?.Count ?? 0}");
Debug.WriteLine($"OnTickSimulacion execution time: {executionStopwatch.Elapsed.TotalMilliseconds:F2}ms | Timer interval: {timeBetweenCalls:F2}ms | Objects: {ObjetosSimulables?.Count ?? 0}");
}
catch (Exception ex)
{
@ -1293,15 +1335,32 @@ namespace CtrEditor
private async void OnTSNetTimerElapsed(object sender, System.Timers.ElapsedEventArgs e)
{
// Solo ejecutar si la simulación está corriendo y hay objetos hidráulicos
if (!IsSimulationRunning || tsnetSimulationManager.IsRunning)
if (!IsSimulationRunning)
return;
try
{
// Verificar que tsnetSimulationManager esté inicializado
if (tsnetSimulationManager == null)
{
Debug.WriteLine("TSNet Auto: tsnetSimulationManager es null, omitiendo ejecución");
return;
}
// Verificar si ya está ejecutándose para evitar concurrencia
if (tsnetSimulationManager.IsRunning)
{
Debug.WriteLine("TSNet Auto: Simulación ya en ejecución, omitiendo");
return;
}
// Contar objetos hidráulicos
var hydraulicObjects = ObjetosSimulables?.Where(obj => obj.GetType().Name.Contains("osHyd")).ToList();
if (hydraulicObjects == null || hydraulicObjects.Count == 0)
{
Debug.WriteLine("TSNet Auto: No hay objetos hidráulicos, omitiendo ejecución");
return;
}
// Resetear y registrar objetos hidráulicos
tsnetSimulationManager.ResetAllCalculatedValues();
@ -1325,18 +1384,43 @@ namespace CtrEditor
// Ejecutar simulación TSNet de forma asíncrona sin bloquear
var result = await tsnetSimulationManager.RunSimulationAsync();
if (result.Success)
if (result != null)
{
Debug.WriteLine($"TSNet Auto: Simulación exitosa con {hydraulicObjects.Count} objetos hidráulicos");
if (result.Success)
{
Debug.WriteLine($"TSNet Auto: Simulación exitosa con {hydraulicObjects.Count} objetos hidráulicos");
}
else
{
Debug.WriteLine($"TSNet Auto: Error en simulación: {result.Message}");
}
}
else
{
Debug.WriteLine($"TSNet Auto: Error en simulación: {result.Message}");
Debug.WriteLine("TSNet Auto: RunSimulationAsync devolvió null");
}
}
catch (NullReferenceException nullEx)
{
Debug.WriteLine($"TSNet Auto: Error de referencia nula: {nullEx.Message}");
Debug.WriteLine($"TSNet Auto: StackTrace: {nullEx.StackTrace}");
// Intentar reinicializar el simulador
try
{
tsnetSimulationManager = new TSNetSimulationManager();
Debug.WriteLine("TSNet Auto: tsnetSimulationManager reinicializado");
}
catch (Exception reinitEx)
{
Debug.WriteLine($"TSNet Auto: Error al reinicializar: {reinitEx.Message}");
}
}
catch (Exception ex)
{
Debug.WriteLine($"TSNet Auto: Excepción: {ex.Message}");
Debug.WriteLine($"TSNet Auto: Excepción general: {ex.Message}");
Debug.WriteLine($"TSNet Auto: Tipo: {ex.GetType().Name}");
Debug.WriteLine($"TSNet Auto: StackTrace: {ex.StackTrace}");
}
}
@ -1965,14 +2049,14 @@ namespace CtrEditor
if (obj is IHydraulicComponent hydraulicComponent && hydraulicComponent.HasHydraulicComponents)
{
// Evitar doble registro - solo registrar si no está ya registrado
if (!hydraulicSimulationManager.HydraulicObjects.Contains(obj))
if (!tsnetSimulationManager.HydraulicObjects.Contains(obj))
{
hydraulicSimulationManager.RegisterHydraulicObject(obj);
Debug.WriteLine($"Objeto hidráulico registrado: {obj.Nombre}");
tsnetSimulationManager.RegisterHydraulicObject(obj);
Debug.WriteLine($"Objeto hidráulico registrado en TSNet: {obj.Nombre}");
}
else
{
Debug.WriteLine($"Objeto hidráulico ya registrado (omitido): {obj.Nombre}");
Debug.WriteLine($"Objeto hidráulico ya registrado en TSNet (omitido): {obj.Nombre}");
}
}
}
@ -1984,8 +2068,8 @@ namespace CtrEditor
{
// 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}");
tsnetSimulationManager.UnregisterHydraulicObject(obj);
Debug.WriteLine($"Objeto desregistrado del simulador hidráulico TSNet: {obj.Nombre}");
}
/// <summary>
@ -1993,8 +2077,8 @@ namespace CtrEditor
/// </summary>
public void ResetHydraulicSimulation()
{
hydraulicSimulationManager.Reset();
Debug.WriteLine("Simulador hidráulico reiniciado");
tsnetSimulationManager.Reset();
Debug.WriteLine("Simulador hidráulico TSNet reiniciado");
}
/// <summary>
@ -2002,7 +2086,7 @@ namespace CtrEditor
/// </summary>
public string GetHydraulicSimulationStats()
{
return hydraulicSimulationManager.GetSimulationStats();
return tsnetSimulationManager.GetSimulationStats();
}
/// <summary>
@ -2010,8 +2094,8 @@ namespace CtrEditor
/// </summary>
public void SetHydraulicSimulationEnabled(bool enabled)
{
hydraulicSimulationManager.IsHydraulicSimulationEnabled = enabled;
Debug.WriteLine($"Simulación hidráulica {(enabled ? "habilitada" : "deshabilitada")}");
tsnetSimulationManager.IsHydraulicSimulationEnabled = enabled;
Debug.WriteLine($"Simulación hidráulica TSNet {(enabled ? "habilitada" : "deshabilitada")}");
}
/// <summary>
@ -2020,7 +2104,7 @@ namespace CtrEditor
private void RegisterLoadedHydraulicObjects()
{
// Limpiar registros previos
hydraulicSimulationManager.ClearHydraulicObjects();
tsnetSimulationManager.ClearHydraulicObjects();
// Crear una lista temporal para evitar duplicados durante la carga
var objectsToRegister = new HashSet<osBase>();
@ -2037,11 +2121,11 @@ namespace CtrEditor
// Registrar los objetos únicos
foreach (var obj in objectsToRegister)
{
hydraulicSimulationManager.RegisterHydraulicObject(obj);
Debug.WriteLine($"Objeto hidráulico registrado en carga: {obj.Nombre}");
tsnetSimulationManager.RegisterHydraulicObject(obj);
Debug.WriteLine($"Objeto hidráulico registrado en carga TSNet: {obj.Nombre}");
}
Debug.WriteLine($"Registrados {hydraulicSimulationManager.HydraulicObjects.Count} objetos hidráulicos únicos tras cargar proyecto");
Debug.WriteLine($"Registrados {tsnetSimulationManager.HydraulicObjects.Count} objetos hidráulicos únicos tras cargar proyecto en TSNet");
}
#endregion
@ -2055,10 +2139,10 @@ namespace CtrEditor
{
try
{
if (hydraulicSimulationManager != null)
if (tsnetSimulationManager != null)
{
hydraulicSimulationManager.InvalidateNetwork();
Debug.WriteLine("Red hidráulica invalidada y marcada para recálculo");
tsnetSimulationManager.InvalidateNetwork();
Debug.WriteLine("Red hidráulica TSNet invalidada y marcada para recálculo");
}
}
catch (Exception ex)
@ -2333,6 +2417,41 @@ namespace CtrEditor
}
}
/// <summary>
/// Método de debugging para generar un reporte detallado de IOException y estado del sistema
/// </summary>
public void GenerateIOExceptionReport()
{
try
{
var finalTimestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
Debug.WriteLine($"\n=== REPORTE DE ESTADO ({finalTimestamp}) ===");
Debug.WriteLine($"Simulación ejecutándose: {IsSimulationRunning}");
Debug.WriteLine($"Timer Simulación activo: {_timerSimulacion?.Enabled}");
Debug.WriteLine($"Timer TSNet activo: {_timerTSNet?.Enabled}");
Debug.WriteLine($"Timer Display activo: {_timerDisplayUpdate?.IsEnabled}");
Debug.WriteLine($"Timer 3D activo: {_timer3DUpdate?.IsEnabled}");
Debug.WriteLine($"TSNet Manager inicializado: {tsnetSimulationManager != null}");
if (tsnetSimulationManager != null)
{
Debug.WriteLine($"TSNet ejecutándose: {tsnetSimulationManager.IsRunning}");
}
Debug.WriteLine($"Objetos simulables: {ObjetosSimulables?.Count ?? 0}");
var hydraulicObjects = ObjetosSimulables?.Where(obj =>
obj.GetType().Name.Contains("osHyd"))?.ToList();
Debug.WriteLine($"Objetos hidráulicos: {hydraulicObjects?.Count ?? 0}");
Debug.WriteLine($"=== FIN REPORTE ===\n");
}
catch (Exception ex)
{
Debug.WriteLine($"Error en GenerateIOExceptionReport: {ex.Message}");
}
}
#endregion
}
@ -2356,7 +2475,6 @@ namespace CtrEditor
[System.Obsolete("Use ImageDataDictionary instead")]
public Dictionary<string, string>? ImageCustomNames { get; set; }
}
public class TipoSimulable

View File

@ -346,16 +346,40 @@ namespace CtrEditor.ObjetosSim
// Verificar que la tubería esté conectada a dos componentes
if (!string.IsNullOrEmpty(Id_ComponenteA) && !string.IsNullOrEmpty(Id_ComponenteB))
{
// Crear elemento pipe con propiedades correctas
var pipeElement = new Pipe(Length, Diameter, Roughness);
elements.Add(new HydraulicElementDefinition(
$"PIPE_{Nombre}",
Id_ComponenteA,
Id_ComponenteB,
pipeElement,
$"Tubería - L:{Length:F1}m, D:{Diameter*1000:F0}mm, Rough:{Roughness*1000:F2}mm"
));
try
{
// Resolver los nombres de los componentes conectados
var nodeNameA = ResolveComponentNodeName(Id_ComponenteA);
var nodeNameB = ResolveComponentNodeName(Id_ComponenteB);
if (!string.IsNullOrEmpty(nodeNameA) && !string.IsNullOrEmpty(nodeNameB))
{
// Crear elemento pipe con propiedades correctas
var pipeElement = new Pipe(Length, Diameter, Roughness);
elements.Add(new HydraulicElementDefinition(
$"PIPE_{Nombre}",
nodeNameA,
nodeNameB,
pipeElement,
$"Tubería - L:{Length:F1}m, D:{Diameter*1000:F0}mm, Rough:{Roughness*1000:F2}mm"
));
if (VerboseOutput)
{
Debug.WriteLine($"Pipe {Nombre}: Conectando nodos '{nodeNameA}' -> '{nodeNameB}'");
}
}
else
{
Debug.WriteLine($"Warning: Pipe {Nombre} no pudo resolver nombres de nodos. " +
$"ComponenteA='{Id_ComponenteA}', ComponenteB='{Id_ComponenteB}'");
}
}
catch (Exception ex)
{
Debug.WriteLine($"Error en GetHydraulicElements para pipe {Nombre}: {ex.Message}");
}
}
return elements;
@ -371,6 +395,209 @@ namespace CtrEditor.ObjetosSim
}
}
/// <summary>
/// Resuelve el ID de un componente al nombre del nodo hidráulico correspondiente
/// THREAD-SAFE: No accede a objetos WPF desde background threads
/// </summary>
private string ResolveComponentNodeName(string componentId)
{
try
{
// Debug: Log the original componentId
Debug.WriteLine($"Pipe {Nombre}: Resolviendo componentId '{componentId}'");
// Primero intentar usar el ID directamente como nombre (para compatibilidad)
if (!string.IsNullOrEmpty(componentId))
{
// Para evitar problemas de threading, usar el Dispatcher para acceder a objetos UI
MainViewModel mainViewModel = null;
// Solo acceder a la UI desde el thread principal
if (Application.Current?.Dispatcher?.CheckAccess() == true)
{
mainViewModel = Application.Current?.MainWindow?.DataContext as MainViewModel;
}
else
{
// Si estamos en un background thread, usar Invoke para acceder de forma segura
Application.Current?.Dispatcher?.Invoke(() =>
{
mainViewModel = Application.Current?.MainWindow?.DataContext as MainViewModel;
});
}
if (mainViewModel?.ObjetosSimulables != null)
{
List<osBase> hydraulicComponents = null;
// Obtener componentes de forma thread-safe
if (Application.Current?.Dispatcher?.CheckAccess() == true)
{
hydraulicComponents = mainViewModel.ObjetosSimulables
.OfType<IHydraulicComponent>()
.Where(comp => comp is osBase)
.Cast<osBase>()
.ToList();
}
else
{
Application.Current?.Dispatcher?.Invoke(() =>
{
hydraulicComponents = mainViewModel.ObjetosSimulables
.OfType<IHydraulicComponent>()
.Where(comp => comp is osBase)
.Cast<osBase>()
.ToList();
});
}
if (hydraulicComponents != null)
{
// Debug: Log available components
Debug.WriteLine($"Pipe {Nombre}: Componentes hidráulicos disponibles:");
foreach (var comp in hydraulicComponents)
{
Debug.WriteLine($" - Nombre: '{comp.Nombre}', ID: '{comp.Id?.Value}'");
}
// Buscar por nombre primero (caso más común)
var componentByName = hydraulicComponents
.FirstOrDefault(comp => comp.Nombre == componentId);
if (componentByName != null)
{
Debug.WriteLine($"Pipe {Nombre}: Encontrado componente por nombre: '{componentByName.Nombre}'");
// Verificar si es una bomba (requiere resolución especial de nodos)
if (componentByName is osHydPump)
{
var sanitizedName = SanitizeNodeName(componentByName.Nombre);
// Determinar si esta tubería está conectada a la entrada o salida de la bomba
// Basándonos en si la bomba es origen (ComponenteA) o destino (ComponenteB) de esta tubería
if (componentId == Id_ComponenteA)
{
// La bomba es ComponenteA (origen de esta tubería)
// Por lo tanto, conectamos desde la salida de la bomba (NODE_B)
Debug.WriteLine($"Pipe {Nombre}: Bomba como origen - conectando desde salida: NODE_B_{sanitizedName}");
return $"NODE_B_{sanitizedName}";
}
else if (componentId == Id_ComponenteB)
{
// La bomba es ComponenteB (destino de esta tubería)
// Por lo tanto, conectamos hacia la entrada de la bomba (NODE_A)
Debug.WriteLine($"Pipe {Nombre}: Bomba como destino - conectando hacia entrada: NODE_A_{sanitizedName}");
return $"NODE_A_{sanitizedName}";
}
else
{
// Fallback: usar salida por defecto
Debug.WriteLine($"Pipe {Nombre}: Usando salida por defecto de bomba: NODE_B_{sanitizedName}");
return $"NODE_B_{sanitizedName}";
}
}
else
{
// Para tanques y otras componentes, usar el nombre sanitizado
return SanitizeNodeName(componentByName.Nombre);
}
}
// Si no se encuentra por nombre, buscar por ID único (convertir string a int)
if (int.TryParse(componentId, out int idValue))
{
var componentById = hydraulicComponents
.FirstOrDefault(comp => comp.Id?.Value == idValue);
if (componentById != null)
{
Debug.WriteLine($"Pipe {Nombre}: Encontrado componente por ID {idValue}: '{componentById.Nombre}'");
// Verificar si es una bomba (requiere resolución especial de nodos)
if (componentById is osHydPump)
{
var sanitizedName = SanitizeNodeName(componentById.Nombre);
// Determinar si esta tubería está conectada a la entrada o salida de la bomba
if (componentId == Id_ComponenteA)
{
Debug.WriteLine($"Pipe {Nombre}: Bomba como origen (ID) - conectando desde salida: NODE_B_{sanitizedName}");
return $"NODE_B_{sanitizedName}";
}
else if (componentId == Id_ComponenteB)
{
Debug.WriteLine($"Pipe {Nombre}: Bomba como destino (ID) - conectando hacia entrada: NODE_A_{sanitizedName}");
return $"NODE_A_{sanitizedName}";
}
else
{
Debug.WriteLine($"Pipe {Nombre}: Usando salida por defecto de bomba (ID): NODE_B_{sanitizedName}");
return $"NODE_B_{sanitizedName}";
}
}
else
{
return SanitizeNodeName(componentById.Nombre);
}
}
}
}
// Último intento: usar el componentId directamente si es un nombre válido
Debug.WriteLine($"Pipe {Nombre}: No se encontró componente, usando '{componentId}' directamente");
return SanitizeNodeName(componentId);
}
else
{
Debug.WriteLine($"Pipe {Nombre}: MainViewModel o ObjetosSimulables es null");
}
}
else
{
Debug.WriteLine($"Pipe {Nombre}: componentId está vacío");
}
}
catch (Exception ex)
{
Debug.WriteLine($"Error resolviendo componente '{componentId}': {ex.Message}");
}
return string.Empty;
}
/// <summary>
/// Sanitiza nombres de nodos para compatibilidad con EPANET INP
/// </summary>
private string SanitizeNodeName(string nodeName)
{
if (string.IsNullOrEmpty(nodeName))
return string.Empty;
// Reemplazar espacios y caracteres especiales con guiones bajos
var sanitized = nodeName
.Replace(" ", "_")
.Replace("á", "a")
.Replace("é", "e")
.Replace("í", "i")
.Replace("ó", "o")
.Replace("ú", "u")
.Replace("ü", "u")
.Replace("ñ", "n")
.Replace("Á", "A")
.Replace("É", "E")
.Replace("Í", "I")
.Replace("Ó", "O")
.Replace("Ú", "U")
.Replace("Ü", "U")
.Replace("Ñ", "N");
// Remover caracteres no válidos para EPANET
var validChars = sanitized.Where(c => char.IsLetterOrDigit(c) || c == '_' || c == '-').ToArray();
return new string(validChars);
}
private bool VerboseOutput => true; // Habilitar salida detallada para debugging
// Implementación de IHydraulicFlowReceiver
public void SetFlow(double flow)
{

View File

@ -275,7 +275,7 @@ namespace CtrEditor.ObjetosSim
if (IsRunning && HasFlow)
ImageSource_oculta = ImageFromPath("/imagenes/pump_run.png");
else if (IsRunning)
ImageSource_oculta = ImageFromPath("/imagenes/pump_idle.png");
ImageSource_oculta = ImageFromPath("/imagenes/pump_run.png"); // Usar pump_run como fallback para idle
else
ImageSource_oculta = ImageFromPath("/imagenes/pump_stop.png");
}
@ -324,16 +324,75 @@ namespace CtrEditor.ObjetosSim
public List<HydraulicNodeDefinition> GetHydraulicNodes()
{
// Las bombas no crean nodos propios, se conectan entre nodos existentes
return new List<HydraulicNodeDefinition>();
var nodes = new List<HydraulicNodeDefinition>();
// Sanitizar el nombre para compatibilidad con EPANET
var sanitizedName = SanitizeNodeName(Nombre);
// Crear nodos de entrada y salida para la bomba
nodes.Add(new HydraulicNodeDefinition(
$"NODE_A_{sanitizedName}",
false,
null,
$"Nodo entrada bomba {sanitizedName}"));
nodes.Add(new HydraulicNodeDefinition(
$"NODE_B_{sanitizedName}",
false,
null,
$"Nodo salida bomba {sanitizedName}"));
return nodes;
}
/// <summary>
/// Sanitiza nombres de nodos para compatibilidad con EPANET INP
/// </summary>
private string SanitizeNodeName(string nodeName)
{
if (string.IsNullOrEmpty(nodeName))
return "UnknownPump";
// Reemplazar espacios y caracteres especiales con guiones bajos
var sanitized = nodeName
.Replace(" ", "_")
.Replace("á", "a")
.Replace("é", "e")
.Replace("í", "i")
.Replace("ó", "o")
.Replace("ú", "u")
.Replace("ü", "u")
.Replace("ñ", "n")
.Replace("Á", "A")
.Replace("É", "E")
.Replace("Í", "I")
.Replace("Ó", "O")
.Replace("Ú", "U")
.Replace("Ü", "U")
.Replace("Ñ", "N");
// Remover caracteres no válidos para EPANET
var validChars = sanitized.Where(c => char.IsLetterOrDigit(c) || c == '_' || c == '-').ToArray();
var result = new string(validChars);
// Asegurar que empiece con una letra
if (!string.IsNullOrEmpty(result) && !char.IsLetter(result[0]))
{
result = "Pump_" + result;
}
return string.IsNullOrEmpty(result) ? "UnknownPump" : result;
}
public void ApplyHydraulicResults(Dictionary<string, double> flows, Dictionary<string, double> pressures)
{
try
{
// Sanitizar el nombre para compatibilidad con EPANET
var sanitizedName = SanitizeNodeName(Nombre);
// Buscar resultados de esta bomba en TSNet
var pumpElementName = $"PUMP_{Nombre}";
var pumpElementName = $"PUMP_{sanitizedName}";
if (flows.ContainsKey(pumpElementName))
{
var pumpFlow = flows[pumpElementName];
@ -365,13 +424,16 @@ namespace CtrEditor.ObjetosSim
if (IsRunning)
{
// Sanitizar el nombre para compatibilidad con EPANET
var sanitizedName = SanitizeNodeName(Nombre);
// Crear elemento de bomba para TSNet
var pumpElement = new PumpHQ(PumpHead, MaxFlow);
elements.Add(new HydraulicElementDefinition(
$"PUMP_{Nombre}",
$"NODE_A_{Nombre}",
$"NODE_B_{Nombre}",
$"PUMP_{sanitizedName}",
$"NODE_A_{sanitizedName}",
$"NODE_B_{sanitizedName}",
pumpElement,
$"Bomba hidráulica - Head: {PumpHead:F1}m, Flow: {MaxFlow * 3600:F1} m³/h"
));

View File

@ -432,19 +432,63 @@ namespace CtrEditor.ObjetosSim
public List<HydraulicNodeDefinition> GetHydraulicNodes()
{
var nodes = new List<HydraulicNodeDefinition>();
// Sanitizar el nombre para compatibilidad con EPANET
var sanitizedName = SanitizeNodeName(Nombre);
// Tanque como nodo libre por defecto
nodes.Add(new HydraulicNodeDefinition(Nombre, false, null, $"Tanque hidráulico - {FluidDescription}"));
nodes.Add(new HydraulicNodeDefinition(sanitizedName, false, null, $"Tanque hidráulico - {FluidDescription}"));
return nodes;
}
/// <summary>
/// Sanitiza nombres de nodos para compatibilidad con EPANET INP
/// </summary>
private string SanitizeNodeName(string nodeName)
{
if (string.IsNullOrEmpty(nodeName))
return "UnknownTank";
// Reemplazar espacios y caracteres especiales con guiones bajos
var sanitized = nodeName
.Replace(" ", "_")
.Replace("á", "a")
.Replace("é", "e")
.Replace("í", "i")
.Replace("ó", "o")
.Replace("ú", "u")
.Replace("ü", "u")
.Replace("ñ", "n")
.Replace("Á", "A")
.Replace("É", "E")
.Replace("Í", "I")
.Replace("Ó", "O")
.Replace("Ú", "U")
.Replace("Ü", "U")
.Replace("Ñ", "N");
// Remover caracteres no válidos para EPANET
var validChars = sanitized.Where(c => char.IsLetterOrDigit(c) || c == '_' || c == '-').ToArray();
var result = new string(validChars);
// Asegurar que empiece con una letra
if (!string.IsNullOrEmpty(result) && !char.IsLetter(result[0]))
{
result = "Tank_" + result;
}
return string.IsNullOrEmpty(result) ? "UnknownTank" : result;
}
public void ApplyHydraulicResults(Dictionary<string, double> flows, Dictionary<string, double> pressures)
{
try
{
// Sanitizar el nombre para compatibilidad con EPANET
var sanitizedName = SanitizeNodeName(Nombre);
// Actualizar presión desde TSNet
if (pressures.ContainsKey(Nombre))
if (pressures.ContainsKey(sanitizedName))
{
var pressurePa = pressures[Nombre];
var pressurePa = pressures[sanitizedName];
CurrentPressure = pressurePa / 100000.0; // Convertir Pa a bar
}

View File

@ -1,6 +1,7 @@
using Newtonsoft.Json;
using System.Windows.Controls;
using CtrEditor.HydraulicSimulator;
using CtrEditor.HydraulicSimulator.TSNet;
using CtrEditor.Simulacion;
using System.Reflection;
using System;
@ -90,6 +91,19 @@ namespace CtrEditor.ObjetosSim
}
}
// Sobrecarga para TSNetSimulationManager - simplemente no asigna el hydraulicSimulationManager
public static void AssignDatos(UserControl userControl, osBase datos, SimulationManagerBEPU simulationManager, TSNetSimulationManager tsnetSimulationManager)
{
if (userControl is IDataContainer dataContainer)
{
dataContainer.Datos = datos;
userControl.DataContext = datos;
datos.VisualRepresentation = userControl;
datos.simulationManager = simulationManager;
// No asignamos hydraulicSimulationManager para TSNet - se maneja de manera diferente
}
}
public static void LimpiarPropiedadesosDatos(PropertyGrid propertyGrid)
{
// Forzar la actualización de bindings pendientes antes de limpiar