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:
parent
0488122229
commit
6475661a02
|
@ -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"
|
||||
}),
|
||||
]
|
|
@ -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} $$
|
|
@ -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,
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Reference in New Issue