462 lines
20 KiB
C#
462 lines
20 KiB
C#
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
|
||
}
|
||
}
|