CtrEditor/HydraulicSimulator/TSNet/TSNetINPGenerator.cs

462 lines
20 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

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

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using HydraulicSimulator.Models;
namespace CtrEditor.HydraulicSimulator.TSNet
{
/// <summary>
/// Generador de archivos INP para TSNet basado en la red hidráulica
/// Convierte el modelo interno a formato EPANET INP compatible con TSNet
/// </summary>
public class TSNetINPGenerator
{
private readonly HydraulicNetwork _network;
private readonly TSNetConfiguration _config;
private readonly TSNetSimulationManager _simulationManager;
public TSNetINPGenerator(HydraulicNetwork network, TSNetConfiguration config, TSNetSimulationManager simulationManager = null)
{
_network = network ?? throw new ArgumentNullException(nameof(network));
_config = config ?? throw new ArgumentNullException(nameof(config));
_simulationManager = simulationManager; // Puede ser null para compatibilidad hacia atrás
}
/// <summary>
/// Genera el archivo INP completo
/// </summary>
public async Task GenerateAsync(string filePath)
{
var content = new StringBuilder();
// Header
content.AppendLine("[TITLE]");
content.AppendLine("TSNet Hydraulic Network");
content.AppendLine($"Generated on {DateTime.Now}");
content.AppendLine("CtrEditor TSNet Integration");
content.AppendLine();
// Junctions (nodos libres)
GenerateJunctions(content);
// Reservoirs (nodos de presión fija)
GenerateReservoirs(content);
// Tanks (tanques)
GenerateTanks(content);
// Pipes (tuberías)
GeneratePipes(content);
// Pumps (bombas)
GeneratePumps(content);
// Valves (válvulas)
GenerateValves(content);
// Patterns (patrones de demanda)
GeneratePatterns(content);
// Curves (curvas de bombas)
GenerateCurves(content);
// Quality
GenerateQuality(content);
// Options
GenerateOptions(content);
// Times
GenerateTimes(content);
// Coordinates (opcional, para visualización)
GenerateCoordinates(content);
// End
content.AppendLine("[END]");
// Escribir archivo
await File.WriteAllTextAsync(filePath, content.ToString());
}
private void GenerateJunctions(StringBuilder content)
{
content.AppendLine("[JUNCTIONS]");
content.AppendLine(";ID \tElev \tDemand \tPattern ");
foreach (var node in _network.Nodes.Values.Where(n => !n.FixedP && !IsTank(n)))
{
var elevation = GetNodeElevation(node);
var demand = GetNodeDemand(node);
var sanitizedName = SanitizeNodeName(node.Name);
content.AppendLine($" {sanitizedName,-15}\t{elevation.ToString("F2", CultureInfo.InvariantCulture)} \t{demand.ToString("F2", CultureInfo.InvariantCulture)} \t;");
}
content.AppendLine();
}
private void GenerateReservoirs(StringBuilder content)
{
content.AppendLine("[RESERVOIRS]");
content.AppendLine(";ID \tHead \tPattern ");
foreach (var node in _network.Nodes.Values.Where(n => n.FixedP && !IsTank(n)))
{
var head = PressureToHead(node.P);
var sanitizedName = SanitizeNodeName(node.Name);
content.AppendLine($" {sanitizedName,-15}\t{head.ToString("F2", CultureInfo.InvariantCulture)} \t;");
}
content.AppendLine();
}
private void GenerateTanks(StringBuilder content)
{
content.AppendLine("[TANKS]");
content.AppendLine(";ID \tElevation \tInitLevel \tMinLevel \tMaxLevel \tDiameter \tMinVol \tVolCurve");
// Usar configuración real de los TSNetTankAdapter en lugar de valores dummy
var tankNodes = _network.Nodes.Values.Where(IsTank);
foreach (var node in tankNodes)
{
var elevation = GetNodeElevation(node);
var sanitizedName = SanitizeNodeName(node.Name);
// Buscar el adapter correspondiente en el simulation manager usando el nombre original (sin sanitizar)
LogToMCP($"INPGenerator: Buscando adaptador para nodo '{node.Name}'");
var tankAdapter = _simulationManager?.GetTankAdapterByNodeName(node.Name);
// Debug logging para diagnóstico
if (_simulationManager != null)
{
LogToMCP($"INPGenerator: SimulationManager disponible, buscando adaptador...");
if (tankAdapter == null)
{
LogToMCP($"INPGenerator: No se encontró adaptador para '{node.Name}'");
// Listar adaptadores disponibles para diagnóstico
LogToMCP($"INPGenerator: Adaptadores disponibles:");
var availableAdapters = _simulationManager.GetType().GetField("_tankAdapters",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
if (availableAdapters?.GetValue(_simulationManager) is System.Collections.IDictionary adapters)
{
foreach (System.Collections.DictionaryEntry entry in adapters)
{
var adapter = entry.Value;
var tankName = adapter?.GetType().GetProperty("Tank")?.GetValue(adapter)?.GetType().GetProperty("Nombre")?.GetValue(adapter?.GetType().GetProperty("Tank")?.GetValue(adapter));
LogToMCP($" - '{tankName}' (ID: {entry.Key})");
}
}
}
else
{
LogToMCP($"INPGenerator: ✅ Adaptador encontrado para '{node.Name}'");
}
}
else
{
LogToMCP($"INPGenerator: ❌ SimulationManager es null");
}
if (tankAdapter?.Configuration != null)
{
// Usar configuración real del tanque
var config = tankAdapter.Configuration;
// Debug: Log de generación INP
LogToMCP($"INPGenerator: Generando tanque {node.Name}");
LogToMCP($" config.InitialLevelM: {config.InitialLevelM}");
LogToMCP($" config.MaxLevelM: {config.MaxLevelM}");
LogToMCP($" config.DiameterM: {config.DiameterM}");
content.AppendLine($" {sanitizedName,-15}\t{elevation.ToString("F2", CultureInfo.InvariantCulture)} \t{config.InitialLevelM.ToString("F2", CultureInfo.InvariantCulture)} \t{config.MinLevelM.ToString("F2", CultureInfo.InvariantCulture)} \t{config.MaxLevelM.ToString("F2", CultureInfo.InvariantCulture)} \t{config.DiameterM.ToString("F2", CultureInfo.InvariantCulture)} \t0 \t");
}
else
{
// Fallback a valores por defecto si no hay configuración
LogToMCP($"INPGenerator: WARNING - No se encontró configuración para tanque {node.Name}, usando valores por defecto");
content.AppendLine($" {sanitizedName,-15}\t{elevation.ToString("F2", CultureInfo.InvariantCulture)} \t1.0 \t0.0 \t2.0 \t1.0 \t0 \t");
}
}
content.AppendLine();
}
private void GeneratePipes(StringBuilder content)
{
content.AppendLine("[PIPES]");
content.AppendLine(";ID \tNode1 \tNode2 \tLength \tDiameter \tRoughness \tMinorLoss \tStatus");
int pipeId = 1;
foreach (var branch in _network.Branches)
{
foreach (var element in branch.Elements.OfType<Pipe>())
{
var id = $"PIPE{pipeId++}";
var length = element.L; // Usar L en lugar de Length
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
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");
}
}
content.AppendLine();
}
private void GeneratePumps(StringBuilder content)
{
content.AppendLine("[PUMPS]");
content.AppendLine(";ID \tNode1 \tNode2 \tParameters");
int pumpId = 1;
foreach (var branch in _network.Branches)
{
foreach (var element in branch.Elements.OfType<PumpHQ>())
{
var id = $"PUMP{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++;
}
}
content.AppendLine();
}
private void GenerateValves(StringBuilder content)
{
content.AppendLine("[VALVES]");
content.AppendLine(";ID \tNode1 \tNode2 \tDiameter \tType\tSetting \tMinorLoss ");
int valveId = 1;
foreach (var branch in _network.Branches)
{
foreach (var element in branch.Elements.OfType<ValveKv>())
{
var id = $"VALVE{valveId++}";
var diameter = EstimateDiameter(element);
content.AppendLine($" {id,-15}\t{branch.N1,-15}\t{branch.N2,-15}\t{diameter:F1} \tFCV \t{element.KvFull:F2} \t0 ");
}
}
content.AppendLine();
}
private void GeneratePatterns(StringBuilder content)
{
content.AppendLine("[PATTERNS]");
content.AppendLine(";ID \tMultipliers");
content.AppendLine(" 1 \t1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0");
content.AppendLine(" 1 \t1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0");
content.AppendLine(" 1 \t1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0");
content.AppendLine();
}
private void GenerateCurves(StringBuilder content)
{
content.AppendLine("[CURVES]");
content.AppendLine(";ID \tX-Value \tY-Value");
// Generar curvas para bombas
int curveId = 1;
foreach (var branch in _network.Branches)
{
foreach (var element in branch.Elements.OfType<PumpHQ>())
{
content.AppendLine($";PUMP CURVE {curveId}");
// Puntos de la curva H-Q (simplificada)
var maxHead = element.H0;
var maxFlow = element.H0 / 10; // Estimación simple
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++;
}
}
content.AppendLine();
}
private void GenerateQuality(StringBuilder content)
{
content.AppendLine("[QUALITY]");
content.AppendLine(";Node \tInitQual");
foreach (var node in _network.Nodes.Values)
{
var sanitizedName = SanitizeNodeName(node.Name);
content.AppendLine($" {sanitizedName,-15}\t0.0");
}
content.AppendLine();
}
private void GenerateOptions(StringBuilder content)
{
content.AppendLine("[OPTIONS]");
content.AppendLine(" Units \tLPS"); // Litros por segundo
content.AppendLine(" Headloss \tD-W"); // Darcy-Weisbach
content.AppendLine(" Specific Gravity\t1.0");
content.AppendLine($" Viscosity \t{_network.Fluid.Mu.ToString("E2", CultureInfo.InvariantCulture)}");
content.AppendLine(" Trials \t40");
content.AppendLine(" Accuracy \t0.001");
content.AppendLine(" CHECKFREQ \t2");
content.AppendLine(" MAXCHECK \t10");
content.AppendLine(" DAMPLIMIT \t0");
content.AppendLine(" Unbalanced \tContinue 10");
content.AppendLine(" Pattern \t1");
content.AppendLine(" Demand Multiplier\t1.0");
content.AppendLine(" Emitter Exponent\t0.5");
content.AppendLine(" Quality \tNone mg/L");
content.AppendLine(" Diffusivity \t1.0");
content.AppendLine(" Tolerance \t0.01");
content.AppendLine();
}
private void GenerateTimes(StringBuilder content)
{
content.AppendLine("[TIMES]");
// Convert duration from seconds to H:MM:SS format
var durationTime = TimeSpan.FromSeconds(_config.Duration);
var durationStr = $"{(int)durationTime.TotalHours}:{durationTime.Minutes:00}:{durationTime.Seconds:00}";
// Convert timestep from seconds to H:MM:SS format
var timestepTime = TimeSpan.FromSeconds(_config.TimeStep);
var timestepStr = $"{(int)timestepTime.TotalHours}:{timestepTime.Minutes:00}:{timestepTime.Seconds:00}";
content.AppendLine($" Duration \t{durationStr}");
content.AppendLine($" Hydraulic Timestep\t{timestepStr}");
content.AppendLine(" Quality Timestep\t0:05:00");
content.AppendLine(" Pattern Timestep\t1:00:00");
content.AppendLine(" Pattern Start \t0:00:00");
content.AppendLine(" Report Timestep \t1:00:00");
content.AppendLine(" Report Start \t0:00:00");
content.AppendLine(" Start ClockTime \t12:00:00 AM");
content.AppendLine(" Statistic \tNone");
content.AppendLine();
}
private void GenerateCoordinates(StringBuilder content)
{
content.AppendLine("[COORDINATES]");
content.AppendLine(";Node \tX-Coord \tY-Coord");
// Generar coordenadas simples en grid
int x = 0, y = 0;
foreach (var node in _network.Nodes.Values)
{
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)
{
x = 0;
y += 1000;
}
}
content.AppendLine();
}
#region Helper Methods
private bool IsTank(Node node)
{
// Determinar si un nodo es un tanque basado en su nombre
return node.Name.ToLower().Contains("tank") || node.Name.ToLower().Contains("tanque");
}
private double GetNodeElevation(Node node)
{
// Por defecto usar elevación de 0, será mejorado con datos reales
return 0.0;
}
private double GetNodeDemand(Node node)
{
// Por defecto sin demanda, será mejorado con datos reales
return 0.0;
}
private double PressureToHead(double pressurePa)
{
// Convertir presión en Pa a altura en metros
// H = P / (ρ * g)
var density = _network.Fluid.Rho; // Usar Rho en lugar de Density
var gravity = 9.81;
return pressurePa / (density * gravity);
}
private double EstimateDiameter(ValveKv valve)
{
// Estimación del diámetro basado en Kv
// Kv = 0.865 * A * sqrt(2*g*H) donde A = π*D²/4
// Esta es una aproximación, se mejorará con datos reales
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;
}
/// <summary>
/// Método de logging simple para debug - usa Console.WriteLine por ahora
/// </summary>
private static void LogToMCP(string message)
{
// Usar Console.WriteLine para debug que es capturado por el sistema de logging
Console.WriteLine($"[DEBUG TSNetINPGenerator] {message}");
}
#endregion
}
}