916 lines
42 KiB
Python
916 lines
42 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
|
|
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()}") |