Mejora en el convertidor LAD de TwinCAT con integración de SymPy para optimización de expresiones lógicas. Se añadieron nuevas funcionalidades para el manejo de variables y ACTIONs, así como mejoras en la estructura del código SCL generado.

This commit is contained in:
Miguel 2025-06-19 15:13:24 +02:00
parent 205e1f4c8d
commit 5da7dcad06
1 changed files with 485 additions and 19 deletions

View File

@ -2,9 +2,26 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
Convertidor simplificado de LAD TwinCAT a pseudocódigo estructurado Convertidor simplificado de LAD TwinCAT a pseudocódigo estructurado
Versión mejorada con SymPy para optimización de expresiones lógicas
""" """
import re import re
import sympy
from sympy import symbols, And, Or, Not, simplify
from sympy.logic.boolalg import to_dnf
class SymbolManager:
"""Gestor de símbolos para SymPy"""
def __init__(self):
self._symbol_cache = {}
def get_symbol(self, name):
"""Obtener o crear símbolo SymPy"""
if name not in self._symbol_cache:
# Limpiar nombre para SymPy (sin espacios, caracteres especiales)
clean_name = re.sub(r'[^a-zA-Z0-9_]', '_', name)
self._symbol_cache[name] = symbols(clean_name)
return self._symbol_cache[name]
class SimpleLadConverter: class SimpleLadConverter:
"""Convertidor simplificado de LAD a código estructurado""" """Convertidor simplificado de LAD a código estructurado"""
@ -12,21 +29,89 @@ class SimpleLadConverter:
def __init__(self): def __init__(self):
self.networks = [] self.networks = []
self.current_network_id = 0 self.current_network_id = 0
self.symbol_manager = SymbolManager()
self.sympy_expressions = {} # Mapeo de network_id -> sympy expression
# Nuevas propiedades para estructura SCL completa
self.program_name = ""
self.program_path = ""
self.var_sections = {} # VAR, VAR_INPUT, VAR_OUTPUT, etc.
self.actions = {} # Diccionario de ACTIONs
def parse_file(self, file_path): def parse_file(self, file_path):
"""Parse el archivo LAD""" """Parse el archivo LAD completo incluyendo variables y ACTIONs"""
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f: with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
content = f.read() content = f.read()
# Extraer información del header
self._parse_header_info(content)
# Extraer declaraciones de variables
self._parse_variable_declarations(content)
# Encontrar sección LAD # Encontrar sección LAD
lad_start = content.find('_LD_BODY') lad_start = content.find('_LD_BODY')
if lad_start == -1: if lad_start != -1:
# Extraer contenido LAD hasta ACTION o END_PROGRAM
action_start = content.find('\nACTION', lad_start)
end_program = content.find('\nEND_PROGRAM', lad_start)
lad_end = action_start if action_start != -1 else end_program
if lad_end == -1:
lad_end = len(content)
lad_content = content[lad_start:lad_end]
lines = lad_content.split('\n')
self._parse_networks(lines)
else:
print("No se encontró _LD_BODY") print("No se encontró _LD_BODY")
return
# Extraer contenido LAD # Extraer ACTIONs
lines = content[lad_start:].split('\n') self._parse_actions(content)
self._parse_networks(lines)
def _parse_header_info(self, content):
"""Extraer información del header del programa"""
# Buscar PATH y nombre del programa
path_match = re.search(r'\(\* @PATH := \'([^\']+)\' \*\)', content)
if path_match:
self.program_path = path_match.group(1)
# Buscar nombre del programa/function_block
program_match = re.search(r'(PROGRAM|FUNCTION_BLOCK)\s+(\w+)', content)
if program_match:
self.program_name = program_match.group(2)
print(f"Programa encontrado: {self.program_name}")
if self.program_path:
print(f"Path: {self.program_path}")
def _parse_variable_declarations(self, content):
"""Extraer todas las declaraciones de variables"""
# Buscar todas las secciones VAR
var_patterns = [
(r'VAR_INPUT(.*?)END_VAR', 'VAR_INPUT'),
(r'VAR_OUTPUT(.*?)END_VAR', 'VAR_OUTPUT'),
(r'VAR_IN_OUT(.*?)END_VAR', 'VAR_IN_OUT'),
(r'VAR\s+(.*?)END_VAR', 'VAR'),
]
for pattern, var_type in var_patterns:
matches = re.findall(pattern, content, re.DOTALL | re.IGNORECASE)
if matches:
variables = []
for match in matches:
# Parsear cada línea de variable
for line in match.split('\n'):
line = line.strip()
if line and not line.startswith('(*') and ':' in line:
# Limpiar comentarios inline
if '(*' in line:
line = line[:line.find('(*')]
variables.append(line.strip())
if variables:
self.var_sections[var_type] = variables
print(f"Variables {var_type}: {len(variables)} encontradas")
def _parse_networks(self, lines): def _parse_networks(self, lines):
"""Parse todas las redes""" """Parse todas las redes"""
@ -205,22 +290,52 @@ class SimpleLadConverter:
return i + 1, ' '.join(comment_lines) return i + 1, ' '.join(comment_lines)
def convert_to_structured(self): def convert_to_structured(self):
"""Convertir a código estructurado""" """Convertir a código SCL completo con variables y ACTIONs"""
output = [] output = []
output.append("// Código pseudo estructurado generado desde LAD TwinCAT")
output.append("// Compatible con IEC61131-3") # Header del archivo
output.append("PROGRAM PumpControl_Converted") output.append("(* Código SCL generado desde LAD TwinCAT *)")
output.append("(* Convertidor mejorado con SymPy - Estructura DNF preferida *)")
if self.program_path:
output.append(f"(* Path original: {self.program_path} *)")
output.append("") output.append("")
# Declaración del programa
program_name = self.program_name if self.program_name else "ConvertedProgram"
output.append(f"PROGRAM {program_name}")
# Declaraciones de variables
self._add_variable_sections(output)
# Inicio del código principal
output.append("")
output.append("(* === CÓDIGO PRINCIPAL === *)")
output.append("")
# Lógica de las redes LAD
for network in self.networks: for network in self.networks:
output.append(f" // Red {network['id']}") output.append(f" // Red {network['id']}")
if network['comment']: if network['comment']:
output.append(f" // {network['comment']}") output.append(f" // {network['comment']}")
if network['logic'] and network['target']: if network['logic'] and network['target']:
condition_str = self._convert_logic_to_string(network['logic']) # Usar expresión DNF optimizada si está disponible
if network['id'] in self.sympy_expressions:
sympy_expr = self.sympy_expressions[network['id']]
condition_str = self._format_dnf_for_lad(sympy_expr)
output.append(f" // Optimizada con SymPy DNF")
else:
# Fallback al método original
condition_str = self._convert_logic_to_string(network['logic'])
output.append(f" // Sin optimización SymPy")
if condition_str: if condition_str:
output.append(f" IF {condition_str} THEN") # Si hay saltos de línea en la condición (múltiples términos OR)
if '\n' in condition_str:
output.append(f" IF {condition_str} THEN")
else:
output.append(f" IF {condition_str} THEN")
output.append(f" {network['target']} := TRUE;") output.append(f" {network['target']} := TRUE;")
output.append(" ELSE") output.append(" ELSE")
output.append(f" {network['target']} := FALSE;") output.append(f" {network['target']} := FALSE;")
@ -230,9 +345,58 @@ class SimpleLadConverter:
output.append("") output.append("")
# Fin del programa principal
output.append("END_PROGRAM") output.append("END_PROGRAM")
# Agregar ACTIONs como subfunciones
self._add_actions_as_procedures(output, program_name)
return '\n'.join(output) return '\n'.join(output)
def _add_variable_sections(self, output):
"""Agregar todas las secciones de variables al código"""
# Orden preferido de las secciones
section_order = ['VAR_INPUT', 'VAR_OUTPUT', 'VAR_IN_OUT', 'VAR']
for section_type in section_order:
if section_type in self.var_sections:
output.append(f"{section_type}")
for var_line in self.var_sections[section_type]:
# Asegurar que termine con punto y coma
if not var_line.endswith(';'):
var_line += ' ;'
output.append(f" {var_line}")
output.append("END_VAR")
output.append("")
def _add_actions_as_procedures(self, output, program_name):
"""Agregar ACTIONs como procedimientos independientes"""
if not self.actions:
return
output.append("")
output.append("(* === SUBFUNCIONES (ACTIONs convertidas) === *)")
output.append("")
for action_name, action_code in self.actions.items():
# Nombre completo como se usa en TwinCAT
full_name = f"{program_name}_{action_name}"
output.append(f"PROCEDURE {full_name}")
output.append("(* Convertida desde ACTION *)")
output.append("")
# Agregar el código de la ACTION con indentación
for line in action_code.split('\n'):
if line.strip():
output.append(f" {line.strip()}")
else:
output.append("")
output.append("")
output.append("END_PROCEDURE")
output.append("")
def _convert_logic_to_string(self, logic): def _convert_logic_to_string(self, logic):
"""Convertir lógica LAD a string estructurado""" """Convertir lógica LAD a string estructurado"""
if not logic: if not logic:
@ -333,32 +497,334 @@ class SimpleLadConverter:
return f"{prefix}UNKNOWN: {logic}" return f"{prefix}UNKNOWN: {logic}"
def _logic_to_sympy(self, logic):
"""Convertir lógica LAD a expresión SymPy"""
if not logic:
return None
if logic['type'] == 'CONTACT':
symbol = self.symbol_manager.get_symbol(logic['name'])
if logic['negated']:
return Not(symbol)
else:
return symbol
elif logic['type'] == 'AND':
sympy_operands = []
for operand in logic['operands']:
operand_sympy = self._logic_to_sympy(operand)
if operand_sympy is not None:
sympy_operands.append(operand_sympy)
if len(sympy_operands) > 1:
return And(*sympy_operands)
elif len(sympy_operands) == 1:
return sympy_operands[0]
else:
return None
elif logic['type'] == 'OR':
sympy_operands = []
for operand in logic['operands']:
operand_sympy = self._logic_to_sympy(operand)
if operand_sympy is not None:
sympy_operands.append(operand_sympy)
if len(sympy_operands) > 1:
return Or(*sympy_operands)
elif len(sympy_operands) == 1:
return sympy_operands[0]
else:
return None
elif logic['type'] == 'FUNCTION_BLOCK':
# Para function blocks, creamos un símbolo especial
fb_name = f"{logic['name']}({', '.join(logic['inputs'])})"
return self.symbol_manager.get_symbol(fb_name)
return None
def _sympy_to_structured_string(self, sympy_expr):
"""Convertir expresión SymPy a string estructurado"""
if sympy_expr is None:
return ""
# Crear un mapeo inverso de símbolos a nombres originales
symbol_to_name = {}
for original_name, symbol in self.symbol_manager._symbol_cache.items():
symbol_to_name[str(symbol)] = original_name
# Convertir la expresión a string
str_expr = str(sympy_expr)
# Reemplazar símbolos por nombres originales
for symbol_str, original_name in symbol_to_name.items():
str_expr = str_expr.replace(symbol_str, original_name)
# Reemplazar operadores SymPy por operadores IEC61131-3
str_expr = str_expr.replace('&', ' AND ')
str_expr = str_expr.replace('|', ' OR ')
str_expr = str_expr.replace('~', 'NOT ')
# Limpiar espacios múltiples
str_expr = re.sub(r'\s+', ' ', str_expr)
return str_expr.strip()
def optimize_expressions(self):
"""Optimizar todas las expresiones usando SymPy - Preferir DNF para LAD"""
print("\n=== Optimizando expresiones con SymPy (forzando DNF para LAD) ===")
for network in self.networks:
if network['logic']:
print(f"\nOptimizando Red {network['id']}:")
# Convertir a SymPy
sympy_expr = self._logic_to_sympy(network['logic'])
if sympy_expr:
print(f" Expresión original: {sympy_expr}")
# Simplificar primero
try:
simplified = simplify(sympy_expr)
print(f" Simplificada: {simplified}")
# SIEMPRE convertir a DNF para LAD (forma natural: (AND) OR (AND) OR (AND))
dnf_expr = to_dnf(simplified)
print(f" DNF (forma LAD preferida): {dnf_expr}")
# Post-procesar para eliminar contradicciones
final_expr = self._post_process_expression(dnf_expr)
# Verificar si el post-procesamiento cambió algo
if str(final_expr) != str(dnf_expr):
print(f" Post-procesada: {final_expr}")
self.sympy_expressions[network['id']] = final_expr
except Exception as e:
print(f" Error optimizando: {e}")
self.sympy_expressions[network['id']] = sympy_expr
def group_common_conditions(self):
"""Agrupar networks con condiciones similares o complementarias"""
print("\n=== Analizando agrupación de condiciones ===")
# Buscar networks que podrían agruparse
groupable_networks = []
for network in self.networks:
if (network['logic'] and network['target'] and
network['id'] in self.sympy_expressions):
groupable_networks.append(network)
if len(groupable_networks) < 2:
print("No hay suficientes networks para agrupar")
return
# Analizar condiciones comunes
print(f"Analizando {len(groupable_networks)} networks para agrupación:")
for i, net1 in enumerate(groupable_networks):
for j, net2 in enumerate(groupable_networks[i+1:], i+1):
expr1 = self.sympy_expressions[net1['id']]
expr2 = self.sympy_expressions[net2['id']]
# Buscar factores comunes
try:
# Extraer factores comunes si es posible
common_factors = self._find_common_factors(expr1, expr2)
if common_factors:
print(f" Red {net1['id']} y Red {net2['id']} comparten: {common_factors}")
# Verificar si son complementarias (útil para SET/RESET)
if self._are_complementary(expr1, expr2):
print(f" Red {net1['id']} y Red {net2['id']} son complementarias")
except Exception as e:
print(f" Error analizando Red {net1['id']} y Red {net2['id']}: {e}")
def _find_common_factors(self, expr1, expr2):
"""Encontrar factores comunes entre dos expresiones"""
try:
# Convertir a conjuntos de símbolos para análisis básico
symbols1 = expr1.free_symbols
symbols2 = expr2.free_symbols
common_symbols = symbols1.intersection(symbols2)
if len(common_symbols) > 1:
return f"{len(common_symbols)} símbolos comunes"
return None
except:
return None
def _are_complementary(self, expr1, expr2):
"""Verificar si dos expresiones son complementarias"""
try:
# Verificar si expr1 == NOT(expr2) simplificado
complement = Not(expr2)
simplified_complement = simplify(complement)
simplified_expr1 = simplify(expr1)
return simplified_expr1.equals(simplified_complement)
except:
return False
def _post_process_expression(self, expr):
"""Post-procesar expresión para eliminar contradicciones obvias"""
try:
# Detectar contradicciones como X & ~X que deberían ser False
cleaned_expr = expr
# Aplicar simplificaciones adicionales
cleaned_expr = simplify(cleaned_expr)
# Si la expresión contiene contradicciones obvias, intentar limpiar
free_symbols = cleaned_expr.free_symbols
for symbol in free_symbols:
# Verificar si tenemos symbol & ~symbol en alguna parte
contradiction = And(symbol, Not(symbol))
if cleaned_expr.has(contradiction):
print(f" Detectada contradicción eliminable: {symbol} AND NOT {symbol}")
# Reemplazar contradicción por False
cleaned_expr = cleaned_expr.replace(contradiction, sympy.false)
cleaned_expr = simplify(cleaned_expr)
return cleaned_expr
except:
return expr
def _format_dnf_for_lad(self, sympy_expr):
"""Formatear expresión DNF para código LAD más legible"""
if sympy_expr is None:
return ""
# Crear mapeo de símbolos a nombres originales
symbol_to_name = {}
for original_name, symbol in self.symbol_manager._symbol_cache.items():
symbol_to_name[str(symbol)] = original_name
# Convertir a string y analizar la estructura
str_expr = str(sympy_expr)
# Reemplazar símbolos por nombres originales
for symbol_str, original_name in symbol_to_name.items():
str_expr = str_expr.replace(symbol_str, original_name)
# Reemplazar operadores SymPy por IEC61131-3 primero
str_expr = str_expr.replace('&', ' AND ')
str_expr = str_expr.replace('|', ' OR ')
str_expr = str_expr.replace('~', 'NOT ')
# Limpiar espacios múltiples
str_expr = re.sub(r'\s+', ' ', str_expr).strip()
# Si es una expresión OR de términos AND principales, formatear cada término
# Buscar el patrón de OR principales (no anidados en paréntesis)
if ' OR ' in str_expr and not str_expr.startswith('('):
# Dividir por OR de nivel principal
# Esto es más complejo debido a paréntesis anidados
parts = self._split_main_or_terms(str_expr)
if len(parts) > 1:
formatted_terms = []
for part in parts:
part = part.strip()
# Asegurar que cada término tenga paréntesis si es complejo
if ' AND ' in part and not (part.startswith('(') and part.endswith(')')):
part = f"({part})"
formatted_terms.append(part)
# Unir con OR y saltos de línea para mejor legibilidad
return '\n OR '.join(formatted_terms)
return str_expr
def _split_main_or_terms(self, expr):
"""Dividir expresión por OR de nivel principal, respetando paréntesis"""
parts = []
current_part = ""
paren_level = 0
i = 0
while i < len(expr):
char = expr[i]
if char == '(':
paren_level += 1
current_part += char
elif char == ')':
paren_level -= 1
current_part += char
elif paren_level == 0 and expr[i:i+4] == ' OR ':
# OR de nivel principal encontrado
parts.append(current_part.strip())
current_part = ""
i += 3 # Saltar ' OR '
else:
current_part += char
i += 1
# Agregar la última parte
if current_part.strip():
parts.append(current_part.strip())
return parts if len(parts) > 1 else [expr]
def _parse_actions(self, content):
"""Extraer todas las ACTIONs del programa"""
# Buscar patrón ACTION nombre: ... END_ACTION
action_pattern = r'ACTION\s+(\w+)\s*:(.*?)END_ACTION'
action_matches = re.findall(action_pattern, content, re.DOTALL | re.IGNORECASE)
for action_name, action_code in action_matches:
# Limpiar el código de la ACTION
clean_code = action_code.strip()
self.actions[action_name] = clean_code
print(f"ACTION encontrada: {action_name} ({len(clean_code)} caracteres)")
print(f"Total ACTIONs: {len(self.actions)}")
def main(): def main():
"""Función principal""" """Función principal"""
converter = SimpleLadConverter() converter = SimpleLadConverter()
try: try:
print("=== Convertidor LAD Mejorado ===") print("=== Convertidor LAD a SCL con SymPy ===")
print("Parseando archivo _PUMPCONTROL.EXP...")
converter.parse_file(".example/_PUMPCONTROL.EXP") # Por ahora probar con SYRUPROOMCTRL que tiene variables y ACTIONs
file_path = ".example/SYRUPROOMCTRL.EXP"
output_name = "SYRUPROOMCTRL_scl"
print(f"Parseando archivo {file_path}...")
converter.parse_file(file_path)
print(f"Redes encontradas: {len(converter.networks)}") print(f"Redes encontradas: {len(converter.networks)}")
print(f"Secciones de variables: {list(converter.var_sections.keys())}")
print(f"ACTIONs encontradas: {list(converter.actions.keys())}")
# Mostrar información de debug # Mostrar información de debug
converter.print_debug_info() converter.print_debug_info()
# NUEVO: Optimizar expresiones con SymPy
converter.optimize_expressions()
# NUEVO: Analizar agrupación de condiciones
converter.group_common_conditions()
# Convertir y guardar # Convertir y guardar
print("\nGenerando código estructurado...") print("\nGenerando código SCL completo...")
structured_code = converter.save_to_file("pump_control_output.txt") structured_code = converter.save_to_file(f"{output_name}.txt")
# Mostrar el código generado # Mostrar el código generado
lines = structured_code.split('\n') lines = structured_code.split('\n')
print(f"\nCódigo generado ({len(lines)} líneas):") print(f"\nCódigo SCL generado ({len(lines)} líneas):")
for i, line in enumerate(lines): for i, line in enumerate(lines):
print(f"{i+1:3d}: {line}") print(f"{i+1:3d}: {line}")
print(f"\n✓ Conversión completada!") print(f"\n✓ Conversión SCL completada!")
except Exception as e: except Exception as e:
print(f"Error: {e}") print(f"Error: {e}")