#!/usr/bin/env python3 """ Motor de Evaluación Algebraico Puro para Calculadora MAV VERSIÓN UNIFICADA: Un solo parser (sympify) con contexto completo """ import re import sympy as sp from sympy import symbols, Eq, solve, sympify, latex, simplify from typing import List, Dict, Any, Optional, Tuple, Union from dataclasses import dataclass import logging try: from sympy_helper import SympyHelper HAS_SYMPY_HELPER = True except ImportError: HAS_SYMPY_HELPER = False from type_registry import ( get_registered_base_context, get_registered_tokenization_patterns, discover_and_register_types ) from tl_bracket_parser import BracketParser @dataclass class EvaluationResult: """Resultado de evaluación simplificado""" input_line: str output: str result_type: str success: bool error_message: Optional[str] = None is_equation: bool = False is_solve_query: bool = False class PureAlgebraicEngine: """Motor algebraico puro unificado - Un solo parser sympify""" def __init__(self): self.logger = logging.getLogger(__name__) self.equations = [] # Lista de ecuaciones Eq() self.variables = set() # Variables conocidas self.symbol_table = {} # Variables del usuario self.unified_context = {} # Contexto unificado para sympify self.bracket_parser = BracketParser() self.tokenization_patterns = [] # Patrones de tokenización # Cargar tipos personalizados PRIMERO self._load_custom_types() # Construir contexto unificado self._build_unified_context() self._load_tokenization_patterns() def _load_custom_types(self): """Carga los tipos personalizados desde custom_types/""" try: discover_and_register_types("custom_types") self.logger.debug("Tipos personalizados cargados") except Exception as e: self.logger.error(f"Error cargando tipos personalizados: {e}") def _build_unified_context(self): """Construye contexto unificado para sympify con TODOS los componentes""" # 1. FUNCIONES SYMPY BÁSICAS sympy_functions = { 'sin': sp.sin, 'cos': sp.cos, 'tan': sp.tan, 'asin': sp.asin, 'acos': sp.acos, 'atan': sp.atan, 'sinh': sp.sinh, 'cosh': sp.cosh, 'tanh': sp.tanh, 'log': sp.log, 'ln': sp.ln, 'exp': sp.exp, 'sqrt': sp.sqrt, 'abs': sp.Abs, 'pi': sp.pi, 'e': sp.E, 'I': sp.I, 'oo': sp.oo, 'inf': sp.oo, 'solve': self._smart_solve, 'Eq': sp.Eq, 'simplify': sp.simplify, 'expand': sp.expand, 'factor': sp.factor, 'diff': sp.diff, 'integrate': sp.integrate, 'Matrix': sp.Matrix, 'symbols': sp.symbols, 'Symbol': sp.Symbol, 'Rational': sp.Rational, 'Float': sp.Float, 'Integer': sp.Integer, 'limit': sp.limit, 'series': sp.series, 'summation': sp.summation, 'product': sp.product, 'binomial': sp.binomial, 'factorial': sp.factorial, 'gcd': sp.gcd, 'lcm': sp.lcm, 'ceiling': sp.ceiling, 'floor': sp.floor, 'Piecewise': sp.Piecewise, } # 2. TIPOS PERSONALIZADOS REGISTRADOS (CLAVE PARA INSTANCIACIÓN) registered_types = get_registered_base_context() # 3. FUNCIONES DE PLOTTING try: from sympy.plotting import plot, plot3d, plot_parametric, plot3d_parametric_line plotting_functions = { 'plot': plot, 'plot3d': plot3d, 'plot_parametric': plot_parametric, 'plot3d_parametric_line': plot3d_parametric_line, } except Exception as e: self.logger.warning(f"Error cargando funciones de plotting: {e}") plotting_functions = {} # 4. COMBINAR TODO EN CONTEXTO UNIFICADO self.unified_context = { **sympy_functions, **registered_types, # IP4, FourBytes, IntBase, etc. **plotting_functions } # 5. VERIFICAR CARGA DE TIPOS PRINCIPALES required_classes = ['IP4', 'IP4Mask', 'FourBytes', 'IntBase', 'Hex', 'Bin', 'Dec', 'Chr', 'LaTeX'] missing_classes = [cls for cls in required_classes if cls not in self.unified_context] if missing_classes: self.logger.warning(f"Clases faltantes en contexto: {missing_classes}") self.logger.debug(f"Contexto unificado construido: {len(self.unified_context)} elementos") # Verificar que tipos principales tengan prioridad correcta para álgebra for name, cls in registered_types.items(): if hasattr(cls, '_op_priority'): self.logger.debug(f"{name} tiene prioridad: {cls._op_priority}") def _load_tokenization_patterns(self): """Carga los patrones de tokenización dinámicos""" try: self.tokenization_patterns = get_registered_tokenization_patterns() self.logger.debug(f"Patrones de tokenización cargados: {len(self.tokenization_patterns)}") except Exception as e: self.logger.error(f"Error cargando patrones de tokenización: {e}") self.tokenization_patterns = [] def _apply_tokenization(self, line: str) -> str: """Aplica tokenización dinámica a la línea de entrada""" # 1. TOKENIZACIÓN ESPECIAL: _x=? → solve(_x) variable_solve_pattern = r'([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*\?' if re.match(variable_solve_pattern, line.strip()): var_name = re.match(variable_solve_pattern, line.strip()).group(1) tokenized_line = f"solve({var_name})" self.logger.debug(f"Tokenización solve: '{line}' → '{tokenized_line}'") line = tokenized_line if not self.tokenization_patterns: return line tokenized_line = line # Ordenar patrones por prioridad (mayor prioridad primero) sorted_patterns = sorted(self.tokenization_patterns, key=lambda p: p.get('priority', 0), reverse=True) for pattern_info in sorted_patterns: pattern = pattern_info['pattern'] replacement_func = pattern_info['replacement'] try: tokenized_line = re.sub(pattern, replacement_func, tokenized_line) except Exception as e: self.logger.debug(f"Error aplicando patrón {pattern}: {e}") continue if tokenized_line != line: self.logger.debug(f"Tokenización: '{line}' → '{tokenized_line}'") return tokenized_line def _get_complete_context(self) -> Dict[str, Any]: """Obtiene contexto completo incluyendo variables del usuario""" complete_context = self.unified_context.copy() complete_context.update(self.symbol_table) return complete_context def evaluate_line(self, line: str) -> EvaluationResult: """Evalúa una línea de entrada usando sympify unificado""" line = line.strip() if not line or line.startswith('#'): return EvaluationResult(line, "", "comment", True) try: # 1. Aplicar tokenización dinámica tokenized_line = self._apply_tokenization(line) # Tokenización aplicada silenciosamente # 2. Preprocesar con bracket parser (legacy) processed_line = self.bracket_parser.process_expression(tokenized_line) self.logger.debug(f"Línea procesada: {processed_line}") # 3. Determinar tipo de entrada if self._is_solve_shortcut(processed_line): return self._evaluate_solve_shortcut(processed_line) elif '=' in processed_line and not self._is_comparison(processed_line): # Verificar si es una asignación simple (lado izquierdo es variable) left_side = processed_line.split('=')[0].strip() if re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', left_side): return self._evaluate_assignment(processed_line) else: return self._evaluate_equation(processed_line) else: return self._evaluate_expression(processed_line) except Exception as e: error_msg = f"Error: {type(e).__name__}: {str(e)}" self.logger.error(f"Error evaluando '{line}': {e}") return EvaluationResult(line, error_msg, "error", False, str(e)) def _is_solve_shortcut(self, line: str) -> bool: """Detecta atajos de resolución como solve(x)""" return line.startswith('solve(') def _is_comparison(self, line: str) -> bool: """Detecta comparaciones como ==, <=, >=, !=""" comparison_ops = ['==', '<=', '>=', '!=', '<', '>'] return any(op in line for op in comparison_ops) def _evaluate_solve_shortcut(self, line: str) -> EvaluationResult: """Evalúa atajos de resolución""" try: if line.startswith('solve('): # Función solve() - manejar casos especiales primero # Extraer el contenido dentro de solve() import re match = re.match(r'solve\(([^)]+)\)', line) if match: var_content = match.group(1).strip() # Si es una variable simple, usar nuestra lógica mejorada if re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', var_content): # Crear símbolo directamente sin usar context para evitar sustitución var_symbol = sp.Symbol(var_content) solution_result = self._smart_solve(var_symbol) output = str(solution_result) numeric = self._get_numeric_approximation(solution_result) if numeric and str(solution_result) != str(numeric): output += f" ≈ {numeric}" return EvaluationResult(line, output, "symbolic", True, is_solve_query=True) # Para casos más complejos, usar sympify result = self._evaluate_expression(line) result.is_solve_query = True return result except Exception as e: error_msg = f"Error en resolución: {str(e)}" return EvaluationResult(line, error_msg, "error", False, str(e)) def _evaluate_assignment(self, line: str) -> EvaluationResult: """Evalúa una asignación manteniendo doble registro""" try: # Separar variable = expresión var_name, expr_str = line.split('=', 1) var_name = var_name.strip() expr_str = expr_str.strip() # Evaluar la expresión del lado derecho context = self._get_complete_context() result = sympify(expr_str, locals=context) # 1. ASIGNACIÓN DIRECTA (para uso inmediato) self.symbol_table[var_name] = result # Variable asignada correctamente # 2. ECUACIÓN IMPLÍCITA (para solve) var_symbol = sp.Symbol(var_name) equation = Eq(var_symbol, result) self.equations.append(equation) # Añadir símbolo a variables conocidas self.variables.add(var_symbol) # Output conciso - mostrar el valor asignado output = str(result) # Añadir aproximación numérica si es útil numeric = self._get_numeric_approximation(result) if numeric and str(result) != str(numeric): output += f" ≈ {numeric}" return EvaluationResult(line, output, "assignment", True) except Exception as e: error_msg = f"Error en asignación: {str(e)}" return EvaluationResult(line, error_msg, "error", False, str(e)) def _evaluate_equation(self, line: str) -> EvaluationResult: """Evalúa una ecuación y la añade al sistema""" try: # Separar left = right left_str, right_str = line.split('=', 1) left_str = left_str.strip() right_str = right_str.strip() # USAR SYMPIFY UNIFICADO para ambos lados context = self._get_complete_context() left_expr = sympify(left_str, locals=context) right_expr = sympify(right_str, locals=context) # Crear ecuación equation = Eq(left_expr, right_expr) # Añadir al sistema self.equations.append(equation) # Extraer variables eq_vars = equation.free_symbols self.variables.update(eq_vars) # Output conciso output = str(equation) # Evaluación numérica si es posible numeric = self._get_numeric_approximation(equation.rhs) if numeric and str(equation.rhs) != str(numeric): output += f" ≈ {numeric}" return EvaluationResult(line, output, "equation", True, is_equation=True) except Exception as e: error_msg = f"Error en ecuación: {str(e)}" return EvaluationResult(line, error_msg, "error", False, str(e)) def _evaluate_expression(self, line: str) -> EvaluationResult: """Evalúa una expresión usando sympify unificado ÚNICAMENTE""" try: # USAR SYMPIFY UNIFICADO - Un solo parser context = self._get_complete_context() result = sympify(line, locals=context) # Nota: Las asignaciones ahora se manejan en _evaluate_assignment # Simplificar si es expresión SymPy if hasattr(result, 'simplify'): result = simplify(result) output = str(result) # Añadir aproximación numérica numeric = self._get_numeric_approximation(result) if numeric and str(result) != str(numeric): output += f" ≈ {numeric}" return EvaluationResult(line, output, "symbolic", True) except Exception as e: error_msg = f"Error: {str(e)}" return EvaluationResult(line, error_msg, "error", False, str(e)) def _smart_solve(self, *args, **kwargs): """Función solve inteligente que usa nuestro sistema de ecuaciones""" if not args: # solve() sin argumentos - resolver todo el sistema if not self.equations: return "No hay ecuaciones en el sistema" try: all_vars = list(self.variables) solution = solve(self.equations, all_vars, dict=True) if solution: return solution[0] if len(solution) == 1 else solution else: return "Sin solución" except Exception as e: return f"Error resolviendo sistema: {e}" elif len(args) == 1 and hasattr(args[0], 'is_Symbol') and args[0].is_Symbol: # solve(variable) - resolver para una variable específica y devolver ecuación var_symbol = args[0] solution_value = self._solve_for_variable(var_symbol) # Si encontramos una solución, devolver como ecuación if solution_value != var_symbol: return Eq(var_symbol, solution_value) else: # Si no hay solución en las ecuaciones, verificar en symbol_table var_name = str(var_symbol) if var_name in self.symbol_table: return Eq(var_symbol, self.symbol_table[var_name]) else: return var_symbol else: # solve() con argumentos específicos (múltiples variables, ecuaciones, etc.) return solve(*args, **kwargs) def _solve_for_variable(self, var_symbol): """Resuelve una variable específica usando el sistema actual""" if not self.equations: return var_symbol try: # 1. Buscar si la variable tiene asignación directa en symbol_table var_name = str(var_symbol) if var_name in self.symbol_table: # Devolver el valor de la asignación directa return self.symbol_table[var_name] # 2. Buscar ecuaciones que contengan esta variable relevant_eqs = [eq for eq in self.equations if var_symbol in eq.free_symbols] if relevant_eqs: # Estrategia 1: Buscar asignación directa for eq in relevant_eqs: left_expr = eq.lhs right_expr = eq.rhs # Caso directo: variable = expresión if left_expr == var_symbol: return right_expr elif right_expr == var_symbol: return left_expr # Estrategia 2: Resolver algebraicamente ecuación por ecuación for eq in relevant_eqs: try: single_solution = solve(eq, var_symbol) if single_solution and isinstance(single_solution, list) and single_solution: result = single_solution[0] if result != var_symbol: return result except: continue # Estrategia 3: Resolver el sistema completo para obtener expresiones # en términos de otras variables try: # Obtener todas las variables del sistema excepto la que queremos resolver all_vars = list(self.variables) other_vars = [v for v in all_vars if v != var_symbol] if other_vars: # Intentar resolver el sistema para todas las variables # esto nos dará expresiones en términos de variables libres solution = solve(self.equations, all_vars, dict=True) if solution and var_symbol in solution[0]: return solution[0][var_symbol] # Alternativa: resolver en términos de una variable específica for other_var in other_vars: try: # Resolver el sistema dejando other_var como variable libre vars_to_solve = [v for v in all_vars if v != other_var] if var_symbol in vars_to_solve: partial_solution = solve(self.equations, vars_to_solve, dict=True) if partial_solution and var_symbol in partial_solution[0]: result = partial_solution[0][var_symbol] # Verificar que la solución contenga la otra variable if other_var in result.free_symbols: return result except: continue except: pass # Estrategia 4: Si todo falla, usar la primera ecuación relevante eq = relevant_eqs[0] try: expr_to_solve = eq.lhs - eq.rhs solution = solve(expr_to_solve, var_symbol) if solution: result = solution[0] if isinstance(solution, list) else solution if result != var_symbol: return result except: pass # 5. Si nada funciona, devolver la variable tal como está return var_symbol except Exception as e: self.logger.debug(f"Error resolviendo {var_symbol}: {e}") return var_symbol def _get_numeric_approximation(self, expr) -> Optional[str]: """Obtiene aproximación numérica si es posible""" try: if hasattr(expr, 'evalf'): numeric_val = expr.evalf() # Solo mostrar si es diferente de la forma simbólica if str(numeric_val) != str(expr): # Formatear números con precisión razonable if hasattr(numeric_val, 'is_real') and numeric_val.is_real: try: float_val = float(numeric_val) if abs(float_val) > 1e-10: return f"{float_val:.6f}".rstrip('0').rstrip('.') except: pass return str(numeric_val) return None except: return None def clear_context(self): """Limpia el contexto de evaluación pero mantiene los tipos base""" self.equations.clear() self.variables.clear() self.symbol_table.clear() self.logger.info("Contexto limpio") def get_context_info(self) -> Dict[str, Any]: """Información del contexto actual""" return { "equations": len(self.equations), "variables": list(self.variables), "symbol_table": len(self.symbol_table), "context_size": len(self.unified_context), "tokenization_patterns": len(self.tokenization_patterns), "recent_equations": [str(eq) for eq in self.equations[-5:]] } def _get_full_context(self) -> Dict[str, Any]: """Obtiene el contexto completo para autocompletado (compatibilidad)""" return self._get_complete_context() def get_available_types(self) -> List[str]: """Obtiene tipos disponibles (compatibilidad)""" available_types = [] for name, obj in self.unified_context.items(): if hasattr(obj, '__class__') and hasattr(obj.__class__, '__name__'): if obj.__class__.__name__ not in ['function', 'builtin_function_or_method']: available_types.append(name) return available_types def reload_types(self): """Recarga los tipos dinámicos (compatibilidad)""" self._load_custom_types() self._build_unified_context() self._load_tokenization_patterns() self.logger.info("Tipos y patrones recargados") # ========== FUNCIÓN DE EVALUACIÓN DIRECTA ========== def evaluate_line(line: str, engine: PureAlgebraicEngine = None) -> EvaluationResult: """Función de evaluación directa para uso desde otros módulos""" if engine is None: engine = PureAlgebraicEngine() return engine.evaluate_line(line) # ========== EJEMPLO DE USO ========== if __name__ == "__main__": # Demo del motor unificado engine = PureAlgebraicEngine() test_lines = [ "a = b + 5", # Ecuación con variables "b=?", # ✅ Tokenización: b=? → solve(b) "solve(b)", # ✅ Debería dar: Eq(b, a - 5) "x = 10", # Asignación directa "y = x + 3", # Asignación usando variable "x=?", # ✅ Tokenización: x=? → solve(x) "solve(x)", # ✅ Debería dar: Eq(x, 10) "solve()", # Resolver todo el sistema "ip = IP4(10.1.1.1)", "ip + 1", # ✅ Aritmética IP con _op_priority "16#FF + 1", # ✅ Aritmética con IntBase y _op_priority ] print("=== DEMO MOTOR ALGEBRAICO UNIFICADO ===") print(f"Tipos personalizados cargados: {len([k for k in engine.unified_context.keys() if k in ['IP4', 'FourBytes', 'IntBase']])}") print(f"Patrones de tokenización: {len(engine.tokenization_patterns)}") print() for line in test_lines: result = engine.evaluate_line(line) status = "✅" if result.success else "❌" print(f"{status} {line} → {result.output}") print(f"\nContexto: {engine.get_context_info()}")