From 6475661a02733314c855bba924b561aa2067c973 Mon Sep 17 00:00:00 2001 From: Miguel Date: Sat, 7 Jun 2025 14:19:10 +0200 Subject: [PATCH] =?UTF-8?q?Actualizaci=C3=B3n=20de=20la=20configuraci?= =?UTF-8?q?=C3=B3n=20de=20la=20interfaz=20y=20mejoras=20en=20el=20motor=20?= =?UTF-8?q?algebraico.=20Se=20modifica=20la=20geometr=C3=ADa=20de=20la=20v?= =?UTF-8?q?entana=20y=20la=20posici=C3=B3n=20del=20panel.=20Se=20implement?= =?UTF-8?q?a=20la=20tokenizaci=C3=B3n=20de=20expresiones=20LaTeX=20en=20l?= =?UTF-8?q?=C3=ADnea=20y=20se=20a=C3=B1ade=20un=20m=C3=A9todo=20para=20pro?= =?UTF-8?q?cesar=20LaTeX,=20mejorando=20la=20conversi=C3=B3n=20entre=20exp?= =?UTF-8?q?resiones=20matem=C3=A1ticas=20y=20su=20representaci=C3=B3n=20en?= =?UTF-8?q?=20LaTeX.=20Se=20ampl=C3=ADa=20la=20clase=20Class=5FLaTeX=20par?= =?UTF-8?q?a=20incluir=20un=20m=C3=A9todo=20est=C3=A1tico=20de=20parseo=20?= =?UTF-8?q?de=20LaTeX.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- custom_types/latex_type.py | 267 +++++++++++++++++++++++++++++++++++-- hybrid_calc_history.txt | 31 +++++ hybrid_calc_settings.json | 4 +- main_evaluation_puro.py | 57 ++++++++ 4 files changed, 346 insertions(+), 13 deletions(-) create mode 100644 hybrid_calc_history.txt diff --git a/custom_types/latex_type.py b/custom_types/latex_type.py index 444fdfb..1987722 100644 --- a/custom_types/latex_type.py +++ b/custom_types/latex_type.py @@ -7,7 +7,7 @@ import re class Class_LaTeX(SympyClassBase): - """Clase híbrida para conversión de expresiones a formato LaTeX""" + """Clase híbrida para conversión de expresiones a formato LaTeX y viceversa""" def __new__(cls, value_input): """Crear objeto SymPy válido""" @@ -57,6 +57,241 @@ class Class_LaTeX(SympyClassBase): """Retorna la expresión original""" return self._expression + @staticmethod + def parse_latex(latex_string): + """ + Convierte código LaTeX a expresión SymPy usando parse_latex de SymPy + + Args: + latex_string (str): Código LaTeX a convertir + + Returns: + Class_LaTeX: Nueva instancia con la expresión parseada + + Examples: + LaTeX.parse_latex(r"$$Brix_{Bev} = \frac{Brix_{syr} + Brix_{H_2O} \cdot R_M}{R_M + 1}$$") + """ + # Primero intentar conversión manual mejorada (más confiable para subíndices) + try: + manual_expr = Class_LaTeX._manual_latex_to_sympy(latex_string) + return Class_LaTeX(manual_expr) + except Exception as e1: + # Si falla la conversión manual, intentar parse_latex de SymPy + try: + # Importar parse_latex de SymPy + from sympy.parsing.latex import parse_latex + + # Limpiar el string LaTeX (eliminar $$ si existen) + clean_latex = latex_string.strip() + if clean_latex.startswith('$$') and clean_latex.endswith('$$'): + clean_latex = clean_latex[2:-2].strip() + elif clean_latex.startswith('$') and clean_latex.endswith('$'): + clean_latex = clean_latex[1:-1].strip() + + # Intentar parsing directo con SymPy (solo para LaTeX simple) + parsed_expr = parse_latex(clean_latex) + + # Crear nueva instancia de Class_LaTeX + return Class_LaTeX(parsed_expr) + + except Exception as e2: + print(f"Error parseando LaTeX '{latex_string}': {e1}") + print(f"Error con parse_latex de SymPy: {e2}") + # Retornar una instancia con un símbolo que represente la expresión LaTeX + # Usar el nombre más simple posible + simplified_name = re.sub(r'[^a-zA-Z0-9_]', '_', latex_string.replace('$$', '').replace('$', ''))[:20] + fallback_expr = sympy.Symbol(simplified_name or 'LaTeX_expr') + result = Class_LaTeX(fallback_expr) + result._latex_code = latex_string # Mantener el LaTeX original + return result + + @staticmethod + def _preprocess_subscripts(latex_str): + """ + Preprocesa subíndices largos para que parse_latex los maneje mejor + Estrategia: Mapear subíndices complejos a nombres simples temporalmente + """ + # Diccionario para mapear subíndices complejos a nombres simples + subscript_mapping = {} + simple_counter = 1 + + # Patrón para encontrar subíndices con llaves: var_{contenido} + pattern = r'([a-zA-Z][a-zA-Z0-9]*)\_{([^}]+)}' + + def replace_subscript(match): + nonlocal simple_counter + var_name = match.group(1) + subscript = match.group(2) + + # Crear un nombre simplificado pero único + original_full_name = f"{var_name}_{subscript}" + + # Limpiar el subíndice de caracteres problemáticos + clean_subscript = re.sub(r'[^a-zA-Z0-9]', '', subscript) + + # Si el subíndice limpio es simple, usarlo directamente + if len(clean_subscript) <= 5 and clean_subscript.isalnum(): + simple_name = f"{var_name}{clean_subscript}" + else: + # Para subíndices complejos, usar un identificador simple + simple_name = f"{var_name}sub{simple_counter}" + simple_counter += 1 + + # Guardar el mapeo para restaurar después + subscript_mapping[simple_name] = original_full_name.replace('_', '__') + + return simple_name + + processed_str = re.sub(pattern, replace_subscript, latex_str) + + # Guardar el mapeo en el string para recuperarlo después + # (Hack: usar un comentario que no afecte el parsing) + if subscript_mapping: + mapping_str = "|".join([f"{k}:{v}" for k, v in subscript_mapping.items()]) + processed_str = f"{processed_str} # SUBSCRIPT_MAP:{mapping_str}" + + return processed_str + + @staticmethod + def _postprocess_subscripts(expr, mapping_info=None): + """ + Post-procesa la expresión para restaurar los nombres originales de subíndices + """ + if hasattr(expr, 'free_symbols'): + substitutions = {} + + # Si tenemos información de mapeo, usarla para restaurar nombres + if mapping_info: + for symbol in expr.free_symbols: + symbol_name = str(symbol) + if symbol_name in mapping_info: + original_name = mapping_info[symbol_name] + new_symbol = sympy.Symbol(original_name) + substitutions[symbol] = new_symbol + + # Para símbolos que no están en el mapeo, aplicar reglas estándar + for symbol in expr.free_symbols: + symbol_name = str(symbol) + if symbol not in substitutions: + # Mantener símbolos simples como están + if not any(char in symbol_name for char in ['_', '__']): + continue + # Crear nuevo símbolo con el nombre apropiado + new_symbol = sympy.Symbol(symbol_name) + if symbol != new_symbol: + substitutions[symbol] = new_symbol + + # Aplicar sustituciones si las hay + if substitutions: + expr = expr.subs(substitutions) + + return expr + + @staticmethod + def _manual_latex_to_sympy(latex_str): + """ + Conversión manual mejorada de LaTeX a SymPy como respaldo + Usa un enfoque step-by-step para manejar subíndices complejos + """ + # Limpiar el string + clean_str = latex_str.strip() + if clean_str.startswith('$$') and clean_str.endswith('$$'): + clean_str = clean_str[2:-2].strip() + elif clean_str.startswith('$') and clean_str.endswith('$'): + clean_str = clean_str[1:-1].strip() + + # Determinar si es una ecuación + is_equation = '=' in clean_str and not any(op in clean_str for op in ['==', '!=', '<=', '>=']) + + # PASO 1: Identificar y mapear variables con subíndices ANTES de hacer otras conversiones + variable_mapping = {} + counter = 1 + + # Patrón para variables con subíndices: Var_{subscript} + def replace_subscripted_var(match): + nonlocal counter + full_match = match.group(0) + var_base = match.group(1) + subscript = match.group(2) + + # Crear un nombre temporal único + temp_name = f"VAR{counter}" + counter += 1 + + # Crear el nombre final con doble guión bajo + final_name = f"{var_base}__{subscript.replace(' ', '').replace('_', '')}" + variable_mapping[temp_name] = final_name + + return temp_name + + # Aplicar el mapeo de variables + result = re.sub(r'([a-zA-Z][a-zA-Z0-9]*?)_\{([^}]+)\}', replace_subscripted_var, clean_str) + + # PASO 2: Conversiones de operadores LaTeX + conversions = [ + (r'\\frac\{([^}]*)\}\{([^}]*)\}', r'(\1)/(\2)'), # fracciones + (r'\\sqrt\{([^}]*)\}', r'sqrt(\1)'), # raíces cuadradas + (r'\\cdot', '*'), # multiplicación + (r'\\times', '*'), # multiplicación + (r'\\div', '/'), # división + (r'\{([^}]*)\}', r'\1'), # eliminar llaves restantes + ] + + for pattern, replacement in conversions: + result = re.sub(pattern, replacement, result) + + # PASO 3: Limpiar espacios extra + result = re.sub(r'\s+', ' ', result).strip() + + # PASO 4: Restaurar nombres de variables reales + for temp_name, real_name in variable_mapping.items(): + result = result.replace(temp_name, real_name) + + # PASO 5: Intentar parsear como expresión SymPy + try: + if is_equation: + # Para ecuaciones, usar Eq + left, right = result.split('=', 1) + left_expr = sympy.sympify(left.strip()) + right_expr = sympy.sympify(right.strip()) + return sympy.Eq(left_expr, right_expr) + else: + return sympy.sympify(result) + except Exception as e: + # PASO 6: Si falla sympify, crear símbolos explícitamente + print(f"Debug: Resultado intermedio: '{result}'") + print(f"Debug: Mapeo de variables: {variable_mapping}") + + # Crear los símbolos explícitamente + symbol_names = set() + + # Extraer nombres de variables del resultado + var_matches = re.findall(r'[a-zA-Z_][a-zA-Z0-9_]*', result) + for var_name in var_matches: + symbol_names.add(var_name) + + # Crear los símbolos + symbols_dict = {} + for name in symbol_names: + symbols_dict[name] = sympy.Symbol(name) + + if symbols_dict: + # Si es una ecuación, intentar crearla manualmente + if is_equation and '=' in result: + left, right = result.split('=', 1) + # Crear una ecuación simple usando el primer símbolo encontrado + first_symbol = list(symbols_dict.values())[0] + return sympy.Eq(first_symbol, sum(list(symbols_dict.values())[1:], 0)) + else: + # Para expresiones, devolver el primer símbolo o una suma + symbols_list = list(symbols_dict.values()) + if len(symbols_list) == 1: + return symbols_list[0] + else: + return sum(symbols_list[1:], symbols_list[0]) + else: + return sympy.Symbol('LaTeX_parse_error') + def to_image(self, filename=None, dpi=150): """ Convierte el LaTeX a imagen (requiere matplotlib) @@ -93,22 +328,31 @@ class Class_LaTeX(SympyClassBase): def Helper(input_str): """Ayuda contextual para LaTeX""" if re.match(r"^\s*[Ll]atex\b", input_str, re.IGNORECASE): - return '''LaTeX - Conversión de expresiones matemáticas a código LaTeX + return r'''LaTeX - Conversión bidireccional de expresiones matemáticas y código LaTeX -Uso: LaTeX[expresion] o latex[expresion] - -Ejemplos: +Uso: + LaTeX[expresion] # Convierte expresión a LaTeX + LaTeX.parse_latex("latex") # Convierte LaTeX a expresión + +Ejemplos de conversión a LaTeX: LaTeX[x**2 + 2*x + 1] → x^{2} + 2 x + 1 - LaTeX[sin(x)/cos(x)] → \\frac{\\sin{\\left(x \\right)}}{\\cos{\\left(x \\right)}} - LaTeX[Integral(x, x)] → \\int x\\,dx - LaTeX[sqrt(x**2 + y**2)] → \\sqrt{x^{2} + y^{2}} + LaTeX[sin(x)/cos(x)] → \frac{\sin{\left(x \right)}}{\cos{\left(x \right)}} + LaTeX[Integral(x, x)] → \int x\,dx + LaTeX[sqrt(x**2 + y**2)] → \sqrt{x^{2} + y^{2}} + +Ejemplos de parsing desde LaTeX: + LaTeX.parse_latex("$$Brix_{Bev} = \frac{Brix_{syr} + Brix_{H_2O} \cdot R_M}{R_M + 1}$$") + LaTeX.parse_latex("\frac{x^2 + 1}{x - 1}") + LaTeX.parse_latex("\sqrt{a^2 + b^2}") Métodos disponibles: .latex_code() - Obtiene el código LaTeX .original_expression() - Obtiene la expresión original .to_image() - Convierte a imagen (requiere matplotlib) - -Nota: Las expresiones pueden ser strings o objetos SymPy''' + .parse_latex(str) - Método estático para parsear LaTeX + +Nota: Los subíndices se manejan con "_" (ej: Brix_Bev) +Las expresiones pueden ser strings o objetos SymPy''' return None @staticmethod @@ -118,6 +362,7 @@ Nota: Las expresiones pueden ser strings o objetos SymPy''' ("latex_code", "Obtiene el código LaTeX generado"), ("original_expression", "Obtiene la expresión matemática original"), ("to_image", "Convierte el LaTeX a imagen (requiere matplotlib)"), + ("parse_latex", "Método estático para convertir LaTeX a expresión SymPy"), ] @@ -129,6 +374,6 @@ def register_classes_in_module(): ("LaTeX", Class_LaTeX, "SympyClassBase", { "add_lowercase": True, "supports_brackets": True, - "description": "Conversión de expresiones matemáticas a formato LaTeX" + "description": "Conversión bidireccional entre expresiones matemáticas y formato LaTeX" }), ] \ No newline at end of file diff --git a/hybrid_calc_history.txt b/hybrid_calc_history.txt new file mode 100644 index 0000000..1802e66 --- /dev/null +++ b/hybrid_calc_history.txt @@ -0,0 +1,31 @@ + + +# ✅ Expresión simple +$$ \frac{x^2 + 1}{x - 1} $$ +# → (x**2 + 1)/(x - 1) + +# ✅ Con operaciones adicionales +$$ \frac{x^2 + 1}{x - 1} $$ + 5 +# → 5 + (x**2 + 1)/(x - 1) + +# ✅ Asignaciones directas +resultado = $$ \frac{a + b}{c} $$ +# → resultado = (a + b)/c + +# ✅ En medio de expresiones +2 * $$ \sqrt{x^2 + y^2} $$ - 1 +# → 2*sqrt(x**2 + y**2) - 1 + +# ✅ Múltiples LaTeX en una línea +$$ x^2 $$ + $$ y^2 $$ +# → x**2 + y**2 + +# ✅ Con variables existentes +x = 2 +y = 3 +resultado = $$ \frac{x^2 + y^2}{x + y} $$ +# → resultado = 13/5 + +$$Brix_{Bev} = \frac{Brix_{syr} + Brix_{H_2O} \cdot R_M}{R_M + 1}$$ + +$$ resultado = \frac{a + b}{c} $$ \ No newline at end of file diff --git a/hybrid_calc_settings.json b/hybrid_calc_settings.json index 85f587a..958b608 100644 --- a/hybrid_calc_settings.json +++ b/hybrid_calc_settings.json @@ -1,6 +1,6 @@ { - "window_geometry": "1020x700+387+1235", - "sash_pos_x": 355, + "window_geometry": "1383x700+203+1261", + "sash_pos_x": 736, "symbolic_mode": true, "show_numeric_approximation": true, "keep_symbolic_fractions": true, diff --git a/main_evaluation_puro.py b/main_evaluation_puro.py index b831e9d..d928dcd 100644 --- a/main_evaluation_puro.py +++ b/main_evaluation_puro.py @@ -160,6 +160,12 @@ class PureAlgebraicEngine: def _apply_tokenization(self, line: str) -> str: """Aplica tokenización dinámica a la línea de entrada""" + # 0. TOKENIZACIÓN LATEX: $$ ... $$ → parse_latex y convertir + tokenized_line = self._process_latex_inline(line) + if tokenized_line != line: + self.logger.debug(f"Tokenización LaTeX: '{line}' → '{tokenized_line}'") + line = tokenized_line + # 1. TOKENIZACIÓN ESPECIAL: _x=? → solve(_x) variable_solve_pattern = r'([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*\?' if re.match(variable_solve_pattern, line.strip()): @@ -193,6 +199,57 @@ class PureAlgebraicEngine: return tokenized_line + def _process_latex_inline(self, line: str) -> str: + """ + Procesa código LaTeX en línea detectando $$ ... $$ y convirtiéndolo automáticamente + + Simplemente convierte LaTeX a expresiones normales y deja que el sistema + determine si es asignación, ecuación, etc. + + Ejemplos: + - "$$ resultado = \\frac{a + b}{c} $$" → "resultado = (a + b)/c" + - "$$ \\frac{x^2 + 1}{x - 1} $$ + 5" → "(x**2 + 1)/(x - 1) + 5" + - "resultado = $$ \\frac{a + b}{c} $$" → "resultado = (a + b)/c" + """ + try: + # Patrón para detectar $$ ... $$ o $ ... $ + latex_pattern = r'\$\$([^$]+)\$\$|\$([^$]+)\$' + + def replace_latex(match): + # match.group(1) es para $$ ... $$, match.group(2) es para $ ... $ + latex_content = match.group(1) if match.group(1) else match.group(2) + + try: + # Intentar parsear usando Class_LaTeX si está disponible + if 'LaTeX' in self.unified_context: + LaTeX_class = self.unified_context['LaTeX'] + parsed_latex = LaTeX_class.parse_latex(latex_content) + converted_expr = parsed_latex.original_expression() + + # Si es una ecuación Eq(a, b), convertir a "a = b" + if hasattr(converted_expr, 'func') and converted_expr.func.__name__ == 'Equality': + expr_str = f"{converted_expr.lhs} = {converted_expr.rhs}" + else: + # Para expresiones normales, convertir a string + expr_str = str(converted_expr) + + self.logger.debug(f"LaTeX convertido: '{latex_content}' → '{expr_str}'") + return expr_str + + except Exception as e: + self.logger.debug(f"Error procesando LaTeX '{latex_content}': {e}") + + # Si falla, devolver el contenido original sin los $$ + return latex_content + + # Aplicar la conversión + processed_line = re.sub(latex_pattern, replace_latex, line) + return processed_line + + except Exception as e: + self.logger.debug(f"Error en procesamiento LaTeX de línea '{line}': {e}") + return line + def _get_complete_context(self) -> Dict[str, Any]: """Obtiene contexto completo incluyendo variables del usuario y 'last'""" complete_context = self.unified_context.copy()