From 5da7dcad06668d9b6fcec7a6e059bb5fbc580e0e Mon Sep 17 00:00:00 2001 From: Miguel Date: Thu, 19 Jun 2025 15:13:24 +0200 Subject: [PATCH] =?UTF-8?q?Mejora=20en=20el=20convertidor=20LAD=20de=20Twi?= =?UTF-8?q?nCAT=20con=20integraci=C3=B3n=20de=20SymPy=20para=20optimizaci?= =?UTF-8?q?=C3=B3n=20de=20expresiones=20l=C3=B3gicas.=20Se=20a=C3=B1adiero?= =?UTF-8?q?n=20nuevas=20funcionalidades=20para=20el=20manejo=20de=20variab?= =?UTF-8?q?les=20y=20ACTIONs,=20as=C3=AD=20como=20mejoras=20en=20la=20estr?= =?UTF-8?q?uctura=20del=20c=C3=B3digo=20SCL=20generado.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TwinCat/simple_lad_converter.py | 504 +++++++++++++++++- 1 file changed, 485 insertions(+), 19 deletions(-) diff --git a/backend/script_groups/TwinCat/simple_lad_converter.py b/backend/script_groups/TwinCat/simple_lad_converter.py index 04b9713..c9b6f3e 100644 --- a/backend/script_groups/TwinCat/simple_lad_converter.py +++ b/backend/script_groups/TwinCat/simple_lad_converter.py @@ -2,9 +2,26 @@ # -*- coding: utf-8 -*- """ Convertidor simplificado de LAD TwinCAT a pseudocódigo estructurado +Versión mejorada con SymPy para optimización de expresiones lógicas """ 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: """Convertidor simplificado de LAD a código estructurado""" @@ -12,21 +29,89 @@ class SimpleLadConverter: def __init__(self): self.networks = [] 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): - """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: 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 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") - return - # Extraer contenido LAD - lines = content[lad_start:].split('\n') - self._parse_networks(lines) + # Extraer ACTIONs + self._parse_actions(content) + + 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): """Parse todas las redes""" @@ -205,22 +290,52 @@ class SimpleLadConverter: return i + 1, ' '.join(comment_lines) def convert_to_structured(self): - """Convertir a código estructurado""" + """Convertir a código SCL completo con variables y ACTIONs""" output = [] - output.append("// Código pseudo estructurado generado desde LAD TwinCAT") - output.append("// Compatible con IEC61131-3") - output.append("PROGRAM PumpControl_Converted") + + # Header del archivo + 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("") + # 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: output.append(f" // Red {network['id']}") if network['comment']: output.append(f" // {network['comment']}") 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: - 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(" ELSE") output.append(f" {network['target']} := FALSE;") @@ -230,9 +345,58 @@ class SimpleLadConverter: output.append("") + # Fin del programa principal output.append("END_PROGRAM") + + # Agregar ACTIONs como subfunciones + self._add_actions_as_procedures(output, program_name) + 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): """Convertir lógica LAD a string estructurado""" if not logic: @@ -333,32 +497,334 @@ class SimpleLadConverter: 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(): """Función principal""" converter = SimpleLadConverter() try: - print("=== Convertidor LAD Mejorado ===") - print("Parseando archivo _PUMPCONTROL.EXP...") + print("=== Convertidor LAD a SCL con SymPy ===") - 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"Secciones de variables: {list(converter.var_sections.keys())}") + print(f"ACTIONs encontradas: {list(converter.actions.keys())}") # Mostrar información de debug 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 - print("\nGenerando código estructurado...") - structured_code = converter.save_to_file("pump_control_output.txt") + print("\nGenerando código SCL completo...") + structured_code = converter.save_to_file(f"{output_name}.txt") # Mostrar el código generado 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): print(f"{i+1:3d}: {line}") - print(f"\n✓ Conversión completada!") + print(f"\n✓ Conversión SCL completada!") except Exception as e: print(f"Error: {e}")