From 048812222907cfb55b482a3d63e929ffaf7350bb Mon Sep 17 00:00:00 2001 From: Miguel Date: Fri, 6 Jun 2025 16:32:25 +0200 Subject: [PATCH] =?UTF-8?q?Eliminaci=C3=B3n=20del=20archivo=20de=20histori?= =?UTF-8?q?al=20de=20c=C3=A1lculos=20y=20ajustes=20en=20la=20configuraci?= =?UTF-8?q?=C3=B3n=20de=20la=20interfaz.=20Se=20mejora=20la=20gesti=C3=B3n?= =?UTF-8?q?=20de=20resultados=20interactivos,=20incluyendo=20la=20actualiz?= =?UTF-8?q?aci=C3=B3n=20del=20panel=20de=20entrada=20al=20editar=20expresi?= =?UTF-8?q?ones.=20Se=20optimizan=20los=20tags=20de=20salida=20para=20una?= =?UTF-8?q?=20mejor=20visualizaci=C3=B3n=20de=20resultados=20y=20se=20impl?= =?UTF-8?q?ementa=20un=20nuevo=20sistema=20de=20redibujo=20para=20gr=C3=A1?= =?UTF-8?q?ficos=20interactivos.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- hybrid_calc_history.txt | 30 - hybrid_calc_settings.json | 4 +- main_calc_app.py | 233 +++++-- main_evaluation_OLD.py | 1284 ------------------------------------- main_evaluation_puro.py | 288 ++++++--- tl_popup.py | 173 ++++- 6 files changed, 532 insertions(+), 1480 deletions(-) delete mode 100644 hybrid_calc_history.txt delete mode 100644 main_evaluation_OLD.py diff --git a/hybrid_calc_history.txt b/hybrid_calc_history.txt deleted file mode 100644 index 5f6c993..0000000 --- a/hybrid_calc_history.txt +++ /dev/null @@ -1,30 +0,0 @@ - - -y = 2*x + 3 -x = z * 4 -z = 8 -y=? -z=? - - -x=? - # Instanciación via sympify -ip = IP4(120.11.255.2,30) -ip.Nodes() -ip.to_hex() -ip + 1 -10.1.1.1 -p=Dec(16#FF + 16#FF) / 18 -p -t= 2#1010 + 16#f -t.to_base(8) - - -ip.bit_representation() -ip=ip+20 -ip.bit_representation() -ip.mask() -ip.get_prefix_length() - -a = b + 5 -solve(x) \ No newline at end of file diff --git a/hybrid_calc_settings.json b/hybrid_calc_settings.json index 8b1e1ad..85f587a 100644 --- a/hybrid_calc_settings.json +++ b/hybrid_calc_settings.json @@ -1,6 +1,6 @@ { - "window_geometry": "1020x700+356+1216", - "sash_pos_x": 347, + "window_geometry": "1020x700+387+1235", + "sash_pos_x": 355, "symbolic_mode": true, "show_numeric_approximation": true, "keep_symbolic_fractions": true, diff --git a/main_calc_app.py b/main_calc_app.py index 5035dae..444b5c9 100644 --- a/main_calc_app.py +++ b/main_calc_app.py @@ -298,6 +298,8 @@ CARACTERÍSTICAS: def setup_interactive_manager(self): """Configura el gestor de resultados interactivos""" self.interactive_manager = InteractiveResultManager(self.root) + # Configurar callback para actualizar el panel de entrada cuando se edite una expresión + self.interactive_manager.set_update_callback(self._update_input_expression) def create_menu(self): """Crea el menú de la aplicación""" @@ -372,25 +374,38 @@ CARACTERÍSTICAS: self.output_text.bind("", _unified_mouse_wheel) def setup_output_tags(self): - """Configura tags para coloreado de salida""" - self.output_text.tag_configure("error", foreground="#ff6b6b", font=("Consolas", 11, "bold")) - self.output_text.tag_configure("result", foreground="#abdbe3") - self.output_text.tag_configure("symbolic", foreground="#82aaff") - self.output_text.tag_configure("numeric", foreground="#c3e88d") - self.output_text.tag_configure("equation", foreground="#c792ea") - self.output_text.tag_configure("info", foreground="#ffcb6b") - self.output_text.tag_configure("comment", foreground="#546e7a") - self.output_text.tag_configure("class_hint", foreground="#888888") - self.output_text.tag_configure("type_hint", foreground="#6a6a6a") + """Configura tags de formato para el panel de salida""" + default_font = self._get_input_font() - # Tags para tipos especializados (genéricos para cualquier tipo) - self.output_text.tag_configure("custom_type", foreground="#f9a825") - self.output_text.tag_configure("hex", foreground="#f9a825") - self.output_text.tag_configure("bin", foreground="#4fc3f7") - self.output_text.tag_configure("ip", foreground="#fff176") - self.output_text.tag_configure("date", foreground="#ff8a80") - self.output_text.tag_configure("chr_type", foreground="#80cbc4") - self.output_text.tag_configure("helper", foreground="#ffd700", font=("Consolas", 11, "italic")) + # Crear una fuente específica para errores (bold) + error_font = tkFont.Font(family=default_font.cget("family"), size=default_font.cget("size"), weight="bold") + + # Tag base + self.output_text.tag_configure("base", font=default_font, foreground="#d4d4d4") + + # Tags específicos + # Sympy y tipos base + self.output_text.tag_configure("symbolic", foreground="#9cdcfe") # Azul claro (SymPy) + self.output_text.tag_configure("numeric", foreground="#b5cea8") # Verde (Números) + self.output_text.tag_configure("boolean", foreground="#569cd6") # Azul (Booleanos) + self.output_text.tag_configure("string", foreground="#ce9178") # Naranja (Strings) + + # Tipos registrados dinámicamente (usar un color base) + self.output_text.tag_configure("custom_type", foreground="#4ec9b0") # Turquesa (Tipos Custom) + + # Estado de la aplicación + self.output_text.tag_configure("error", foreground="#f44747", font=error_font) # Rojo + self.output_text.tag_configure("comment", foreground="#6a9955") # Verde Oliva (Comentarios) + self.output_text.tag_configure("assignment", foreground="#dcdcaa") # Amarillo (Asignaciones) + self.output_text.tag_configure("equation", foreground="#c586c0") # Púrpura (Ecuaciones) + self.output_text.tag_configure("plot", foreground="#569cd6", underline=True) # Azul con subrayado (Plots) + + # Para el nuevo indicador de tipo algebraico + self.output_text.tag_configure("type_indicator", foreground="#808080") # Gris oscuro + + # Configurar tags para tipos específicos si es necesario (ejemplo) + # self.output_text.tag_configure("IP4", foreground="#4ec9b0") + # self.output_text.tag_configure("IntBase", foreground="#4ec9b0") def on_key_release(self, event=None): """Maneja eventos de teclado""" @@ -670,28 +685,99 @@ CARACTERÍSTICAS: self._display_output(output_data) def _process_evaluation_result(self, result: EvaluationResult) -> List[tuple]: - """Procesa el resultado de evaluación para display - ADAPTADO AL MOTOR PURO""" + """Procesa el resultado de evaluación para display, priorizando resultados interactivos.""" output_parts = [] - + indicator_text: Optional[str] = None + + if result.result_type == "comment": + output_parts.append(("comment", result.output if result.output is not None else "")) + return output_parts + if not result.success: - # Error - ayuda = self.obtener_ayuda(result.input_line) - if ayuda: - ayuda_linea = ayuda.replace("\n", " ").replace("\r", " ") - if len(ayuda_linea) > 120: + # Manejo de errores + error_prefix = "Error: " + main_error_message = f"{error_prefix}{result.error_message}" + + # Intentar obtener ayuda contextual para el error + ayuda_text = self.obtener_ayuda(result.input_line) # obtener_ayuda devuelve string o None + if ayuda_text: + ayuda_linea = ayuda_text.replace("\n", " ").replace("\r", " ").strip() + if len(ayuda_linea) > 120: # Acortar si es muy larga ayuda_linea = ayuda_linea[:117] + "..." - output_parts.append(("helper", ayuda_linea)) + + # Mostrar primero el error principal, luego la sugerencia + output_parts.append(("error", main_error_message)) + output_parts.append( ("\n", "\n") ) # Separador para la ayuda + output_parts.append(("helper", f"Sugerencia: {ayuda_linea}")) else: - output_parts.append(("error", f"Error: {result.error_message}")) - elif result.result_type == "comment": - output_parts.append(("comment", result.input_line)) - elif result.result_type == "equation": - output_parts.append(("equation", result.output)) - elif result.result_type == "symbolic": - output_parts.append(("symbolic", result.output)) + output_parts.append(("error", main_error_message)) + # No se añade type_indicator para errores aquí, el mensaje de error es suficiente. + else: - # Resultado general - output_parts.append(("result", result.output)) + # RESULTADO EXITOSO: + # 1. Intentar crear un tag interactivo + interactive_tag_info = self.interactive_manager.create_interactive_tag( + result.actual_result_object, + self.output_text + ) + + if interactive_tag_info: + tag_name, display_text = interactive_tag_info + output_parts.append((tag_name, display_text)) + + # Añadir también el indicador de tipo algebraico + if result.algebraic_type: + indicator_text = f"[{result.algebraic_type}]" + output_parts.append((" ", " ")) + output_parts.append(("type_indicator", indicator_text)) + else: + # 2. Si no es interactivo, usar la lógica de formato de texto anterior + main_output_tag = "base" + + if result.is_assignment: + main_output_tag = "assignment" + indicator_text = "[=]" + elif result.is_equation: + main_output_tag = "equation" + indicator_text = "[eq]" + elif result.result_type == "plot": + main_output_tag = "plot" + # Este caso es un fallback si create_interactive_tag no lo manejó + else: + # Lógica para determinar el tag principal y el texto del indicador + if result.algebraic_type: + type_lower = result.algebraic_type.lower() + if type_lower in self.output_text.tag_names(): + main_output_tag = type_lower + elif isinstance(result.actual_result_object, sympy.Basic): + main_output_tag = "symbolic" + elif type_lower in ["int", "float", "complex"] or isinstance(result.actual_result_object, (int, float)): + main_output_tag = "numeric" + elif type_lower == "bool" or isinstance(result.actual_result_object, bool): + main_output_tag = "boolean" + elif type_lower == "str" or isinstance(result.actual_result_object, str): + main_output_tag = "string" + elif result.actual_result_object is not None and \ + not isinstance(result.actual_result_object, (sympy.Basic, int, float, bool, str, list, dict, tuple, type(None))): + main_output_tag = "custom_type" + else: + main_output_tag = "symbolic" + else: + main_output_tag = "symbolic" + + if result.algebraic_type: + is_collection = any(kw in result.algebraic_type.lower() for kw in ["matrix", "list", "dict", "tuple", "vector", "array"]) + is_custom_obj_tag = (main_output_tag == "custom_type") + is_non_trivial_sympy = isinstance(result.actual_result_object, sympy.Basic) and \ + result.algebraic_type not in ["Symbol", "Expr", "Integer", "Float", "Rational", "BooleanTrue", "BooleanFalse"] + if is_collection or is_custom_obj_tag or is_non_trivial_sympy: + indicator_text = f"[{result.algebraic_type}]" + + output_parts.append((main_output_tag, result.output if result.output is not None else "")) + + if indicator_text: + output_parts.append((" ", " ")) + output_parts.append(("type_indicator", indicator_text)) return output_parts @@ -1119,14 +1205,41 @@ programación y análisis numérico. self.logger.error(f"Error guardando historial: {e}", exc_info=True) def on_close(self): - """Maneja cierre de la aplicación""" - self.save_history() - self._save_settings() - - if self.interactive_manager: - self.interactive_manager.close_all_windows() - - self.root.destroy() + """Maneja cierre de la aplicación de forma completa""" + try: + # Guardar historial y configuraciones + self.save_history() + self._save_settings() + + # Cerrar todas las ventanas interactivas + if self.interactive_manager: + self.interactive_manager.close_all_windows() + + # Detener cualquier job pendiente + if self._debounce_job: + self.root.after_cancel(self._debounce_job) + + # Cerrar autocompletado si está abierto + self._close_autocomplete_popup() + + # Asegurar que matplotlib libere recursos + try: + import matplotlib.pyplot as plt + plt.close('all') + except: + pass + + # Forzar la salida del mainloop + self.root.quit() + + except Exception as e: + self.logger.error(f"Error durante el cierre: {e}") + finally: + # Destruir la ventana principal como último recurso + try: + self.root.destroy() + except: + pass def show_help_window(self): """Muestra ventana de ayuda con archivo externo - NUEVO SISTEMA""" @@ -1441,6 +1554,40 @@ Para crear un archivo de ayuda personalizado, cree un archivo `readme.md` en el self._evaluate_and_update() self._adjust_input_pane_width() + def _update_input_expression(self, original_expression, new_expression): + """Actualiza el panel de entrada reemplazando la expresión original con la nueva""" + try: + # Obtener todo el contenido actual + current_content = self.input_text.get("1.0", tk.END).rstrip('\n') + + # Buscar y reemplazar la expresión original + if original_expression in current_content: + updated_content = current_content.replace(original_expression, new_expression, 1) + + # Actualizar el panel de entrada + self.input_text.delete("1.0", tk.END) + self.input_text.insert("1.0", updated_content) + + # Evaluar automáticamente la nueva expresión + self._evaluate_and_update() + + self.logger.info(f"Expresión actualizada: '{original_expression}' -> '{new_expression}'") + else: + # Si no se encuentra la expresión original, agregar la nueva al final + if current_content and not current_content.endswith('\n'): + current_content += '\n' + updated_content = current_content + new_expression + + self.input_text.delete("1.0", tk.END) + self.input_text.insert("1.0", updated_content) + + self.logger.info(f"Expresión agregada: '{new_expression}'") + + except Exception as e: + self.logger.error(f"Error actualizando expresión: {e}") + # Fallback: simplemente insertar la nueva expresión + self.input_text.insert(tk.END, f"\n{new_expression}") + def main(): """Función principal""" diff --git a/main_evaluation_OLD.py b/main_evaluation_OLD.py deleted file mode 100644 index 97cbd19..0000000 --- a/main_evaluation_OLD.py +++ /dev/null @@ -1,1284 +0,0 @@ -""" -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 "" diff --git a/main_evaluation_puro.py b/main_evaluation_puro.py index 8ea1076..b831e9d 100644 --- a/main_evaluation_puro.py +++ b/main_evaluation_puro.py @@ -22,6 +22,7 @@ from type_registry import ( discover_and_register_types ) from tl_bracket_parser import BracketParser +from tl_popup import PlotResult @dataclass @@ -29,11 +30,14 @@ class EvaluationResult: """Resultado de evaluación simplificado""" input_line: str output: str - result_type: 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: @@ -47,6 +51,7 @@ class PureAlgebraicEngine: 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() @@ -93,18 +98,34 @@ class PureAlgebraicEngine: # 2. TIPOS PERSONALIZADOS REGISTRADOS (CLAVE PARA INSTANCIACIÓN) registered_types = get_registered_base_context() - # 3. FUNCIONES DE PLOTTING - try: - from sympy.plotting import plot, plot3d, plot_parametric, plot3d_parametric_line - plotting_functions = { - 'plot': plot, - 'plot3d': plot3d, - 'plot_parametric': plot_parametric, - 'plot3d_parametric_line': plot3d_parametric_line, - } - except Exception as e: - self.logger.warning(f"Error cargando funciones de plotting: {e}") - plotting_functions = {} + # 3. 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, + } # 4. COMBINAR TODO EN CONTEXTO UNIFICADO self.unified_context = { @@ -173,16 +194,18 @@ class PureAlgebraicEngine: return tokenized_line def _get_complete_context(self) -> Dict[str, Any]: - """Obtiene contexto completo incluyendo variables del usuario""" + """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('#'): - return EvaluationResult(line, "", "comment", True) + # 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 @@ -222,134 +245,189 @@ class PureAlgebraicEngine: return any(op in line for op in comparison_ops) def _evaluate_solve_shortcut(self, line: str) -> EvaluationResult: - """Evalúa atajos de resolución""" + """Evalúa atajos de resolución y gestiona 'last'""" try: if line.startswith('solve('): - # Función solve() - manejar casos especiales primero - # Extraer el contenido dentro de solve() import re match = re.match(r'solve\(([^)]+)\)', line) if match: var_content = match.group(1).strip() - # Si es una variable simple, usar nuestra lógica mejorada if re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', var_content): - # Crear símbolo directamente sin usar context para evitar sustitución var_symbol = sp.Symbol(var_content) - solution_result = self._smart_solve(var_symbol) - output = str(solution_result) - numeric = self._get_numeric_approximation(solution_result) - if numeric and str(solution_result) != str(numeric): + 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}" - return EvaluationResult(line, output, "symbolic", True, is_solve_query=True) + + 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, usar sympify - result = self._evaluate_expression(line) - result.is_solve_query = True + # 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 resolución: {str(e)}" - return EvaluationResult(line, error_msg, "error", False, str(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 manteniendo doble registro""" + """Evalúa una asignación""" try: - # Separar variable = expresión - var_name, expr_str = line.split('=', 1) + var_name, expression_str = line.split('=', 1) var_name = var_name.strip() - expr_str = expr_str.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 - context = self._get_complete_context() - result = sympify(expr_str, locals=context) + # 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 DIRECTA (para uso inmediato) - self.symbol_table[var_name] = result - # Variable asignada correctamente + # Actualizar tabla de símbolos + self.symbol_table[var_name] = result_obj + self.variables.add(var_name) - # 2. ECUACIÓN IMPLÍCITA (para solve) - var_symbol = sp.Symbol(var_name) - equation = Eq(var_symbol, result) - self.equations.append(equation) + output = f"{var_name} = {result_obj}" - # Añadir símbolo a variables conocidas - self.variables.add(var_symbol) - - # Output conciso - mostrar el valor asignado - output = str(result) - - # Añadir aproximación numérica si es útil - numeric = self._get_numeric_approximation(result) - if numeric and str(result) != str(numeric): - output += f" ≈ {numeric}" - - return EvaluationResult(line, output, "assignment", True) + # 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 en asignación: {str(e)}" - return EvaluationResult(line, error_msg, "error", False, str(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 y la añade al sistema""" + """Evalúa una ecuación""" try: - # Separar left = right - left_str, right_str = line.split('=', 1) - left_str = left_str.strip() - right_str = right_str.strip() + # Intentar convertir a objeto Ecuación de sympy + # Usar _get_complete_context para incluir 'last' y otras variables + eval_context = self._get_complete_context() + equation_obj = sympify(line, locals=eval_context, parse_function=lambda s: Eq(*s.split('=',1))) + + 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.") - # USAR SYMPIFY UNIFICADO para ambos lados - context = self._get_complete_context() - left_expr = sympify(left_str, locals=context) - right_expr = sympify(right_str, locals=context) + 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)) - # Crear ecuación - equation = Eq(left_expr, right_expr) - - # Añadir al sistema - self.equations.append(equation) - - # Extraer variables - eq_vars = equation.free_symbols - self.variables.update(eq_vars) - - # Output conciso - output = str(equation) - - # Evaluación numérica si es posible - numeric = self._get_numeric_approximation(equation.rhs) - if numeric and str(equation.rhs) != str(numeric): - output += f" ≈ {numeric}" - - return EvaluationResult(line, output, "equation", True, is_equation=True) + 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: {str(e)}" - return EvaluationResult(line, error_msg, "error", False, str(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 ÚNICAMENTE""" + """Evalúa una expresión usando sympify unificado y gestiona 'last'""" try: - # USAR SYMPIFY UNIFICADO - Un solo parser - context = self._get_complete_context() - result = sympify(line, locals=context) + # Usar _get_complete_context para incluir 'last' y otras variables + eval_context = self._get_complete_context() + result_obj = sympify(line, locals=eval_context) - # Nota: Las asignaciones ahora se manejan en _evaluate_assignment + # 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 - # Simplificar si es expresión SymPy - if hasattr(result, 'simplify'): - result = simplify(result) + 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 - output = str(result) - - # Añadir aproximación numérica - numeric = self._get_numeric_approximation(result) - if numeric and str(result) != str(numeric): - output += f" ≈ {numeric}" - - return EvaluationResult(line, output, "symbolic", True) + 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: {str(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): @@ -588,7 +666,7 @@ class PureAlgebraicEngine: self.equations.clear() self.variables.clear() self.symbol_table.clear() - self.logger.info("Contexto limpio") + def get_context_info(self) -> Dict[str, Any]: """Información del contexto actual""" diff --git a/tl_popup.py b/tl_popup.py index 8ca817b..1996197 100644 --- a/tl_popup.py +++ b/tl_popup.py @@ -13,10 +13,11 @@ import numpy as np class PlotResult: """Placeholder para resultados de plotting - DEFINICIÓN PRINCIPAL""" - def __init__(self, plot_type: str, args: tuple, kwargs: dict): + def __init__(self, plot_type: str, args: tuple, kwargs: dict, original_expression: str = None): self.plot_type = plot_type self.args = args self.kwargs = kwargs + self.original_expression = original_expression or "" def __str__(self): return f"📊 Ver {self.plot_type.title()}" @@ -31,8 +32,13 @@ class InteractiveResultManager: def __init__(self, parent_window: tk.Tk): self.parent = parent_window self.open_windows: Dict[str, Toplevel] = {} + self.update_input_callback = None # Callback para actualizar el panel de entrada - def create_interactive_tag(self, result: Any, text_widget: tk.Text, index: str) -> Optional[Tuple[str, str]]: + def set_update_callback(self, callback): + """Establece el callback para actualizar el panel de entrada""" + self.update_input_callback = callback + + def create_interactive_tag(self, result: Any, text_widget: tk.Text) -> Optional[Tuple[str, str]]: """ Crea un tag interactivo para un resultado si es necesario @@ -135,7 +141,7 @@ class InteractiveResultManager: print(f"❌ Error abriendo ventana interactiva: {e}") def _show_plot_window(self, plot_result: PlotResult, window_key: str): - """Muestra ventana con plot matplotlib""" + """Muestra ventana con plot matplotlib e interfaz de edición""" # Asegurar que las dimensiones de la ventana principal estén actualizadas self.parent.update_idletasks() parent_x = self.parent.winfo_x() @@ -144,7 +150,7 @@ class InteractiveResultManager: parent_height = self.parent.winfo_height() # Definir dimensiones y posición para la ventana del plot - plot_window_width = 600 # Ancho deseado para la ventana del plot (puedes ajustarlo) + plot_window_width = 700 # Aumentado para dar espacio al campo de edición plot_window_height = parent_height # Misma altura que la ventana principal # Posicionar la ventana del plot a la derecha de la ventana principal @@ -163,8 +169,129 @@ class InteractiveResultManager: ) self.open_windows[window_key] = window + # Frame principal para organizar la ventana + main_frame = tk.Frame(window, bg="#2b2b2b") + main_frame.pack(fill=tk.BOTH, expand=True) + + # Frame superior para el campo de edición + edit_frame = tk.Frame(main_frame, bg="#2b2b2b") + edit_frame.pack(fill=tk.X, padx=10, pady=5) + + # Label para el campo de edición + tk.Label( + edit_frame, + text="Expresión:", + bg="#2b2b2b", + fg="#d4d4d4", + font=("Consolas", 10) + ).pack(side=tk.LEFT) + + # Campo de entrada para editar la expresión + self.current_expression = tk.StringVar() + self.current_expression.set(plot_result.original_expression) + + expression_entry = tk.Entry( + edit_frame, + textvariable=self.current_expression, + bg="#1e1e1e", + fg="#d4d4d4", + font=("Consolas", 11), + insertbackground="#ffffff" + ) + expression_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(10, 5)) + + # Botón para redibujar + redraw_btn = tk.Button( + edit_frame, + text="Redibujar", + command=lambda: self._redraw_plot(plot_result, canvas_frame, expression_entry.get()), + bg="#4fc3f7", + fg="white", + font=("Consolas", 9), + relief=tk.FLAT + ) + redraw_btn.pack(side=tk.RIGHT, padx=5) + + # Frame para el canvas del plot + canvas_frame = tk.Frame(main_frame, bg="#2b2b2b") + canvas_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=5) + + # Configurar el protocolo de cierre para guardar la expresión editada + def on_window_close(): + edited_expression = expression_entry.get().strip() + original_expression = plot_result.original_expression.strip() + + # Si la expresión cambió y tenemos un callback, actualizar el panel de entrada + if edited_expression != original_expression and self.update_input_callback: + self.update_input_callback(original_expression, edited_expression) + + # Limpiar la ventana del registro + if window_key in self.open_windows: + del self.open_windows[window_key] + + # Cerrar la ventana + window.destroy() + + window.protocol("WM_DELETE_WINDOW", on_window_close) + + # Crear el plot inicial + self._create_plot_in_frame(plot_result, canvas_frame) + + # Hacer focus en el campo de entrada para edición inmediata + expression_entry.focus_set() + expression_entry.select_range(0, tk.END) + + def _redraw_plot(self, plot_result: PlotResult, canvas_frame: tk.Frame, new_expression: str): + """Redibuja el plot con una nueva expresión""" try: - fig, ax = plt.subplots(figsize=(8, 6)) # Tamaño de la figura interna del plot + # Limpiar el frame actual + for widget in canvas_frame.winfo_children(): + widget.destroy() + + # Evaluar la nueva expresión + import sympy as sp + + # Crear contexto básico para evaluación + eval_context = { + 'sin': sp.sin, 'cos': sp.cos, 'tan': sp.tan, + 'exp': sp.exp, 'log': sp.log, 'sqrt': sp.sqrt, + 'pi': sp.pi, 'e': sp.E, 'x': sp.Symbol('x'), 'y': sp.Symbol('y'), + 'z': sp.Symbol('z'), 't': sp.Symbol('t') + } + + # Evaluar la expresión + new_expr = sp.sympify(new_expression, locals=eval_context) + + # Crear nuevo PlotResult con la expresión actualizada + new_plot_result = PlotResult( + plot_result.plot_type, + (new_expr,) + plot_result.args[1:], # Mantener argumentos adicionales + plot_result.kwargs, + new_expression + ) + + # Actualizar la expresión original en el objeto + plot_result.original_expression = new_expression + + # Redibujar + self._create_plot_in_frame(new_plot_result, canvas_frame) + + except Exception as e: + # Mostrar error en el frame + error_label = tk.Label( + canvas_frame, + text=f"Error en expresión: {e}", + fg="#f44747", + bg="#2b2b2b", + font=("Consolas", 11), + wraplength=600 + ) + error_label.pack(pady=20) + + def _create_plot_in_frame(self, plot_result: PlotResult, parent_frame: tk.Frame): + """Crea el plot dentro del frame especificado""" + try: + fig, ax = plt.subplots(figsize=(8, 6)) if plot_result.plot_type == "plot": self._create_2d_plot(fig, ax, plot_result.args, plot_result.kwargs) @@ -172,30 +299,28 @@ class InteractiveResultManager: self._create_3d_plot(fig, plot_result.args, plot_result.kwargs) # Embed en tkinter - canvas = FigureCanvasTkAgg(fig, window) + canvas = FigureCanvasTkAgg(fig, parent_frame) canvas.draw() canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True) # Toolbar para interactividad try: from matplotlib.backends.backend_tkagg import NavigationToolbar2Tk - toolbar = NavigationToolbar2Tk(canvas, window) + toolbar = NavigationToolbar2Tk(canvas, parent_frame) toolbar.update() except ImportError: pass # Si no está disponible, continuar sin toolbar except Exception as e: error_label = tk.Label( - window, + parent_frame, text=f"Error generando plot: {e}", - fg="red", + fg="#f44747", bg="#2b2b2b", font=("Consolas", 12) ) error_label.pack(pady=20) - - print(f"❌ Error en plot: {e}") - + def _create_2d_plot(self, fig, ax, args, kwargs): """Crea plot 2D usando SymPy""" if len(args) >= 1: @@ -518,11 +643,27 @@ class InteractiveResultManager: return window def close_all_windows(self): - """Cierra todas las ventanas interactivas""" - for window in list(self.open_windows.values()): + """Cierra todas las ventanas interactivas de forma segura""" + windows_to_close = list(self.open_windows.items()) + + for window_key, window in windows_to_close: try: - if window.winfo_exists(): - window.destroy() + if window and window.winfo_exists(): + # Forzar el cierre del protocolo de cierre si existe + window.protocol("WM_DELETE_WINDOW", window.destroy) + window.quit() # Detener el mainloop de la ventana si lo tiene + window.destroy() # Destruir la ventana except tk.TclError: + # La ventana ya fue destruida o no es válida pass + except Exception as e: + print(f"Error cerrando ventana {window_key}: {e}") + + # Limpiar el diccionario self.open_windows.clear() + + # Cerrar todas las figuras de matplotlib para liberar memoria + try: + plt.close('all') + except: + pass