CtrEditor/HydraulicSimulator/TSNet/TSNetINPGenerator.cs

397 lines
16 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;
public TSNetINPGenerator(HydraulicNetwork network, TSNetConfiguration config)
{
_network = network ?? throw new ArgumentNullException(nameof(network));
_config = config ?? throw new ArgumentNullException(nameof(config));
}
/// <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");
// Por ahora generar tanques dummy - será expandido cuando integremos osHydTank
var tankNodes = _network.Nodes.Values.Where(IsTank);
foreach (var node in tankNodes)
{
var elevation = GetNodeElevation(node);
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();
}
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;
}
#endregion
}
}