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 { /// /// Generador de archivos INP para TSNet basado en la red hidráulica /// Convierte el modelo interno a formato EPANET INP compatible con TSNet /// 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 } /// /// Genera el archivo INP completo /// 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 var tankAdapter = _simulationManager?.GetTankAdapterByNodeName(node.Name); if (tankAdapter?.Configuration != null) { // Usar configuración real del tanque var config = tankAdapter.Configuration; // Debug: Log de generación INP System.Diagnostics.Debug.WriteLine($"INPGenerator: Generando tanque {node.Name}"); System.Diagnostics.Debug.WriteLine($" config.InitialLevelM: {config.InitialLevelM}"); System.Diagnostics.Debug.WriteLine($" config.MaxLevelM: {config.MaxLevelM}"); System.Diagnostics.Debug.WriteLine($" 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 System.Diagnostics.Debug.WriteLine($"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()) { 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()) { 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()) { 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()) { 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 } /// /// Sanitiza nombres de nodos para compatibilidad con EPANET INP /// 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 } }