#!/usr/bin/env python3 """ Motor de Evaluación Algebraico Puro para Calculadora MAV ARQUITECTURA SIMPLIFICADA: Todas las líneas con = son ecuaciones """ 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 - Todas las asignaciones son ecuaciones""" def __init__(self): self.logger = logging.getLogger(__name__) self.equations = [] # Lista de ecuaciones Eq() self.variables = set() # Variables conocidas self.context = {} # Contexto de evaluación self.bracket_parser = BracketParser() self.tokenization_patterns = [] # Patrones de tokenización # Cargar tipos personalizados PRIMERO self._load_custom_types() # Cargar contexto base self._load_base_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 _load_base_context(self): """Carga el contexto base con funciones y tipos""" try: # Contexto de SymPy self.context.update({ 'sin': sp.sin, 'cos': sp.cos, 'tan': sp.tan, '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, # Usar nuestro solve inteligente 'Eq': sp.Eq, 'simplify': sp.simplify, 'expand': sp.expand, 'factor': sp.factor, 'diff': sp.diff, 'integrate': sp.integrate, 'Matrix': sp.Matrix, 'symbols': sp.symbols, }) # Contexto dinámico de tipos personalizados dynamic_context = get_registered_base_context() self.context.update(dynamic_context) self.logger.debug(f"Contexto cargado: {len(self.context)} elementos") except Exception as e: self.logger.error(f"Error cargando contexto: {e}") 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""" 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: # Aplicar el patrón con su función de reemplazo 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 evaluate_line(self, line: str) -> EvaluationResult: """Evalúa una línea de entrada""" line = line.strip() if not line or line.startswith('#'): return EvaluationResult(line, "", "comment", True) try: # 1. Aplicar tokenización dinámica PRIMERO tokenized_line = self._apply_tokenization(line) # 2. Preprocesar con bracket parser 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): 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 x=? o solve(x)""" return line.endswith('=?') or 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.endswith('=?'): # Atajo x=? var_name = line[:-2].strip() var_symbol = sp.Symbol(var_name) solution = self._solve_for_variable(var_symbol) # Output conciso if solution != var_symbol: # Si hay solución real output = str(solution) numeric = self._get_numeric_approximation(solution) if numeric and str(solution) != str(numeric): output += f" ≈ {numeric}" else: output = str(var_symbol) # Variable sin resolver return EvaluationResult(line, output, "symbolic", True, is_solve_query=True) elif line.startswith('solve('): # Función solve() 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_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() # Convertir a expresiones SymPy left_expr = sympify(left_str, locals=self.context) right_expr = sympify(right_str, locals=self.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 de una línea output = str(equation) # Evaluación numérica si es posible (solo para lado derecho) 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 libre""" try: # Evaluar con SymPy expr = sympify(line, locals=self.context) # Evaluar la expresión result = expr if hasattr(expr, 'evalf'): # Es expresión SymPy, simplificar result = simplify(expr) 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: # Resolver usando todas las ecuaciones 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}" else: # solve() con argumentos específicos 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: # Intentar resolver la variable en el contexto del sistema solution = solve(self.equations, var_symbol, dict=True) if solution and var_symbol in solution[0]: return solution[0][var_symbol] else: # Intentar resolver solo las ecuaciones que contienen esta variable relevant_eqs = [eq for eq in self.equations if var_symbol in eq.free_symbols] if relevant_eqs: solution = solve(relevant_eqs, var_symbol) if solution: return solution[0] if isinstance(solution, list) else solution 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: # Evitar números muy pequeños 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.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), "context_size": len(self.context), "tokenization_patterns": len(self.tokenization_patterns), "recent_equations": [str(eq) for eq in self.equations[-5:]] # Últimas 5 } def _get_full_context(self) -> Dict[str, Any]: """Obtiene el contexto completo para autocompletado (compatibilidad)""" # Este método es necesario para el autocompletado en la GUI full_context = self.context.copy() # Añadir variables actuales for var in self.variables: full_context[str(var)] = var return full_context def get_available_types(self) -> List[str]: """Obtiene tipos disponibles (compatibilidad)""" available_types = [] for name, obj in self.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_base_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 puro engine = PureAlgebraicEngine() test_lines = [ "x = 5", "y = x + 3", "z = y + x", "x=?", "solve()", "10.1.1.1", # Debería tokenizarse a FourBytes "16#FF", # Debería tokenizarse a IntBase "2#1010" # Debería tokenizarse a IntBase ] print("=== DEMO MOTOR ALGEBRAICO PURO ===") 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()}")