Calc/main_evaluation_puro.py

494 lines
20 KiB
Python

#!/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"""
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 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:
output = str(solution)
numeric = self._get_numeric_approximation(solution)
if numeric and str(solution) != str(numeric):
output += f"{numeric}"
else:
output = str(var_symbol)
return EvaluationResult(line, output, "symbolic", True, is_solve_query=True)
elif line.startswith('solve('):
# Función solve() - usar sympify unificado
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}"
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:
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 = [
"x = 5",
"y = x + 3",
"solve()",
"ip = IP4(10.1.1.1)",
"ip + 1", # ✅ Aritmética IP con _op_priority
"ip.to_hex()",
"10.1.1.1", # ✅ Tokenización automática
"16#FF + 1", # ✅ Aritmética con IntBase y _op_priority
"16#FF", # IntBase directo
"16#FF.to_hex()", # Método directo
"n = 16#FF", # Asignación IntBase
"n.to_hex()" # Método del objeto asignado
]
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()}")