#!/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 from tl_popup import PlotResult @dataclass class EvaluationResult: """Resultado de evaluación simplificado""" input_line: str output: str result_type: str # e.g., "symbolic", "numeric", "error", "comment", "plot" success: bool error_message: Optional[str] = None is_equation: bool = False is_solve_query: bool = False algebraic_type: Optional[str] = None # Tipo del objeto Python resultante (e.g., "Matrix", "Integer") actual_result_object: Any = None # El objeto Python real del resultado is_assignment: bool = False # True si la línea fue una asignación 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 self.last_result_object = None # Para la variable 'last' # 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, '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. FUNCIÓN SOLVE ESPECIAL que maneja cadenas como símbolos def solve_wrapper(*args, **kwargs): """Wrapper para solve que convierte cadenas a símbolos automáticamente""" processed_args = [] for arg in args: if isinstance(arg, str): # Si es una cadena, convertir a símbolo processed_args.append(sp.Symbol(arg)) elif hasattr(arg, 'is_number') and arg.is_number: # Si es un valor numérico, buscar el símbolo correspondiente # en symbol_table y verificar contexto for var_name, var_value in self.symbol_table.items(): if var_value == arg: # Encontramos una variable con este valor, usar el símbolo processed_args.append(sp.Symbol(var_name)) break else: # Si no se encuentra, intentar usar _smart_solve directo # que puede manejar valores numéricos processed_args.append(arg) else: processed_args.append(arg) result = self._smart_solve(*processed_args, **kwargs) # Si el resultado está vacío, intentar un enfoque alternativo if hasattr(result, '__len__') and len(result) == 0: # Caso especial: si solve() devuelve lista vacía, # intentar resolver como si no hubiera valores asignados alt_args = [] for arg in args: if hasattr(arg, 'is_number') and arg.is_number: # Buscar la variable que tiene este valor for var_name, var_value in self.symbol_table.items(): if var_value == arg: alt_args.append(sp.Symbol(var_name)) break else: alt_args.append(arg) else: alt_args.append(arg) if alt_args != processed_args: # Si hay diferencia, reintentar return self._smart_solve(*alt_args, **kwargs) return result # 4. FUNCIONES DE PLOTTING (WRAPPED) # Wrappers para capturar llamadas de plot y devolver un objeto PlotResult def plot_wrapper(*args, **kwargs): # Intentar extraer la expresión original del primer argumento original_expr = str(args[0]) if args else "" return PlotResult("plot", args, kwargs, original_expr) def plot3d_wrapper(*args, **kwargs): # Intentar extraer la expresión original del primer argumento original_expr = str(args[0]) if args else "" return PlotResult("plot3d", args, kwargs, original_expr) def plot_parametric_wrapper(*args, **kwargs): # Intentar extraer la expresión original del primer argumento original_expr = str(args[0]) if args else "" return PlotResult("plot_parametric", args, kwargs, original_expr) def plot3d_parametric_line_wrapper(*args, **kwargs): # Intentar extraer la expresión original del primer argumento original_expr = str(args[0]) if args else "" return PlotResult("plot3d_parametric_line", args, kwargs, original_expr) plotting_functions = { 'plot': plot_wrapper, 'plot3d': plot3d_wrapper, 'plot_parametric': plot_parametric_wrapper, 'plot3d_parametric_line': plot3d_parametric_line_wrapper, } # 5. COMBINAR TODO EN CONTEXTO UNIFICADO self.unified_context = { **sympy_functions, **registered_types, # IP4, FourBytes, IntBase, etc. **plotting_functions, 'solve': solve_wrapper } # 6. 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""" # 0. TOKENIZACIÓN LATEX: $$ ... $$ → parse_latex y convertir tokenized_line = self._process_latex_inline(line) if tokenized_line != line: self.logger.debug(f"Tokenización LaTeX: '{line}' → '{tokenized_line}'") line = tokenized_line # 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 _process_latex_inline(self, line: str) -> str: """ Procesa código LaTeX en línea detectando $$ ... $$ y convirtiéndolo automáticamente Simplemente convierte LaTeX a expresiones normales y deja que el sistema determine si es asignación, ecuación, etc. Ejemplos: - "$$ resultado = \\frac{a + b}{c} $$" → "resultado = (a + b)/c" - "$$ \\frac{x^2 + 1}{x - 1} $$ + 5" → "(x**2 + 1)/(x - 1) + 5" - "resultado = $$ \\frac{a + b}{c} $$" → "resultado = (a + b)/c" """ try: # Patrón para detectar $$ ... $$ o $ ... $ latex_pattern = r'\$\$([^$]+)\$\$|\$([^$]+)\$' def replace_latex(match): # match.group(1) es para $$ ... $$, match.group(2) es para $ ... $ latex_content = match.group(1) if match.group(1) else match.group(2) try: # Intentar parsear usando Class_LaTeX si está disponible if 'LaTeX' in self.unified_context: LaTeX_class = self.unified_context['LaTeX'] parsed_latex = LaTeX_class.parse_latex(latex_content) converted_expr = parsed_latex.original_expression() # Si es una ecuación Eq(a, b), convertir a "a = b" if hasattr(converted_expr, 'func') and converted_expr.func.__name__ == 'Equality': expr_str = f"{converted_expr.lhs} = {converted_expr.rhs}" else: # Para expresiones normales, convertir a string expr_str = str(converted_expr) self.logger.debug(f"LaTeX convertido: '{latex_content}' → '{expr_str}'") return expr_str except Exception as e: self.logger.debug(f"Error procesando LaTeX '{latex_content}': {e}") # Si falla, devolver el contenido original sin los $$ return latex_content # Aplicar la conversión processed_line = re.sub(latex_pattern, replace_latex, line) return processed_line except Exception as e: self.logger.debug(f"Error en procesamiento LaTeX de línea '{line}': {e}") return line def _get_complete_context(self) -> Dict[str, Any]: """Obtiene contexto completo incluyendo variables del usuario y 'last'""" complete_context = self.unified_context.copy() complete_context.update(self.symbol_table) complete_context['last'] = self.last_result_object # Añadir 'last' al contexto 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('#'): # Devolver la línea como output para que se muestre como comentario return EvaluationResult(line, 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 y gestiona 'last'""" try: if line.startswith('solve('): import re match = re.match(r'solve\(([^)]+)\)', line) if match: var_content = match.group(1).strip() if re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', var_content): var_symbol = sp.Symbol(var_content) solution_result_obj = self._smart_solve(var_symbol) output = str(solution_result_obj) numeric = self._get_numeric_approximation(solution_result_obj) if numeric and str(solution_result_obj) != str(numeric): output += f" ≈ {numeric}" current_algebraic_type = type(solution_result_obj).__name__ # Los resultados de solve() generalmente no son plots self.last_result_object = solution_result_obj return EvaluationResult( input_line=line, output=output, result_type="symbolic", success=True, is_solve_query=True, algebraic_type=current_algebraic_type, actual_result_object=solution_result_obj ) # Para casos más complejos de solve() o si el regex no coincide, # usar _evaluate_expression que ya maneja 'last' y los tipos. # _evaluate_expression se encargará de self.last_result_object. result = self._evaluate_expression(line) # Llama a la versión ya modificada result.is_solve_query = True # Mantener esta bandera return result # Si no es solve(), podría ser otro tipo de atajo (si se añaden más tarde) # Por ahora, si no empieza con solve(, lo tratamos como una expresión normal. return self._evaluate_expression(line) except Exception as e: error_msg = f"Error en atajo de resolución '{line}': {type(e).__name__}: {str(e)}" self.logger.error(error_msg) # No actualizar last_result_object en caso de error return EvaluationResult(line, error_msg, "error", False, str(e), is_solve_query=True) def _evaluate_assignment(self, line: str) -> EvaluationResult: """Evalúa una asignación CON FUNCIONALIDAD DUAL: asignación al contexto + ecuación a sympy""" try: var_name, expression_str = line.split('=', 1) var_name = var_name.strip() expression_str = expression_str.strip() # Validar nombre de variable if not re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', var_name): error_msg = f"Error: Nombre de variable inválido '{var_name}'" self.logger.error(error_msg) return EvaluationResult(line, error_msg, "error", False, error_msg) # Evaluar la expresión del lado derecho # Usar _get_complete_context para incluir 'last' y otras variables eval_context = self._get_complete_context() result_obj = sympify(expression_str, locals=eval_context) # 1. ASIGNACIÓN AL CONTEXTO (para evaluación directa) self.symbol_table[var_name] = result_obj self.variables.add(var_name) # 2. AGREGAR COMO ECUACIÓN A SYMPY (para resolución algebraica) try: # Crear el símbolo de la variable y la ecuación simbólica var_symbol = sp.Symbol(var_name) right_expr = sympify(expression_str, locals=eval_context) equation_obj = Eq(var_symbol, right_expr) # Agregar a la lista de ecuaciones para resolución self.equations.append(equation_obj) # Registrar símbolos de la ecuación for free_symbol in equation_obj.free_symbols: self.variables.add(str(free_symbol)) self.logger.debug(f"Asignación dual agregada: {var_name} = {result_obj} (como ecuación: {equation_obj})") except Exception as eq_error: # Si falla como ecuación, continuar solo con la asignación self.logger.warning(f"No se pudo agregar '{line}' como ecuación: {eq_error}") output = f"{var_name} = {result_obj}" # Devolver el resultado de la asignación return EvaluationResult( input_line=line, output=output, result_type="assignment", # o "symbolic" si queremos que se muestre como tal success=True, is_assignment=True, # Indicar que es una asignación algebraic_type="=", # Marcador para la GUI actual_result_object=result_obj # Guardar el objeto asignado ) except Exception as e: error_msg = f"Error asignando '{line}': {type(e).__name__}: {str(e)}" self.logger.error(error_msg) # No actualizar last_result_object en caso de error return EvaluationResult(line, error_msg, "error", False, str(e), is_assignment=True) def _evaluate_equation(self, line: str) -> EvaluationResult: """Evalúa una ecuación""" try: # Intentar convertir a objeto Ecuación de sympy # Usar _get_complete_context para incluir 'last' y otras variables eval_context = self._get_complete_context() # Dividir la ecuación manualmente para evitar problemas de parsing parts = line.split('=', 1) if len(parts) != 2: raise ValueError("Ecuación debe tener exactamente un signo '='") left_str, right_str = parts[0].strip(), parts[1].strip() left_expr = sympify(left_str, locals=eval_context) right_expr = sympify(right_str, locals=eval_context) equation_obj = Eq(left_expr, right_expr) if not isinstance(equation_obj, sp.Equality): # Si no se pudo parsear como Eq(LHS,RHS), tratar como expresión que contiene un igual (posible error o comparación) # Esto podría ser una comparación booleana que sympify evalúa if isinstance(equation_obj, sp.logic.boolalg.BooleanFunction) or isinstance(equation_obj, bool): # Es una comparación, evaluarla como expresión normal return self._evaluate_expression(line) else: raise ValueError("La expresión no es una ecuación válida.") self.equations.append(equation_obj) # Registrar símbolos de la ecuación para posible autocompletado o análisis futuro for free_symbol in equation_obj.free_symbols: self.variables.add(str(free_symbol)) output = str(equation_obj) # No actualizar last_result_object para ecuaciones return EvaluationResult( input_line=line, output=output, result_type="equation", success=True, is_equation=True, algebraic_type="eq", # Marcador para la GUI actual_result_object=equation_obj ) except Exception as e: error_msg = f"Error en ecuación '{line}': {type(e).__name__}: {str(e)}" self.logger.error(error_msg) # No actualizar last_result_object en caso de error return EvaluationResult(line, error_msg, "error", False, str(e), is_equation=True) def _evaluate_expression(self, line: str) -> EvaluationResult: """Evalúa una expresión usando sympify unificado y gestiona 'last'""" try: # Usar _get_complete_context para incluir 'last' y otras variables eval_context = self._get_complete_context() result_obj = sympify(line, locals=eval_context) # Si es un PlotResult, asegurar que tenga la línea original if isinstance(result_obj, PlotResult) and not result_obj.original_expression: result_obj.original_expression = line output = str(result_obj) result_type_str = "symbolic" # Tipo por defecto current_algebraic_type = type(result_obj).__name__ # Manejo especial para tipos de sympy que pueden necesitar aproximación numérica if hasattr(result_obj, 'evalf') and not isinstance(result_obj, (sp.Matrix, sp.Basic, sp.Expr)): # Evitar evalf en matrices directamente o en tipos ya específicos como Integer, Float pass numeric_approximation = self._get_numeric_approximation(result_obj) if numeric_approximation and output != numeric_approximation: output += f" ≈ {numeric_approximation}" # Considerar si esto cambia el result_type a "numeric_approx" # Determinar si es un objeto de plotting (para no asignarlo a 'last') is_plot_object = False if isinstance(result_obj, PlotResult): is_plot_object = True result_type_str = "plot" # Marcar como plot para la GUI # No es necesario cambiar current_algebraic_type, ya será "PlotResult" # Actualizar last_result_object si no es error y no es plot if not is_plot_object: self.last_result_object = result_obj else: # Si es un plot, no queremos que 'last' lo referencie para evitar problemas # si el plot se cierra o se maneja de forma especial. # Podríamos incluso setear last_result_object a None o al valor previo. # Por ahora, simplemente no lo actualizamos si es plot. pass return EvaluationResult( input_line=line, output=output, result_type=result_type_str, success=True, algebraic_type=current_algebraic_type, actual_result_object=result_obj ) except Exception as e: error_msg = f"Error evaluando '{line}': {type(e).__name__}: {str(e)}" self.logger.error(error_msg) # No actualizar last_result_object en caso de error 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 var_symbol = args[0] solution_value = self._solve_for_variable(var_symbol) # Si encontramos una solución diferente de la variable if solution_value != var_symbol: # Verificar si la solución contiene otras variables free_symbols = solution_value.free_symbols unresolved_vars = free_symbols - {var_symbol} if unresolved_vars: # Hay variables sin resolver, verificar si tienen valores numéricos has_numeric_values = True for var_sym in unresolved_vars: var_name = str(var_sym) if var_name in self.symbol_table: value = self.symbol_table[var_name] # Si el valor es simbólico (no numérico), no podemos resolver completamente if not (hasattr(value, 'is_number') and value.is_number): has_numeric_values = False break else: # Variable no tiene valor asignado has_numeric_values = False break if has_numeric_values: # Todas las variables tienen valores numéricos, resolver iterativamente final_value = self._resolve_iteratively(solution_value) # Auto-aplicar la solución numérica al sistema if final_value != var_symbol and not str(final_value) in ['True', 'False']: self._auto_apply_solution(var_symbol, final_value) return Eq(var_symbol, final_value) else: # Hay variables con valores simbólicos, intentar resolver lo máximo posible # Aplicar resolución iterativa parcial resolved_solution = self._resolve_iteratively(solution_value) result_eq = Eq(var_symbol, resolved_solution) return result_eq else: # No hay variables sin resolver, intentar resolver completamente final_value = self._resolve_iteratively(solution_value) # Auto-aplicar la solución al sistema solo si es un valor específico if final_value != var_symbol and not str(final_value) in ['True', 'False']: self._auto_apply_solution(var_symbol, final_value) return Eq(var_symbol, final_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: value = self.symbol_table[var_name] # Si el valor en symbol_table es diferente de la variable, devolverlo if value != var_symbol: final_value = self._resolve_iteratively(value) return Eq(var_symbol, final_value) # Si no hay información, devolver la variable tal como está 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 ecuaciones que contengan esta variable PRIMERO 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 # Convertir todas las variables a símbolos para sympy all_vars = [sp.Symbol(v) if isinstance(v, str) else v for v in 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 no hay ecuaciones relevantes, buscar en symbol_table como último recurso var_name = str(var_symbol) if var_name in self.symbol_table: value = self.symbol_table[var_name] # Solo devolver si el valor no es la misma variable (evitar bucles) if value != var_symbol: return value # 6. 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 _resolve_iteratively(self, expression, max_iterations=10): """Resuelve una expresión iterativamente sustituyendo valores conocidos""" try: current_expr = expression for iteration in range(max_iterations): # Sustituir valores del symbol_table substituted = current_expr # Sustituir cada variable conocida por su valor for var_name, value in self.symbol_table.items(): var_symbol = sp.Symbol(var_name) if var_symbol in substituted.free_symbols: # Resolver recursivamente el valor antes de sustituir resolved_value = self._resolve_iteratively(value, max_iterations - iteration - 1) if iteration < max_iterations - 1 else value substituted = substituted.subs(var_symbol, resolved_value) # Si no hay cambio, hemos terminado if substituted == current_expr: break current_expr = substituted # Simplificar el resultado if hasattr(current_expr, 'simplify'): current_expr = simplify(current_expr) # Si llegamos a un valor numérico, terminar if current_expr.is_number: break return current_expr except Exception as e: self.logger.debug(f"Error en resolución iterativa: {e}") return expression def _auto_apply_solution(self, var_symbol, solution_value): """Auto-aplica una solución al sistema como si fuera una nueva asignación""" try: var_name = str(var_symbol) # 1. Actualizar symbol_table self.symbol_table[var_name] = solution_value # 2. Buscar y actualizar/reemplazar ecuaciones existentes updated_equations = [] equation_found = False for eq in self.equations: if eq.lhs == var_symbol or eq.rhs == var_symbol: # Reemplazar la ecuación existente new_eq = Eq(var_symbol, solution_value) updated_equations.append(new_eq) equation_found = True else: updated_equations.append(eq) # 3. Si no había ecuación para esta variable, agregar una nueva if not equation_found: new_eq = Eq(var_symbol, solution_value) updated_equations.append(new_eq) # 4. Actualizar la lista de ecuaciones self.equations = updated_equations # 5. Asegurar que la variable esté en el conjunto de variables self.variables.add(var_symbol) self.logger.debug(f"Auto-aplicada solución: {var_symbol} = {solution_value}") except Exception as e: self.logger.debug(f"Error auto-aplicando solución: {e}") 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() 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()}")