Calc/main_evaluation.py

1119 lines
50 KiB
Python

"""
Motor de evaluación híbrida INTEGRADO con el sistema de auto-descubrimiento de tipos
"""
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 INTEGRADA con auto-descubrimiento de tipos
"""
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
self.symbolic_mode = True # Por defecto, mantener forma simbólica
self.show_numeric_approximation = True # Mostrar aproximación numérica
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': sympy.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 sobre los tipos disponibles"""
return {
'registered_classes': self.registered_types_info.get('registered_classes', {}),
'bracket_classes': list(self.registered_types_info.get('bracket_classes', set())),
'total_context_entries': len(self.base_context),
'helper_functions_count': len(self.helper_functions)
}
def get_type_help(self, type_name: str) -> Optional[str]:
"""Obtiene ayuda para un tipo específico"""
# Buscar en clases registradas
registered_classes = self.registered_types_info.get('registered_classes', {})
if type_name in registered_classes:
cls = registered_classes[type_name]
if hasattr(cls, 'Helper'):
return cls.Helper(type_name)
return None
def _create_plot_placeholder(self, *args, **kwargs):
"""Crear placeholder para plots que será manejado por resultados interactivos"""
if self.debug:
print(f"🎯 Creando PlotResult con args: {args}, kwargs: {kwargs}")
return PlotResult('plot', args, kwargs)
def _create_plot3d_placeholder(self, *args, **kwargs):
"""Crear placeholder para plots 3D"""
if self.debug:
print(f"🎯 Creando PlotResult 3D con args: {args}, kwargs: {kwargs}")
return PlotResult('plot3d', args, kwargs)
def _help_function(self, obj=None):
"""Función de ayuda integrada que usa el sistema de helpers"""
if obj is None:
return "Ayuda disponible. Use help(función) para ayuda específica."
# Primero intentar con el objeto directamente
if hasattr(obj, '__doc__') and obj.__doc__:
return obj.__doc__
elif hasattr(obj, 'Helper'):
return obj.Helper("")
# Luego buscar en helpers registrados
obj_name = getattr(obj, '__name__', str(obj))
for helper_func in self.helper_functions:
try:
help_result = helper_func(obj_name)
if help_result:
return help_result
except:
continue
return f"No hay ayuda disponible para {obj}"
# ========== RESTO DE MÉTODOS EXISTENTES ==========
# (Los métodos de evaluación permanecen igual)
def evaluate_line(self, line: str) -> 'EvaluationResult':
"""
Evalúa una línea de código y retorna el resultado
NUEVA LÓGICA: Priorizar asignaciones, intentar ecuaciones silenciosamente
"""
try:
# 1. Aplicar tokenización distribuida
parsed_line = self.parser.process_expression(line)
if self.debug:
print(f"Parse: '{line}''{parsed_line}'")
# 2. Clasificar tipo de línea
line_type = self._classify_line(parsed_line)
if line_type == "comment":
return EvaluationResult(None, "comment", original_line=line)
elif line_type == "assignment":
# NUEVA LÓGICA: Para asignaciones, también intentar agregar como ecuación silenciosamente
assignment_result = self._evaluate_assignment(parsed_line, line)
# Intentar agregar como ecuación silenciosamente (sin mostrar errores)
if not assignment_result.is_error:
try:
self._add_equation_silently(line)
except:
pass # Ignorar errores de ecuación
return assignment_result
elif line_type == "equation":
return self._evaluate_equation_addition(parsed_line, line)
# 3. Evaluación SymPy
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 _classify_line(self, parsed_line: str) -> str:
"""Clasifica el tipo de línea después del parsing"""
# Simplificado: priorizar asignaciones, ser menos estricto
if self._is_assignment(parsed_line):
return "assignment"
elif self._is_standalone_equation(parsed_line):
return "equation"
elif not parsed_line or parsed_line.strip().startswith('#'):
return "comment"
else:
return "expression"
def _is_assignment(self, line: str) -> bool:
"""
Detecta si una línea es una asignación de variable
NUEVA LÓGICA: Priorizar asignaciones, ser menos estricto
"""
try:
# Pattern: variable = expresión (que no sea comparación)
if '=' in line and not any(op in line for op in ['==', '!=', '<=', '>=']):
# Verificar que sea una asignación válida de Python
parts = line.split('=', 1)
if len(parts) == 2:
var_part = parts[0].strip()
expr_part = parts[1].strip()
# Verificar que la parte izquierda sea un identificador válido
if re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', var_part) and expr_part:
return True
return False
except:
return False
def _is_standalone_equation(self, line: str) -> bool:
"""
Determina si una línea es una ecuación standalone
NUEVA LÓGICA: Solo ecuaciones matemáticas obvias, no asignaciones
"""
try:
# Primero verificar si contiene '=' simple
if '=' not in line or any(op in line for op in ['==', '!=', '<=', '>=']):
return False
# NUEVA LÓGICA: Si ya fue clasificada como asignación, NO es ecuación
if self._is_assignment(line):
return False
# Solo considerar como ecuación si tiene estructura matemática compleja
# Por ejemplo: expressions con funciones matemáticas, símbolos algebraicos, etc.
if any(pattern in line for pattern in ['sin(', 'cos(', 'log(', 'sqrt(', 'diff(', 'integrate(']):
return True
# O si contiene múltiples variables en un formato algebraico
# Básicamente, ecuaciones que NO son asignaciones simples
return False
except:
return False
def _evaluate_assignment(self, parsed_line: str, original_line: str) -> 'EvaluationResult':
"""Maneja la asignación de variables"""
try:
# ARREGLO: Transformar asignación en llamada a _assign_variable
# parsed_line es algo como: mask=FourBytes("255.240.0.3")
# Necesitamos convertirlo en: _assign_variable("mask", FourBytes("255.240.0.3"))
if '=' in parsed_line:
parts = parsed_line.split('=', 1)
var_name = parts[0].strip()
expr_part = parts[1].strip()
# Crear llamada a _assign_variable
assign_call = f'_assign_variable("{var_name}", {expr_part})'
if self.debug:
print(f"🔧 Transformando asignación: '{parsed_line}''{assign_call}'")
# Ejecutar la asignación transformada
result = self._eval_in_context(assign_call)
# Obtener el valor asignado
assigned_value = self.symbol_table.get(var_name)
else:
# Fallback: si no hay '=' algo está mal
raise ValueError(f"Línea de asignación sin '=': {parsed_line}")
# Generar evaluación numérica si está configurado para mostrarla
numeric_result = None
if self.show_numeric_approximation and hasattr(assigned_value, 'evalf'):
try:
numeric_eval = assigned_value.evalf()
# MEJORADO: Solo mostrar aproximación si es realmente útil
if hasattr(assigned_value, 'is_Rational') and assigned_value.is_Rational:
# Es una fracción racional, mostrar aproximación decimal
numeric_result = numeric_eval
elif hasattr(assigned_value, 'is_Integer') and assigned_value.is_Integer:
# Es un entero SymPy, no mostrar aproximación
numeric_result = None
elif hasattr(assigned_value, 'is_number') and assigned_value.is_number:
# Es un número, verificar si la aproximación es diferente significativamente
try:
if abs(float(numeric_eval) - float(assigned_value)) > 1e-10:
numeric_result = numeric_eval
except:
# Si no se puede comparar, mostrar solo si el string es diferente
if str(numeric_eval) != str(assigned_value):
numeric_result = numeric_eval
elif numeric_eval != assigned_value:
# Para otros casos, mostrar si son diferentes
try:
# Intentar comparación numérica más robusta
if abs(float(numeric_eval) - float(assigned_value)) > 1e-10:
numeric_result = numeric_eval
except:
# Si la comparación falla, asumir que son diferentes solo si el string es diferente
if str(numeric_eval) != str(assigned_value):
numeric_result = numeric_eval
except Exception as e:
if self.debug:
print(f"DEBUG: Error en evaluación numérica: {e}")
pass
return EvaluationResult(
assigned_value, "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 _evaluate_equation_addition(self, parsed_line: str, original_line: str) -> 'EvaluationResult':
"""Maneja la adición de ecuaciones al sistema"""
try:
# Ejecutar _add_equation
result = self._eval_in_context(parsed_line)
return EvaluationResult(
result, "equation_added",
symbolic_result=f"Ecuación agregada: {original_line}",
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"""
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
# Intentar evaluación numérica si está configurado para mostrarla
numeric_result = None
if self.show_numeric_approximation and hasattr(result, 'evalf'):
try:
numeric_eval = result.evalf()
# MEJORADO: Solo mostrar evaluación numérica si es realmente útil
if hasattr(result, 'is_Integer') and result.is_Integer:
# Es un entero SymPy, no mostrar aproximación
numeric_result = None
elif hasattr(result, 'is_Rational') and result.is_Rational:
# Es una fracción racional, mostrar aproximación decimal
numeric_result = numeric_eval
elif hasattr(result, 'is_number') and result.is_number:
# Es un número, verificar si la aproximación es diferente significativamente
try:
if abs(float(numeric_eval) - float(result)) > 1e-10:
numeric_result = numeric_eval
except:
# Si no se puede comparar, mostrar solo si el string es diferente
if str(numeric_eval) != str(result):
numeric_result = numeric_eval
elif (str(numeric_eval) != str(result) and numeric_eval != result and
not (isinstance(result, (int, float)) or
(hasattr(result, 'is_number') and result.is_number and
hasattr(result, 'is_Integer') and result.is_Integer))):
numeric_result = numeric_eval
except:
pass
return EvaluationResult(
result, "expression",
symbolic_result=result,
numeric_result=numeric_result,
parse_info=parse_info,
original_line=original_line
)
except NameError as e:
# Intentar crear símbolos automáticamente
return self._handle_undefined_symbols(expression, original_line, e)
except Exception as 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"""
context = self.base_context.copy()
context.update(self.symbol_table)
context['last'] = self.last_result
return context
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 ""