Actualización de la configuración de la interfaz y mejoras en el motor algebraico. Se modifica la geometría de la ventana y la posición del panel. Se implementa la tokenización de expresiones LaTeX en línea y se añade un método para procesar LaTeX, mejorando la conversión entre expresiones matemáticas y su representación en LaTeX. Se amplía la clase Class_LaTeX para incluir un método estático de parseo de LaTeX.

This commit is contained in:
Miguel 2025-06-07 14:19:10 +02:00
parent 0488122229
commit 6475661a02
4 changed files with 346 additions and 13 deletions

View File

@ -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"
}),
]

31
hybrid_calc_history.txt Normal file
View File

@ -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} $$

View File

@ -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,

View File

@ -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()