Calc/main_evaluation.py

1285 lines
55 KiB
Python

"""
Motor de evaluación híbrida INTEGRADO con el sistema de auto-descubrimiento de tipos
VERSIÓN CORREGIDA siguiendo principios de Guía_Base.md
"""
import sympy
from sympy import symbols, Symbol, sympify, solve, Eq, simplify
from typing import Dict, Any, Optional, Tuple, List, Union
import ast
import re
from contextlib import contextmanager
# Importaciones del sistema de tipos
from type_registry import (
discover_and_register_types,
get_registered_base_context,
get_registered_bracket_classes,
get_registered_helper_functions
)
# Importaciones existentes
from tl_bracket_parser import BracketParser
from tl_popup import PlotResult
# ========== ELIMINADO: Las clases base ahora se obtienen del registro ==========
# Las clases IntBase y FourBytes se cargan dinámicamente desde el registro de tipos
class HybridEvaluationEngine:
"""
Motor de evaluación híbrida que combina SymPy con clases especializadas
VERSIÓN CORREGIDA siguiendo principios de Guía_Base.md
"""
def __init__(self, auto_discover_types: bool = True, types_directory: str = "custom_types"):
# ========== NUEVO: Inicializar parser con tokenización distribuida ==========
self.parser = BracketParser(enable_tokenization=True, debug=False)
# Debug mode (configurar antes que otros módulos)
self.debug = False
self.symbol_table: Dict[str, Any] = {}
self.equations: List[sympy.Eq] = []
self.last_result = None
# Configuración del sistema de tipos
self.types_directory = types_directory
self.auto_discover_enabled = auto_discover_types
# Información de tipos registrados
self.registered_types_info = {}
self.helper_functions = []
# NUEVA CONFIGURACIÓN: Modo simbólico según Guía Base
self.symbolic_mode = True # Por defecto, mantener forma simbólica
self.show_numeric_approximation = True # Mostrar aproximación numérica cuando es útil
self.keep_symbolic_fractions = True # Mantener fracciones como 4/5
self.auto_simplify = False # No simplificar automáticamente
# Configurar contexto base
self._setup_base_context()
def _setup_base_context(self):
"""Configura el contexto base con funciones matemáticas y clases"""
# 1. DESCOBRIR Y REGISTRAR TIPOS AUTOMÁTICAMENTE
if self.auto_discover_enabled:
try:
self.registered_types_info = discover_and_register_types(self.types_directory)
if self.debug:
print(f"🔍 Tipos descubiertos: {self.registered_types_info['class_count']} clases")
except Exception as e:
print(f"⚠️ Error en auto-descubrimiento: {e}")
self.registered_types_info = {
'base_context': {},
'bracket_classes': set(),
'helper_functions': []
}
# 2. FUNCIONES MATEMÁTICAS DE SYMPY (BASE)
math_functions = {
'pi': sympy.pi,
'e': sympy.E,
'I': sympy.I,
'oo': sympy.oo,
'sin': sympy.sin,
'cos': sympy.cos,
'tan': sympy.tan,
'asin': sympy.asin,
'acos': sympy.acos,
'atan': sympy.atan,
'sinh': sympy.sinh,
'cosh': sympy.cosh,
'tanh': sympy.tanh,
'exp': sympy.exp,
'log': sympy.log,
'ln': sympy.log,
'sqrt': sympy.sqrt,
'abs': sympy.Abs,
'sign': sympy.sign,
'floor': sympy.floor,
'ceiling': sympy.ceiling,
'factorial': sympy.factorial,
# Funciones de cálculo
'diff': sympy.diff,
'integrate': sympy.integrate,
'limit': sympy.limit,
'series': sympy.series,
'solve': self._smart_solve,
'simplify': sympy.simplify,
'expand': sympy.expand,
'factor': sympy.factor,
'collect': sympy.collect,
'cancel': sympy.cancel,
'apart': sympy.apart,
'together': sympy.together,
# Álgebra lineal
'Matrix': sympy.Matrix,
'det': lambda m: m.det() if hasattr(m, 'det') else sympy.det(m),
'inv': lambda m: m.inv() if hasattr(m, 'inv') else sympy.Matrix(m).inv(),
# Printing
'latex': sympy.latex, # NUEVO: función latex global
# Plotting (será manejado por resultados interactivos)
'plot': self._create_plot_placeholder,
'plot3d': self._create_plot3d_placeholder,
}
# 3. CLASES ESPECIALIZADAS (DESDE AUTO-DESCUBRIMIENTO)
specialized_classes = self.registered_types_info.get('base_context', {})
# 4. FUNCIONES DE UTILIDAD
utility_functions = {
'_add_equation': self._add_equation,
'_assign_variable': self._assign_variable,
'_solve_variable_in_system': self._solve_variable_in_system,
'clear': self.clear,
'clearContext': self.clearContext,
'help': self._help_function,
'evalf': lambda expr, n=15: expr.evalf(n) if hasattr(expr, 'evalf') else float(expr),
}
# ========== NUEVO: 4.5. CLASES BASE YA ESTÁN EN SPECIALIZED_CLASSES ==========
# Las clases IntBase y FourBytes ya están en specialized_classes desde el registro
# No necesitamos añadirlas por separado
# 5. COMBINAR TODO EN EL CONTEXTO BASE
self.base_context = {
**math_functions,
**specialized_classes,
**utility_functions
}
# 6. ACTUALIZAR HELPER FUNCTIONS
self.helper_functions = get_registered_helper_functions()
# 7. ACTUALIZAR BRACKET PARSER CON CLASES DESCUBIERTAS
self._update_bracket_parser()
if self.debug:
print(f"📋 Contexto base configurado: {len(self.base_context)} entradas")
print(f"🆘 Helper functions: {len(self.helper_functions)}")
# ========== NUEVO: Sincronizar debug con parser ==========
if hasattr(self.parser, 'debug'):
self.parser.debug = self.debug
# ========== NUEVO: Sincronizar debug con tokenizer ==========
if hasattr(self.parser, 'tokenizer') and hasattr(self.parser.tokenizer, 'debug'):
self.parser.tokenizer.debug = self.debug
def _update_bracket_parser(self):
"""Actualiza el BracketParser con las clases descubiertas"""
try:
# NUEVO: Recargar reglas de tokenización del sistema distribuido
if hasattr(self.parser, 'reload_tokenization_rules'):
self.parser.reload_tokenization_rules()
if self.debug:
tokenization_info = self.parser.get_tokenization_info()
print(f"🔧 Reglas de tokenización actualizadas: {len(tokenization_info['rules'])} reglas")
for rule in tokenization_info['rules']:
print(f" {rule['priority']:2d}: {rule['class']} - {rule['description']}")
else:
if self.debug:
print("⚠️ Parser sin soporte para tokenización distribuida")
except Exception as e:
print(f"⚠️ Error actualizando tokenización: {e}")
def reload_types(self):
"""Recarga todos los tipos del directorio (útil para desarrollo)"""
if self.debug:
print("🔄 Recargando tipos...")
self._setup_base_context()
if self.debug:
print("✅ Tipos recargados")
def get_available_types(self) -> Dict[str, Any]:
"""Retorna información completa sobre tipos disponibles"""
return self.registered_types_info
def get_type_help(self, type_name: str) -> Optional[str]:
"""Obtiene ayuda para un tipo específico"""
# Buscar la clase en el registro
classes = self.registered_types_info.get('registered_classes', {})
if type_name in classes:
cls = classes[type_name]
if hasattr(cls, 'Helper'):
return cls.Helper("")
return None
def _create_plot_placeholder(self, *args, **kwargs):
"""Crea un resultado interactivo para plot"""
return PlotResult("plot", args, kwargs)
def _create_plot3d_placeholder(self, *args, **kwargs):
"""Crea un resultado interactivo para plot3d"""
return PlotResult("plot3d", args, kwargs)
def _help_function(self, obj=None):
"""Función de ayuda personalizada"""
if obj is None:
return "Ayuda general disponible. Usa help(objeto) para ayuda específica."
# Si es un tipo personalizado, buscar en helpers
obj_type = type(obj).__name__
for helper_func in self.helper_functions:
try:
help_text = helper_func(obj_type)
if help_text and help_text.strip():
return help_text
except:
continue
# Fallback a ayuda de Python
import pydoc
return pydoc.render_doc(obj, renderer=pydoc.plaintext)
def _smart_solve(self, *args, **kwargs):
"""
Función solve inteligente que usa nuestro sistema de ecuaciones
cuando es apropiado, o llama a sympy.solve en otros casos
"""
try:
# Caso 1: solve(variable) → usar nuestro sistema
if len(args) == 1 and isinstance(args[0], (sympy.Symbol, str)):
var_name = str(args[0])
return self._solve_variable_in_system(var_name)
# Caso 2: solve(ecuacion, variable) → usar sympy.solve directamente
elif len(args) == 2:
return sympy.solve(*args, **kwargs)
# Caso 3: solve(lista_ecuaciones, lista_variables) → usar sympy.solve
elif len(args) >= 1 and isinstance(args[0], (list, tuple)):
return sympy.solve(*args, **kwargs)
# Caso 4: solve() sin argumentos → resolver todas las variables del sistema
elif len(args) == 0:
return self.solve_system()
# Otros casos → usar sympy.solve directamente
else:
return sympy.solve(*args, **kwargs)
except Exception as e:
if self.debug:
print(f"⚠️ Error en _smart_solve: {e}")
# Fallback a sympy.solve
return sympy.solve(*args, **kwargs)
def evaluate_line(self, line: str) -> 'EvaluationResult':
"""
Evalúa una línea de código y retorna el resultado
VERSIÓN CORREGIDA siguiendo principios de Guía_Base.md
"""
try:
# 1. Aplicar tokenización distribuida
parsed_line = self.parser.process_expression(line)
if self.debug:
print(f"Parse: '{line}''{parsed_line}'")
# 2. NUEVA LÓGICA: Detectar atajo =?
if self._is_solve_shortcut(line):
return self._evaluate_solve_shortcut(line)
# 3. Clasificar tipo de línea según criterios de la Guía Base
line_type = self._classify_line(parsed_line, line)
if line_type == "comment":
return EvaluationResult(None, "comment", original_line=line)
elif line_type == "assignment":
return self._evaluate_assignment(parsed_line, line)
elif line_type == "equation":
return self._evaluate_equation_addition(parsed_line, line)
# 4. Evaluación SymPy con evaluación numérica automática
return self._evaluate_sympy_expression(parsed_line, line_type, line)
except Exception as e:
return EvaluationResult(
None, "error",
error=str(e),
original_line=line
)
def _is_solve_shortcut(self, line: str) -> bool:
"""Detecta el atajo variable=? según Guía Base"""
# Patrón: variable=?
pattern = r'^([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*\?\s*$'
return bool(re.match(pattern, line.strip()))
def _evaluate_solve_shortcut(self, line: str) -> 'EvaluationResult':
"""Evalúa el atajo variable=? como solve(variable)"""
try:
# Extraer variable del patrón variable=?
pattern = r'^([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*\?\s*$'
match = re.match(pattern, line.strip())
if not match:
raise ValueError("Formato inválido para =?")
var_name = match.group(1)
# Crear llamada a solve
solve_expression = f"solve({var_name})"
# Evaluar usando _solve_variable_in_system directamente
result = self._solve_variable_in_system(var_name)
return EvaluationResult(
result, "solve_result",
symbolic_result=result,
info=f"Resolviendo {var_name} en el sistema de ecuaciones",
original_line=line
)
except Exception as e:
return EvaluationResult(
None, "error",
error=f"Error en solve shortcut: {e}",
original_line=line
)
def _classify_line(self, parsed_line: str, original_line: str) -> str:
"""
Clasifica el tipo de línea según criterios de la Guía Base
Criterios:
- Asignación: variable = expresión (solo variables simples)
- Ecuación: contiene = Y NO es asignación simple O contiene operadores de comparación
- Comentario: línea vacía o con #
- Expresión: todo lo demás
"""
# Comentarios
if not parsed_line or parsed_line.strip().startswith('#'):
return "comment"
# Verificar si contiene operadores de ecuación
has_equals = '=' in parsed_line
has_comparison = any(op in parsed_line for op in ['==', '!=', '<=', '>=', '<', '>'])
if has_comparison:
return "equation"
if has_equals:
# Verificar si es asignación simple según Guía Base
if self._is_simple_assignment(parsed_line):
return "assignment"
else:
# Es una ecuación (estructura algebraica en ambos lados)
return "equation"
return "expression"
def _is_simple_assignment(self, line: str) -> bool:
"""
Detecta asignaciones simples según Guía Base:
- variable = expresión
- Solo un = (no ==, !=, etc.)
- Lado izquierdo es un identificador válido de Python
"""
try:
# Verificar que solo tiene un = y no es comparación
if line.count('=') != 1 or any(op in line for op in ['==', '!=', '<=', '>=']):
return False
parts = line.split('=', 1)
if len(parts) != 2:
return False
var_part = parts[0].strip()
expr_part = parts[1].strip()
# Verificar que la parte izquierda sea un identificador válido
if not re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', var_part):
return False
# Verificar que la parte derecha no esté vacía
if not expr_part:
return False
return True
except:
return False
def _evaluate_assignment(self, parsed_line: str, original_line: str) -> 'EvaluationResult':
"""
Evalúa asignación de variable según Guía Base
- Guarda el valor simbólico
- Si contiene variables no definidas, también agrega como ecuación implícita
- Proporciona evaluación numérica cuando es útil
"""
try:
if '=' not in parsed_line:
raise ValueError(f"Línea de asignación sin '=': {parsed_line}")
parts = parsed_line.split('=', 1)
var_name = parts[0].strip()
expr_part = parts[1].strip()
# Evaluar la expresión en el contexto actual
result = self._eval_in_context(expr_part)
# Guardar en symbol_table
self.symbol_table[var_name] = result
self.last_result = result
# NUEVO: Si la asignación contiene símbolos no definidos, agregarla como ecuación implícita
if self._assignment_has_undefined_symbols(var_name, result):
try:
# Crear ecuación implícita: var_name = result
var_symbol = sympy.Symbol(var_name)
equation = sympy.Eq(var_symbol, result)
self.equations.append(equation)
if self.debug:
print(f"🔗 Asignación con símbolos agregada como ecuación: {equation}")
except Exception as eq_error:
if self.debug:
print(f"⚠️ No se pudo agregar asignación como ecuación: {eq_error}")
# Generar evaluación numérica según Guía Base
numeric_result = self._generate_numeric_approximation(result)
return EvaluationResult(
result, "assignment",
symbolic_result=result,
numeric_result=numeric_result,
original_line=original_line
)
except Exception as e:
return EvaluationResult(
None, "error",
error=f"Error en asignación: {e}",
original_line=original_line
)
def _assignment_has_undefined_symbols(self, var_name: str, result: Any) -> bool:
"""
Determina si una asignación contiene símbolos no definidos
y por tanto debería agregarse como ecuación implícita
"""
try:
# Obtener símbolos libres del resultado
if hasattr(result, 'free_symbols'):
free_symbols = result.free_symbols
# Verificar si hay símbolos que no están definidos en symbol_table
# (excluyendo el símbolo de la variable que se está asignando)
for symbol in free_symbols:
symbol_name = str(symbol)
if symbol_name != var_name and symbol_name not in self.symbol_table:
return True
# También verificar si el símbolo existe pero es solo un Symbol sin valor
elif symbol_name in self.symbol_table:
symbol_value = self.symbol_table[symbol_name]
if isinstance(symbol_value, sympy.Symbol) and str(symbol_value) == symbol_name:
return True
return False
except Exception as e:
if self.debug:
print(f"⚠️ Error verificando símbolos de asignación: {e}")
return False
def _generate_numeric_approximation(self, result: Any) -> Optional[Any]:
"""
Genera aproximación numérica según Guía Base:
- Solo mostrar cuando la representación string difiere del resultado algebraico
"""
if not self.show_numeric_approximation:
return None
try:
# Intentar evaluación numérica
if hasattr(result, 'evalf'):
numeric_eval = result.evalf()
# Solo mostrar si el string es diferente
if str(numeric_eval) != str(result):
return numeric_eval
elif hasattr(result, '__float__'):
try:
numeric_eval = float(result)
if str(numeric_eval) != str(result):
return numeric_eval
except:
pass
except Exception as e:
if self.debug:
print(f"DEBUG: Error en evaluación numérica: {e}")
return None
def _evaluate_equation_addition(self, parsed_line: str, original_line: str) -> 'EvaluationResult':
"""
Evalúa y agrega ecuación al sistema según Guía Base
- Se agrega automáticamente al sistema de ecuaciones
"""
try:
# Detectar si es ecuación con = o con operadores de comparación
if '=' in parsed_line and '==' not in parsed_line:
# Ecuación con = → convertir a Eq()
parts = parsed_line.split('=', 1)
left_expr = parts[0].strip()
right_expr = parts[1].strip()
# Evaluar ambos lados
left_val = self._eval_in_context(left_expr)
right_val = self._eval_in_context(right_expr)
# Crear ecuación SymPy
equation = sympy.Eq(left_val, right_val)
else:
# Ecuación con operadores de comparación → evaluar directamente
equation = self._eval_in_context(parsed_line)
# Agregar al sistema
self.equations.append(equation)
return EvaluationResult(
equation, "equation_added",
symbolic_result=equation,
info=f"Ecuación agregada al sistema: {equation}",
original_line=original_line
)
except Exception as e:
return EvaluationResult(
None, "error",
error=f"Error agregando ecuación: {e}",
original_line=original_line
)
def _evaluate_sympy_expression(self, expression: str, parse_info: str, original_line: str) -> 'EvaluationResult':
"""
Evalúa una expresión usando SymPy con evaluación numérica automática
"""
try:
# Evaluar en contexto SymPy
result = self._eval_in_context(expression)
if self.debug:
print(f"🔍 Resultado evaluación: {result} (tipo: {type(result)})")
if isinstance(result, PlotResult):
print(f" 📊 Es PlotResult: plot_type={result.plot_type}")
# Actualizar last_result
self.last_result = result
# Generar evaluación numérica automática según Guía Base
numeric_result = self._generate_numeric_approximation(result)
# Determinar tipo de resultado
result_type = type(result).__name__
return EvaluationResult(
result, result_type,
symbolic_result=result,
numeric_result=numeric_result,
parse_info=parse_info,
original_line=original_line
)
except Exception as e:
# Manejar símbolos no definidos
if "undefined" in str(e).lower() or "not defined" in str(e).lower():
return self._handle_undefined_symbols(expression, original_line, e)
return EvaluationResult(
None, "error",
error=str(e),
original_line=original_line
)
def _handle_undefined_symbols(self, expression: str, original_line: str, error: Exception) -> 'EvaluationResult':
"""Maneja símbolos no definidos creándolos automáticamente"""
try:
# Extraer nombres de variables de la expresión
var_names = self._extract_variable_names(expression)
# Crear símbolos automáticamente
new_symbols = {}
for name in var_names:
if name not in self.symbol_table and name not in self.base_context:
new_symbols[name] = Symbol(name)
self.symbol_table[name] = Symbol(name)
if new_symbols:
# Reintentar evaluación
result = self._eval_in_context(expression)
symbol_names = list(new_symbols.keys())
info_msg = f"Símbolos creados: {', '.join(symbol_names)}"
return EvaluationResult(
result, "symbolic_with_new_vars",
symbolic_result=result,
info=info_msg,
original_line=original_line
)
else:
raise error
except Exception as e:
return EvaluationResult(
None, "error",
error=str(e),
original_line=original_line
)
def _extract_variable_names(self, expression: str) -> List[str]:
"""Extrae nombres de variables de una expresión"""
try:
# Usar SymPy para extraer símbolos
expr = sympify(expression, locals=self._get_full_context())
return [str(symbol) for symbol in expr.free_symbols]
except:
# Fallback: usar regex
pattern = r'\b[a-zA-Z_][a-zA-Z0-9_]*\b'
names = re.findall(pattern, expression)
# Filtrar funciones conocidas
return [name for name in names if name not in self.base_context]
def _eval_in_context(self, expression: str) -> Any:
"""Evalúa una expresión en el contexto completo"""
context = self._get_full_context()
# Casos especiales para funciones del sistema
if expression.strip().startswith('_add_equation'):
return eval(expression, {"__builtins__": {}}, context)
elif expression.strip().startswith('_assign_variable'):
# NUEVA LÓGICA: Manejar asignaciones en modo simbólico
# Extraer la expresión de la llamada _assign_variable("var", expresión)
import re
match = re.match(r'_assign_variable\("([^"]+)",\s*(.+)\)', expression.strip())
if match:
var_name = match.group(1)
expr_to_evaluate = match.group(2).strip()
# Evaluar la expresión usando la lógica simbólica
if self.symbolic_mode:
try:
value = sympify(expr_to_evaluate, locals=context, rational=self.keep_symbolic_fractions)
if self.auto_simplify and hasattr(value, 'simplify'):
value = value.simplify()
except:
# Si falla SymPy, usar eval como fallback
value = eval(expr_to_evaluate, {"__builtins__": {}}, context)
else:
# En modo numérico, usar eval
value = eval(expr_to_evaluate, {"__builtins__": {}}, context)
# Asignar directamente usando los valores evaluados
self.symbol_table[var_name] = value
return f"{var_name} = {value}"
else:
# Si no se puede parsear, usar eval como fallback
return eval(expression, {"__builtins__": {}}, context)
else:
# NUEVA LÓGICA: Priorizar SymPy en modo simbólico
if self.symbolic_mode:
try:
# INTERCEPTAR: Detectar operaciones aritméticas con objetos nativos
intercepted_result = self._intercept_native_operations(expression, context)
if intercepted_result is not None:
return intercepted_result
# Primero intentar con SymPy para mantener formas simbólicas
result = sympify(expression, locals=context, rational=self.keep_symbolic_fractions)
# Si auto_simplify está activado, simplificar
if self.auto_simplify and hasattr(result, 'simplify'):
result = result.simplify()
return result
except (SyntaxError, TypeError, ValueError) as sympy_error:
# Si SymPy falla, intentar con eval para objetos especializados
try:
result = eval(expression, {"__builtins__": {}}, context)
# Si el resultado es un objeto híbrido, retornarlo
if hasattr(result, '_sympystr'): # SympyClassBase
return result
elif isinstance(result, PlotResult):
if self.debug:
print(f" 📊 PlotResult detectado en eval: {result}")
return result
elif hasattr(result, '__iter__') and not isinstance(result, str):
return result
else:
# Convertir resultado de eval a SymPy si es posible
try:
return sympify(result, rational=self.keep_symbolic_fractions)
except:
return result
except Exception as eval_error:
# Si ambos fallan, re-lanzar el error más informativo
if "invalid syntax" in str(sympy_error):
raise eval_error
else:
raise sympy_error
else:
# MODO NO SIMBÓLICO: usar lógica original
try:
# Primero intentar evaluación directa para objetos especializados
try:
result = eval(expression, {"__builtins__": {}}, context)
# Si el resultado es un objeto híbrido, integrarlo con SymPy si es necesario
if hasattr(result, '_sympystr'): # SympyClassBase
return result
elif isinstance(result, PlotResult):
if self.debug:
print(f" 📊 PlotResult detectado en eval: {result}")
return result
elif hasattr(result, '__iter__') and not isinstance(result, str):
# Si es una lista/tupla, verificar si contiene objetos híbridos
return result
else:
return result
except (NameError, TypeError) as eval_error:
# Si eval falla, intentar con SymPy
try:
result = sympify(expression, locals=context)
return result
except:
# Si ambos fallan, re-lanzar el error original de eval
raise eval_error
except SyntaxError as syntax_error:
# Para errores de sintaxis, intentar SymPy directamente
try:
result = sympify(expression, locals=context)
return result
except:
raise syntax_error
def _intercept_native_operations(self, expression: str, context: Dict[str, Any]) -> Any:
"""
Intercepta operaciones aritméticas con objetos nativos para evitar
que SymPy las convierta prematuramente
"""
import re
# Detectar patrones de aritmética con FourBytes, IntBase, etc.
# Patrones como: FourBytes(...) + número, número + FourBytes(...), etc.
# Patrón 1: Constructor de tipo + operador + número/expresión
pattern1 = r'(\w+\([^)]+\))\s*([+\-*/])\s*(\d+|\w+)'
# Patrón 2: número/expresión + operador + Constructor de tipo
pattern2 = r'(\d+|\w+)\s*([+\-*/])\s*(\w+\([^)]+\))'
for pattern in [pattern1, pattern2]:
match = re.match(pattern, expression.strip())
if match:
try:
# Evaluar usando eval para que los objetos manejen sus propias operaciones
result = eval(expression, {"__builtins__": {}}, context)
# Verificar si el resultado es un objeto nativo que debe mantenerse
if hasattr(result, '__class__') and result.__class__.__module__ != 'sympy':
# Es un objeto de nuestras clases personalizadas
if hasattr(result, 'original') or hasattr(result, '_numeric_value'):
if self.debug:
print(f"🔧 Interceptando operación nativa: {expression}{result} ({type(result).__name__})")
return result
except Exception as e:
if self.debug:
print(f"🔧 Error en intercepción: {e}")
pass
# Si no se detecta patrón específico, retornar None para continuar con SymPy
return None
def _get_full_context(self) -> Dict[str, Any]:
"""Obtiene el contexto completo para evaluación con sustituciones automáticas"""
context = self.base_context.copy()
# NUEVO: Aplicar sustituciones automáticas en expresiones simbólicas
substituted_table = self._apply_automatic_substitutions()
context.update(substituted_table)
context['last'] = self.last_result
return context
def _apply_automatic_substitutions(self) -> Dict[str, Any]:
"""
Aplica sustituciones automáticas a expresiones simbólicas usando valores conocidos
"""
substituted = {}
# Separar valores numéricos de expresiones simbólicas
numeric_values = {}
symbolic_expressions = {}
for var_name, value in self.symbol_table.items():
if hasattr(value, 'free_symbols') and value.free_symbols:
# Es una expresión simbólica
symbolic_expressions[var_name] = value
else:
# Es un valor numérico o constante
numeric_values[var_name] = value
substituted[var_name] = value
# Crear diccionario de sustituciones con valores numéricos
substitutions = {}
for var_name, value in numeric_values.items():
if hasattr(value, 'evalf'):
# Es un valor SymPy, conservar forma exacta para sustitución
substitutions[sympy.Symbol(var_name)] = value
elif isinstance(value, (int, float)):
# Es un valor numérico Python
substitutions[sympy.Symbol(var_name)] = value
else:
# Otros tipos, intentar convertir a SymPy
try:
substitutions[sympy.Symbol(var_name)] = sympy.sympify(value)
except:
substitutions[sympy.Symbol(var_name)] = value
# Aplicar sustituciones a expresiones simbólicas
for var_name, expr in symbolic_expressions.items():
try:
if substitutions:
# Aplicar todas las sustituciones conocidas
substituted_expr = expr.subs(substitutions)
# Si después de las sustituciones no quedan símbolos libres, evaluar numéricamente
if hasattr(substituted_expr, 'free_symbols') and not substituted_expr.free_symbols:
try:
# Evaluar completamente la expresión
evaluated = substituted_expr.evalf() if hasattr(substituted_expr, 'evalf') else substituted_expr
substituted[var_name] = evaluated
if self.debug:
print(f"🔄 Sustitución automática: {var_name} = {expr}{evaluated}")
except:
# Si falla la evaluación, usar la expresión sustituida
substituted[var_name] = substituted_expr
else:
# Aún hay símbolos libres, usar expresión parcialmente sustituida
substituted[var_name] = substituted_expr
if self.debug and substituted_expr != expr:
print(f"🔄 Sustitución parcial: {var_name} = {expr}{substituted_expr}")
else:
# No hay sustituciones disponibles, mantener expresión original
substituted[var_name] = expr
except Exception as e:
if self.debug:
print(f"⚠️ Error en sustitución automática para {var_name}: {e}")
# En caso de error, mantener expresión original
substituted[var_name] = expr
return substituted
def _assign_variable(self, var_name: str, expression) -> str:
"""Asigna un valor a una variable"""
try:
# Evaluar la expresión usando el contexto completo y configuraciones simbólicas
if isinstance(expression, str):
value = self._eval_in_context(expression)
else:
value = expression
# Asignar al contexto
self.symbol_table[var_name] = value
return f"{var_name} = {value}"
except Exception as e:
raise ValueError(f"Error asignando variable '{var_name}': {e}")
def _add_equation(self, equation_str: str) -> str:
"""Agrega una ecuación al sistema"""
try:
# Parsear ecuación
if '=' in equation_str and '==' not in equation_str:
# Ecuación simple: convertir a igualdad SymPy
left, right = equation_str.split('=', 1)
left_expr = sympify(left.strip(), locals=self._get_full_context())
right_expr = sympify(right.strip(), locals=self._get_full_context())
equation = Eq(left_expr, right_expr)
else:
# Ya es una comparación válida de SymPy
equation = sympify(equation_str, locals=self._get_full_context())
self.equations.append(equation)
return f"Ecuación {len(self.equations)}: {equation}"
except Exception as e:
raise ValueError(f"Error parseando ecuación '{equation_str}': {e}")
def _add_equation_silently(self, equation_str: str) -> bool:
"""
Intenta agregar una ecuación al sistema silenciosamente
Retorna True si tuvo éxito, False si falló
NUEVA FUNCIÓN: Para agregar ecuaciones sin mostrar errores
"""
try:
# Parsear ecuación
if '=' in equation_str and '==' not in equation_str:
# Ecuación simple: convertir a igualdad SymPy
left, right = equation_str.split('=', 1)
left_expr = sympify(left.strip(), locals=self._get_full_context())
right_expr = sympify(right.strip(), locals=self._get_full_context())
equation = Eq(left_expr, right_expr)
else:
# Ya es una comparación válida de SymPy
equation = sympify(equation_str, locals=self._get_full_context())
self.equations.append(equation)
if self.debug:
print(f"🔍 Ecuación agregada silenciosamente: {equation}")
return True
except Exception as e:
if self.debug:
print(f"🔍 No se pudo agregar como ecuación: {e}")
return False
def solve_system(self, variables: Optional[List[str]] = None) -> Dict[str, Any]:
"""Resuelve el sistema de ecuaciones"""
if not self.equations:
raise ValueError("No hay ecuaciones en el sistema")
if variables is None:
# Obtener todas las variables libres
all_symbols = set()
for eq in self.equations:
all_symbols.update(eq.free_symbols)
variables = [str(s) for s in all_symbols]
# Convertir nombres a símbolos
symbol_vars = []
for var_name in variables:
if var_name in self.symbol_table:
symbol_vars.append(self.symbol_table[var_name])
else:
symbol_vars.append(Symbol(var_name))
# Resolver sistema
solutions = solve(self.equations, symbol_vars)
# Convertir resultado a diccionario con nombres de variables
if isinstance(solutions, dict):
result = {}
for symbol, value in solutions.items():
result[str(symbol)] = value
# Actualizar tabla de símbolos
self.symbol_table[str(symbol)] = value
return result
elif isinstance(solutions, list):
# Múltiples soluciones
return {"solutions": solutions}
else:
return {"result": solutions}
def assign_variable(self, name: str, value: Any):
"""Asigna un valor a una variable"""
self.symbol_table[name] = value
def get_variable(self, name: str) -> Optional[Any]:
"""Obtiene el valor de una variable"""
return self.symbol_table.get(name)
def clear_equations(self):
"""Limpia todas las ecuaciones"""
self.equations.clear()
def clear_variables(self):
"""Limpia todas las variables"""
self.symbol_table.clear()
def clear_all(self):
"""Limpia ecuaciones y variables"""
self.clear_equations()
self.clear_variables()
def set_symbolic_mode(self, symbolic_mode: bool = True,
show_numeric: bool = True,
keep_fractions: bool = True,
auto_simplify: bool = False):
"""Configura el modo de evaluación simbólica"""
self.symbolic_mode = symbolic_mode
self.show_numeric_approximation = show_numeric
self.keep_symbolic_fractions = keep_fractions
self.auto_simplify = auto_simplify
def clear(self, *variables) -> str:
"""
Función flexible para limpiar variables
Sin argumentos: Limpia TODAS las variables
Con argumentos: Limpia solo las variables especificadas
Examples:
clear() # Limpia todas las variables
clear("x", "y") # Limpia solo x e y
clear("a") # Limpia solo a
"""
if not variables:
# Sin argumentos: limpiar todas las variables
cleared_count = len(self.symbol_table)
self.symbol_table.clear()
return f"Limpiadas {cleared_count} variables del contexto"
else:
# Con argumentos: limpiar variables específicas
cleared_vars = []
not_found_vars = []
for var_name in variables:
if var_name in self.symbol_table:
del self.symbol_table[var_name]
cleared_vars.append(var_name)
else:
not_found_vars.append(var_name)
result_parts = []
if cleared_vars:
result_parts.append(f"Variables limpiadas: {', '.join(cleared_vars)}")
if not_found_vars:
result_parts.append(f"Variables no encontradas: {', '.join(not_found_vars)}")
return " | ".join(result_parts) if result_parts else "No hay variables para limpiar"
def clearContext(self) -> str:
"""
Limpia completamente el contexto: variables Y ecuaciones
Equivale a hacer clear() + clear_equations()
"""
var_count = len(self.symbol_table)
eq_count = len(self.equations)
self.symbol_table.clear()
self.equations.clear()
return f"Contexto limpiado: {var_count} variables y {eq_count} ecuaciones eliminadas"
def _solve_variable_in_system(self, var_name: str) -> Any:
"""
Resuelve una variable específica usando las ecuaciones del sistema
Esta función reemplaza el comportamiento de solve(variable) para el atajo =?
Implementa mejores prácticas según la documentación de SymPy:
- Usa dict=True para formato consistente
- Especifica explícitamente la variable a resolver
- Manejo robusto de diferentes tipos de resultado
- Manejo especial para sistemas de múltiples variables
- Manejo mejorado de sistemas subdeterminados
"""
# 1. Buscar la variable en el contexto o crearla
var_symbol = self.symbol_table.get(var_name)
if var_symbol is None:
var_symbol = Symbol(var_name)
self.symbol_table[var_name] = var_symbol
# 2. Buscar ecuaciones que contengan esta variable
relevant_equations = []
for eq in self.equations:
if var_symbol in eq.free_symbols:
relevant_equations.append(eq)
if not relevant_equations:
# Si no hay ecuaciones, mostrar el valor actual de la variable
if var_name in self.symbol_table:
current_value = self.symbol_table[var_name]
if isinstance(current_value, Symbol):
return f"'{var_name}' es un símbolo sin valor definido. No hay ecuaciones para resolverlo."
else:
return current_value
else:
return f"No se encontraron ecuaciones para '{var_name}' y la variable no está definida"
# 3. Resolver las ecuaciones para esta variable usando mejores prácticas de SymPy
try:
if self.debug:
print(f"🔍 Resolviendo '{var_name}' usando ecuaciones: {relevant_equations}")
# MEJORA: Detectar si es un sistema de múltiples variables
all_variables = set()
for eq in relevant_equations:
all_variables.update(eq.free_symbols)
if len(all_variables) > 1:
# Sistema de múltiples variables: resolver todo el sistema y extraer la variable deseada
if self.debug:
print(f"🔍 Sistema detectado con variables: {all_variables}")
# ESTRATEGIA MEJORADA: Intentar primero resolver solo la variable deseada
# Si falla, resolver el sistema completo
# Intento 1: Resolver solo la variable específica
try:
solutions_specific = solve(relevant_equations, var_symbol, dict=True)
if self.debug:
print(f"🔍 Soluciones específicas para {var_name}: {solutions_specific}")
if solutions_specific and len(solutions_specific) > 0:
# Éxito con resolución específica
if len(solutions_specific) == 1:
solution_dict = solutions_specific[0]
if var_symbol in solution_dict:
solution = solution_dict[var_symbol]
self.symbol_table[var_name] = solution
return solution
else:
# Múltiples soluciones específicas
solution_values = []
for sol_dict in solutions_specific:
if var_symbol in sol_dict:
solution_values.append(sol_dict[var_symbol])
if solution_values:
# MEJORA: Asignar la lista completa a la variable para permitir t[0], t[1], etc.
self.symbol_table[var_name] = solution_values
return solution_values
except:
if self.debug:
print(f"🔍 Resolución específica falló, probando sistema completo")
# Intento 2: Resolver el sistema completo y extraer
all_vars_list = list(all_variables)
solutions = solve(relevant_equations, all_vars_list, dict=True)
if self.debug:
print(f"🔍 Soluciones del sistema: {solutions}")
# Extraer la variable específica del resultado
if isinstance(solutions, list) and len(solutions) > 0:
solution_dict = solutions[0] # Tomar la primera solución
if var_symbol in solution_dict:
solution = solution_dict[var_symbol]
# Actualizar la variable en el sistema
self.symbol_table[var_name] = solution
# También actualizar las otras variables resueltas
for sym, val in solution_dict.items():
self.symbol_table[str(sym)] = val
return solution
else:
# NUEVO: Variable no en solución directa, intentar despeje algebraico
if self.debug:
print(f"🔍 '{var_name}' no en solución directa, intentando despeje")
# Buscar una ecuación simple que podamos despejar
for eq in relevant_equations:
if len(eq.free_symbols) == 2 and var_symbol in eq.free_symbols:
# Ecuación con 2 variables que incluye la deseada
try:
# Intentar resolver esta ecuación específica para nuestra variable
simple_solution = solve(eq, var_symbol)
if simple_solution:
result = simple_solution[0] if len(simple_solution) == 1 else simple_solution
self.symbol_table[var_name] = result
return result
except:
continue
return f"Variable '{var_name}' no se pudo resolver directamente. Solución del sistema: {solution_dict}"
else:
return f"No se pudo resolver el sistema para '{var_name}'"
else:
# Variable única: usar el método original
solutions = solve(relevant_equations, var_symbol, dict=True)
if self.debug:
print(f"🔍 Soluciones obtenidas: {solutions}")
# Manejar diferentes tipos de resultado según la documentación
if isinstance(solutions, list):
if len(solutions) == 0:
return f"No se encontraron soluciones para '{var_name}'"
elif len(solutions) == 1:
# Una solución única - extraer del diccionario
solution_dict = solutions[0]
if var_symbol in solution_dict:
solution = solution_dict[var_symbol]
# Actualizar la variable en el sistema
self.symbol_table[var_name] = solution
return solution
else:
return f"Solución encontrada pero no contiene '{var_name}': {solution_dict}"
else:
# Múltiples soluciones - extraer valores del diccionario
solution_values = []
for sol_dict in solutions:
if var_symbol in sol_dict:
solution_values.append(sol_dict[var_symbol])
if solution_values:
# MEJORA: Asignar la lista completa a la variable para permitir t[0], t[1], etc.
self.symbol_table[var_name] = solution_values
return solution_values
else:
return f"Múltiples soluciones encontradas pero ninguna contiene '{var_name}': {solutions}"
else:
# Resultado no esperado - mostrar tal como está
return solutions
except Exception as e:
# Información de error más detallada
error_msg = f"Error resolviendo '{var_name}': {e}"
if self.debug:
print(f"🚨 {error_msg}")
print(f"🚨 Ecuaciones que causaron el error: {relevant_equations}")
return error_msg
class EvaluationResult:
"""Resultado de evaluación con información contextual"""
def __init__(self,
result: Any,
result_type: str,
symbolic_result: Any = None,
numeric_result: Any = None,
error: Optional[str] = None,
info: Optional[str] = None,
parse_info: Optional[str] = None,
original_line: Optional[str] = None):
self.result = result
self.result_type = result_type
self.symbolic_result = symbolic_result or result
self.numeric_result = numeric_result
self.error = error
self.info = info
self.parse_info = parse_info
self.original_line = original_line
@property
def is_error(self) -> bool:
return self.result_type == "error"
@property
def is_interactive(self) -> bool:
"""Determina si el resultado requiere interactividad"""
return isinstance(self.result, (PlotResult, sympy.Matrix)) or \
(isinstance(self.result, list) and len(self.result) > 3)
def __str__(self):
if self.is_error:
return f"Error: {self.error}"
elif self.result is not None:
return str(self.result)
return ""