494 lines
20 KiB
Python
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()}") |