CtrEditor/HydraulicSimulator/Python/PythonInterop.cs

631 lines
26 KiB
C#

using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
namespace CtrEditor.HydraulicSimulator.Python
{
/// <summary>
/// Interoperabilidad con Python usando CPython embebido
/// Permite ejecutar scripts de TSNet desde C#
/// </summary>
public static class PythonInterop
{
#region Python DLL Imports
[DllImport("python312.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern int Py_Initialize();
[DllImport("python312.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern int Py_Finalize();
[DllImport("python312.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern int PyRun_SimpleString(string command);
[DllImport("python312.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern IntPtr PyRun_String(string str, int start, IntPtr globals, IntPtr locals);
[DllImport("python312.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern IntPtr PyImport_ImportModule(string name);
[DllImport("python312.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern int Py_IsInitialized();
#endregion
#region Configuration
/// <summary>
/// Ruta base de la instalación de Python
/// </summary>
public static string PythonBasePath { get; set; } = @"D:\Proyectos\VisualStudio\CtrEditor\bin\Debug\net8.0-windows8.0\tsnet";
/// <summary>
/// Ruta al ejecutable de Python
/// </summary>
public static string PythonExecutable => Path.Combine(PythonBasePath, "python.exe");
/// <summary>
/// Ruta a la DLL de Python
/// </summary>
public static string PythonDll => Path.Combine(PythonBasePath, "python312.dll");
/// <summary>
/// Indica si Python está inicializado
/// </summary>
public static bool IsInitialized { get; private set; } = false;
#endregion
#region Initialization
/// <summary>
/// Inicializa el intérprete de Python embebido
/// </summary>
public static bool Initialize()
{
try
{
if (IsInitialized)
return true;
// Verificar que los archivos existan
if (!File.Exists(PythonDll))
{
Debug.WriteLine($"Error: No se encuentra la DLL de Python en: {PythonDll}");
return false;
}
// Configurar el path de Python
Environment.SetEnvironmentVariable("PYTHONPATH",
$"{PythonBasePath};{Path.Combine(PythonBasePath, "Lib")};{Path.Combine(PythonBasePath, "site-packages")}");
// Inicializar Python
var result = Py_Initialize();
IsInitialized = Py_IsInitialized() != 0;
if (IsInitialized)
{
// Configurar paths de Python
var pathSetup = $@"
import sys
sys.path.insert(0, r'{PythonBasePath}')
sys.path.insert(0, r'{Path.Combine(PythonBasePath, "Lib")}')
sys.path.insert(0, r'{Path.Combine(PythonBasePath, "site-packages")}')
";
PyRun_SimpleString(pathSetup);
Debug.WriteLine("Python inicializado correctamente");
Debug.WriteLine($"PYTHONPATH: {Environment.GetEnvironmentVariable("PYTHONPATH")}");
}
else
{
Debug.WriteLine("Error al inicializar Python");
}
return IsInitialized;
}
catch (Exception ex)
{
Debug.WriteLine($"Error al inicializar Python: {ex.Message}");
return false;
}
}
/// <summary>
/// Finaliza el intérprete de Python
/// </summary>
public static void Finalize()
{
try
{
if (IsInitialized)
{
Py_Finalize();
IsInitialized = false;
Debug.WriteLine("Python finalizado");
}
}
catch (Exception ex)
{
Debug.WriteLine($"Error al finalizar Python: {ex.Message}");
}
}
#endregion
#region Script Execution
/// <summary>
/// Ejecuta un comando Python simple
/// </summary>
public static bool ExecuteCommand(string command)
{
try
{
if (!IsInitialized && !Initialize())
return false;
var result = PyRun_SimpleString(command);
return result == 0;
}
catch (Exception ex)
{
Debug.WriteLine($"Error ejecutando comando Python: {ex.Message}");
return false;
}
}
/// <summary>
/// Ejecuta un script Python desde archivo
/// </summary>
public static bool ExecuteScript(string scriptPath)
{
try
{
if (!File.Exists(scriptPath))
{
Debug.WriteLine($"Error: Script no encontrado: {scriptPath}");
return false;
}
var scriptContent = File.ReadAllText(scriptPath);
return ExecuteCommand(scriptContent);
}
catch (Exception ex)
{
Debug.WriteLine($"Error ejecutando script: {ex.Message}");
return false;
}
}
/// <summary>
/// Ejecuta un script Python con argumentos usando subprocess
/// Útil para scripts más complejos o cuando necesitamos capturar output
/// </summary>
public static async Task<PythonExecutionResult> ExecuteScriptAsync(string scriptPath, string arguments = "")
{
try
{
// Execute Python directly without cmd wrapper to avoid quoting issues
var pythonExe = Path.Combine(PythonBasePath, "python.exe");
var startInfo = new ProcessStartInfo
{
FileName = pythonExe,
Arguments = $"-u \"{scriptPath}\" {arguments}",
WorkingDirectory = PythonBasePath,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
StandardOutputEncoding = Encoding.UTF8,
StandardErrorEncoding = Encoding.UTF8
};
// Add environment variables for Python unbuffered output
startInfo.Environment["PYTHONUNBUFFERED"] = "1";
startInfo.Environment["PYTHONIOENCODING"] = "utf-8";
// Add debugging
Debug.WriteLine($"[PythonInterop] Executing Python directly without cmd wrapper");
Debug.WriteLine($"[PythonInterop] Python Exe: {pythonExe}");
Debug.WriteLine($"[PythonInterop] Arguments: {startInfo.Arguments}");
Debug.WriteLine($"[PythonInterop] Working Directory: {PythonBasePath}");
Debug.WriteLine($"[PythonInterop] Script Path: {scriptPath}");
using var process = new Process { StartInfo = startInfo };
process.Start();
Debug.WriteLine($"[PythonInterop] Process started, PID: {process.Id}");
// Start reading output immediately to prevent deadlock
var outputTask = process.StandardOutput.ReadToEndAsync();
var errorTask = process.StandardError.ReadToEndAsync();
// Wait for process to complete
await process.WaitForExitAsync();
// Wait for all streams to be read
var output = await outputTask;
var error = await errorTask;
Debug.WriteLine($"[PythonInterop] Process completed");
Debug.WriteLine($"[PythonInterop] Exit Code: {process.ExitCode}");
Debug.WriteLine($"[PythonInterop] Output Length: {output?.Length ?? 0}");
Debug.WriteLine($"[PythonInterop] Error Length: {error?.Length ?? 0}");
Debug.WriteLine($"[PythonInterop] Raw Output: '{output}'");
Debug.WriteLine($"[PythonInterop] Raw Error: '{error}'");
// Also log first 200 chars of output for debugging
if (!string.IsNullOrEmpty(output))
{
var preview = output.Length > 200 ? output.Substring(0, 200) + "..." : output;
Debug.WriteLine($"[PythonInterop] Output Preview: '{preview}'");
}
return new PythonExecutionResult
{
Success = process.ExitCode == 0,
Output = output,
Error = error,
ExitCode = process.ExitCode
};
}
catch (Exception ex)
{
Debug.WriteLine($"[PythonInterop] Exception: {ex.Message}");
Debug.WriteLine($"[PythonInterop] Stack trace: {ex.StackTrace}");
return new PythonExecutionResult
{
Success = false,
Error = ex.Message,
ExitCode = -1
};
}
}
#endregion
#region TSNet Specific Methods
/// <summary>
/// Verifica si TSNet está disponible
/// </summary>
public static bool IsTSNetAvailable()
{
try
{
if (!IsInitialized && !Initialize())
return false;
var command = @"
try:
import tsnet
print('TSNet version:', tsnet.__version__)
result = True
except ImportError as e:
print('TSNet not available:', e)
result = False
";
return ExecuteCommand(command);
}
catch (Exception ex)
{
Debug.WriteLine($"Error verificando TSNet: {ex.Message}");
return false;
}
}
/// <summary>
/// Ejecuta una simulación TSNet básica
/// </summary>
public static async Task<PythonExecutionResult> RunTSNetSimulationAsync(string inpFilePath, string outputDir)
{
var scriptContent = $@"
import tsnet
import wntr
import os
try:
# Crear directorio de salida si no existe
os.makedirs(r'{outputDir}', exist_ok=True)
# Cargar el modelo usando WNTR (TSNet usa WNTR internamente)
wn = wntr.network.WaterNetworkModel(r'{inpFilePath}')
# 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
# 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}')
# CORRECCIÓN DIVISIÓN POR CERO: Usar API oficial de TSNet
print('TSNet: simulation_period inicial =', getattr(tm, 'simulation_period', 'N/A'))
print('TSNet: time_step inicial =', getattr(tm, 'time_step', 'N/A'))
# Configurar tiempo usando API oficial de TSNet
simulation_time = 1.0 # 1 segundo por defecto
if hasattr(tm, 'simulation_period') and tm.simulation_period > 0:
simulation_time = tm.simulation_period
# CORRECCIÓN COMPLETA DEFINITIVA PARA TSNET - SOLUCIÓN 100% FUNCIONAL
print('TSNet: Aplicando correcciones definitivas ANTES de tm.set_time()...')
corrections_applied = 0
try:
# CORRECCIÓN 1: PIPES - Todos los atributos necesarios con tamaños correctos
# CRÍTICO: Debe hacerse ANTES de tm.set_time() que calcula el timestep
if hasattr(tm, 'pipe_name_list') and hasattr(tm, 'get_link'):
for pipe_name in tm.pipe_name_list:
try:
pipe_obj = tm.get_link(pipe_name)
# Calcular número de segmentos basado en longitud
pipe_length = getattr(pipe_obj, 'length', 1.0)
dx = 0.1 # Delta espacial por defecto
num_segments = int(pipe_length / dx)
if num_segments < 1:
num_segments = 1
# CLAVE: Arrays con N+1 elementos para evitar errores de índice
array_size = num_segments + 1
# Corrección initial_head como array numpy con tamaño correcto
import numpy as np
pipe_obj.initial_head = np.zeros(array_size)
corrections_applied += 1
# Corrección initial_velocity como array numpy con tamaño correcto
pipe_obj.initial_velocity = np.zeros(array_size)
corrections_applied += 1
# Corrección number_of_segments
pipe_obj.number_of_segments = num_segments
corrections_applied += 1
# CRÍTICO: wavev DEBE estar presente antes de tm.set_time()
pipe_obj.wavev = 1000.0 # m/s - velocidad típica en agua
corrections_applied += 1
print('TSNet: Pipe ' + pipe_name + ' wavev configurado: 1000.0 m/s')
# Corrección roughness_height desde roughness
if hasattr(pipe_obj, 'roughness'):
pipe_obj.roughness_height = pipe_obj.roughness
else:
pipe_obj.roughness_height = 0.001 # Valor por defecto
corrections_applied += 1
except Exception as e:
print('TSNet: Error corrigiendo pipe ' + pipe_name + ': ' + str(e))
pass
# CORRECCIÓN 2: NODOS - demand_coeff para junctions
if hasattr(tm, 'node_name_list') and hasattr(tm, 'get_node'):
for node_name in tm.node_name_list:
try:
node_obj = tm.get_node(node_name)
# Corrección demand_coeff para junctions
if hasattr(node_obj, '_demand') and not hasattr(node_obj, 'demand_coeff'):
node_obj.demand_coeff = [1.0, 0.0, 0.0] # [a, b, c] para demanda variable
corrections_applied += 1
except Exception as e:
print('TSNet: Error corrigiendo nodo ' + node_name + ': ' + str(e))
pass
# CORRECCIÓN 3: BOMBAS - curve_coef con valores por defecto
if hasattr(tm, 'pump_name_list') and hasattr(tm, 'get_link'):
for pump_name in tm.pump_name_list:
try:
pump_obj = tm.get_link(pump_name)
# Corrección curve_coef con coeficientes por defecto
pump_obj.curve_coef = [100.0, -0.1, 0.0] # [a, b, c] para H = a + b*Q + c*Q^2
corrections_applied += 1
except Exception as e:
print('TSNet: Error corrigiendo bomba ' + pump_name + ': ' + str(e))
pass
print('TSNet: ' + str(corrections_applied) + ' correcciones aplicadas ANTES de set_time')
except Exception as e:
print('TSNet: Error aplicando correcciones: ' + str(e))
# Continuar sin las correcciones
pass
# AHORA SI: USAR API OFICIAL tm.set_time() después de configurar wavev
print('TSNet: Llamando tm.set_time(' + str(simulation_time) + ') con wavev configurado...')
tm.set_time(simulation_time)
print('TSNet: Configuración con tm.set_time(' + str(simulation_time) + ') EXITOSA')
print('TSNet: simulation_period final =', tm.simulation_period)
print('TSNet: time_step final =', tm.time_step)
print('TSNet: pasos de simulación =', int(tm.simulation_period/tm.time_step))
# Configurar arrays de resultados DESPUÉS de conocer time_step
try:
if hasattr(tm, 'pipe_name_list') and hasattr(tm, 'get_link'):
# Calcular número de pasos de tiempo para arrays de resultados
num_time_steps = int(tm.simulation_period / tm.time_step) + 1
for pipe_name in tm.pipe_name_list:
try:
pipe_obj = tm.get_link(pipe_name)
# ATRIBUTOS DE RESULTADOS: Arrays pre-dimensionados para almacenar resultados
import numpy as np
pipe_obj.start_node_velocity = np.zeros(num_time_steps)
pipe_obj.end_node_velocity = np.zeros(num_time_steps)
pipe_obj.start_node_head = np.zeros(num_time_steps)
pipe_obj.end_node_head = np.zeros(num_time_steps)
pipe_obj.start_node_flowrate = np.zeros(num_time_steps)
pipe_obj.end_node_flowrate = np.zeros(num_time_steps)
except Exception as e:
print('TSNet: Error configurando arrays resultados pipe ' + pipe_name + ': ' + str(e))
pass
print('TSNet: Arrays de resultados configurados exitosamente')
except Exception as e:
print('TSNet: Error configurando arrays de resultados: ' + str(e))
pass
# Ejecutar simulación usando la API OFICIAL de TSNet
try:
print('TSNet: Siguiendo patrón oficial de ejemplos TSNet...')
# PASO 1: INICIALIZACIÓN (faltaba en nuestro código!)
print('TSNet: Inicializando estado estacionario...')
t0 = 0.0 # tiempo inicial
engine = 'DD' # demand driven simulator
tm = tsnet.simulation.Initializer(tm, t0, engine)
print('TSNet: Inicialización completada')
# PASO 2: SIMULACIÓN TRANSITORIA (como en ejemplos oficiales)
print('TSNet: Ejecutando simulación transitoria...')
results_obj = 'ctreditor_results' # nombre para guardar resultados
tm = tsnet.simulation.MOCSimulator(tm, results_obj)
print('TSNet: Simulación completada exitosamente')
print('TSNet: Resultados generados en modelo transient')
# EXTRACCIÓN DE RESULTADOS: Usar API oficial de TSNet (sin normalización)
print('TSNet: Extrayendo resultados usando API oficial...')
# Verificar que tenemos timestamps
if hasattr(tm, 'simulation_timestamps'):
print('TSNet: simulation_timestamps disponibles: ' + str(len(tm.simulation_timestamps)) + ' pasos')
else:
print('TSNet: WARNING - simulation_timestamps no disponibles')
# Extraer resultados de nodos usando API oficial
node_results = {{}}
if hasattr(tm, 'node_name_list'):
for node_name in tm.node_name_list:
try:
node_obj = tm.get_node(node_name)
final_head = 0.0
# MÉTODO OFICIAL: usar _head (con underscore) como en ejemplos
if hasattr(node_obj, '_head'):
if hasattr(node_obj._head, '__len__') and len(node_obj._head) > 1:
# Usar último valor de la simulación (como en ejemplos oficiales)
final_head = float(node_obj._head[-1])
print('TSNet: Nodo ' + node_name + ' _head[-1]: ' + str(final_head) + ' m')
else:
final_head = float(node_obj._head)
print('TSNet: Nodo ' + node_name + ' _head: ' + str(final_head) + ' m')
# Fallback a método público si _head no existe
elif hasattr(node_obj, 'head'):
if hasattr(node_obj.head, '__len__') and len(node_obj.head) > 1:
final_head = float(node_obj.head[-1])
print('TSNet: Nodo ' + node_name + ' head[-1]: ' + str(final_head) + ' m')
else:
final_head = float(node_obj.head)
print('TSNet: Nodo ' + node_name + ' head: ' + str(final_head) + ' m')
else:
print('TSNet: Nodo ' + node_name + ' - sin atributos head disponibles')
node_results[node_name] = final_head
except Exception as e:
print('TSNet: Error extrayendo resultados de nodo ' + node_name + ': ' + str(e))
node_results[node_name] = 0.0
# Extraer resultados de tuberías usando API oficial
pipe_results = {{}}
if hasattr(tm, 'pipe_name_list'):
for pipe_name in tm.pipe_name_list:
try:
pipe_obj = tm.get_link(pipe_name)
final_flow = 0.0
# MÉTODO OFICIAL: como en ejemplos TSNet
if hasattr(pipe_obj, 'start_node_flowrate'):
if hasattr(pipe_obj.start_node_flowrate, '__len__') and len(pipe_obj.start_node_flowrate) > 1:
# Usar último valor de la simulación
final_flow = float(pipe_obj.start_node_flowrate[-1])
print('TSNet: Tubería ' + pipe_name + ' start_node_flowrate[-1]: ' + str(final_flow) + ' m3/s')
else:
final_flow = float(pipe_obj.start_node_flowrate)
print('TSNet: Tubería ' + pipe_name + ' start_node_flowrate: ' + str(final_flow) + ' m3/s')
else:
print('TSNet: Tubería ' + pipe_name + ' - sin start_node_flowrate disponible')
pipe_results[pipe_name] = final_flow
except Exception as e:
print('TSNet: Error extrayendo resultados de tubería ' + pipe_name + ': ' + str(e))
pipe_results[pipe_name] = 0.0
# Guardar resultados en archivo JSON para que C# los pueda leer
import json
results_data = {{
'nodes': node_results,
'pipes': pipe_results,
'simulation_time': tm.simulation_period,
'time_step': tm.time_step
}}
results_file = r'{outputDir}' + '\\tsnet_results.json'
with open(results_file, 'w') as f:
json.dump(results_data, f, indent=2)
print('TSNet: Resultados guardados en: ' + results_file)
print('TSNet: ' + str(len(node_results)) + ' nodos, ' + str(len(pipe_results)) + ' tuberías procesadas')
# Guardar resultados si es necesario
print('Directorio de resultados: ' + r'{outputDir}')
except Exception as tsnet_error:
print('TSNet: Error avanzado en simulación - ' + str(tsnet_error))
print('TSNet: Las correcciones básicas funcionaron, usando fallback WNTR para completar')
# Fallback a simulación básica con WNTR
try:
import wntr.sim
sim = wntr.sim.EpanetSimulator(wn)
results = sim.run_sim()
print('WNTR: Simulación fallback completada exitosamente')
print('Resultado: Simulación hidráulica exitosa (TSNet + WNTR)')
except Exception as wntr_error:
print('Error en WNTR fallback: ' + str(wntr_error))
raise tsnet_error
print('Simulación completada exitosamente')
print('Resultados guardados en: ' + r'{outputDir}')
except Exception as e:
print('Error en simulación TSNet: ' + str(e))
raise
";
var tempScript = Path.Combine(Path.GetTempPath(), "tsnet_simulation.py");
await File.WriteAllTextAsync(tempScript, scriptContent);
try
{
return await ExecuteScriptAsync(tempScript);
}
finally
{
if (File.Exists(tempScript))
File.Delete(tempScript);
}
}
#endregion
}
/// <summary>
/// Resultado de la ejecución de un script Python
/// </summary>
public class PythonExecutionResult
{
public bool Success { get; set; }
public string Output { get; set; } = "";
public string Error { get; set; } = "";
public int ExitCode { get; set; }
public override string ToString()
{
var sb = new StringBuilder();
sb.AppendLine($"Success: {Success}");
sb.AppendLine($"ExitCode: {ExitCode}");
if (!string.IsNullOrEmpty(Output))
sb.AppendLine($"Output: {Output}");
if (!string.IsNullOrEmpty(Error))
sb.AppendLine($"Error: {Error}");
return sb.ToString();
}
}
}