Calc/main_evaluation_puro.py

428 lines
17 KiB
Python

#!/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 básico
self.context.update({
'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, # 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,
'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,
})
# Funciones de plotting
try:
from sympy.plotting import plot, plot3d, plot_parametric, plot3d_parametric_line
self.context.update({
'plot': plot,
'plot3d': plot3d,
'plot_parametric': plot_parametric,
'plot3d_parametric_line': plot3d_parametric_line,
})
self.logger.debug("Funciones de plotting cargadas")
except Exception as e:
self.logger.warning(f"Error cargando funciones de plotting: {e}")
# Contexto dinámico de tipos personalizados
dynamic_context = get_registered_base_context()
self.context.update(dynamic_context)
# Verificar que las clases principales estén disponibles
required_classes = ['IP4', 'IP4Mask', 'FourBytes', 'IntBase', 'Hex', 'Bin', 'Dec', 'Chr', 'LaTeX']
missing_classes = []
for cls_name in required_classes:
if cls_name not in self.context:
missing_classes.append(cls_name)
if missing_classes:
self.logger.warning(f"Clases faltantes en contexto: {missing_classes}")
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()}")