commit ebb0ac82faf3c7819176dd60706a182ee1bd4fa9 Author: Miguel Date: Sun Jun 1 16:30:03 2025 +0200 Primera version funcionante diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bda86c7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,194 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +#uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# Abstra +# Abstra is an AI-powered process automation framework. +# Ignore directories containing user credentials, local state, and settings. +# Learn more at https://abstra.io/docs +.abstra/ + +# Visual Studio Code +# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore +# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore +# and can be added to the global gitignore or merged into this file. However, if you prefer, +# you could uncomment the following to ignore the entire vscode folder +# .vscode/ + +# Ruff stuff: +.ruff_cache/ + +# PyPI configuration file +.pypirc + +# Cursor +# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to +# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data +# refer to https://docs.cursor.com/context/ignore-files +.cursorignore +.cursorindexingignore \ No newline at end of file diff --git a/bracket_parser.py b/bracket_parser.py new file mode 100644 index 0000000..40de54b --- /dev/null +++ b/bracket_parser.py @@ -0,0 +1,269 @@ +""" +Bracket Parser - Transformador de sintaxis con corchetes y detección contextual de ecuaciones +""" +import ast +import re +from typing import Tuple, Optional + + +class BracketParser: + """Parser que convierte sintaxis con corchetes y detecta ecuaciones contextualmente""" + + # Clases que soportan sintaxis con corchetes + BRACKET_CLASSES = {'IP4', 'Hex', 'Bin', 'Date', 'Dec', 'Chr'} + + # Operadores de comparación que pueden formar ecuaciones + EQUATION_OPERATORS = {'==', '!=', '<', '<=', '>', '>=', '='} + + def __init__(self): + self.debug = False + + def parse_line(self, code_line: str) -> Tuple[str, str]: + """ + Parsea una línea de código aplicando todas las transformaciones + + Returns: + (transformed_code, parse_info): Código transformado e información de parsing + """ + original_line = code_line.strip() + if not original_line or original_line.startswith('#'): + return code_line, "comment" + + try: + # 1. Detectar y transformar atajo solve + transformed_line, has_solve_shortcut = self._transform_solve_shortcut(original_line) + if has_solve_shortcut: + return transformed_line, "solve_shortcut" + + # 2. Detectar asignaciones de variables + if self._is_assignment(original_line): + return self._transform_assignment(original_line), "assignment" + + # 3. Detectar ecuaciones standalone + if self._is_standalone_equation(original_line): + return f'_add_equation("{original_line}")', "equation" + + # 4. Transformar sintaxis con corchetes + transformed_line = self._transform_brackets(original_line) + + # 5. Si no hay transformaciones, devolver original + if transformed_line == original_line: + return original_line, "expression" + else: + return transformed_line, "bracket_transform" + + except Exception as e: + if self.debug: + print(f"Error parsing line '{original_line}': {e}") + return code_line, "parse_error" + + def _transform_solve_shortcut(self, line: str) -> Tuple[str, bool]: + """ + Transforma 'variable=?' en 'solve(variable)' + + Returns: + (transformed_line, was_transformed) + """ + # Pattern: variable_name = ? + pattern = r'^([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*\?$' + match = re.match(pattern, line.strip()) + + if match: + var_name = match.group(1) + return f'solve({var_name})', True + + return line, False + + def _is_assignment(self, line: str) -> bool: + """Detecta si una línea es una asignación de variable""" + try: + # Pattern: variable = expresión (que no sea ecuación) + if '=' in line and not any(op in line for op in ['==', '!=', '<=', '>=']): + # Verificar que sea una asignación válida de Python + parts = line.split('=', 1) + if len(parts) == 2: + var_part = parts[0].strip() + # Verificar que la parte izquierda sea un identificador válido + if re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', var_part): + return True + return False + except: + return False + + def _transform_assignment(self, line: str) -> str: + """Transforma asignación a llamada de función especial""" + parts = line.split('=', 1) + var_name = parts[0].strip() + expression = parts[1].strip() + + # Transformar corchetes en la expresión + expression = self._transform_brackets(expression) + + return f'_assign_variable("{var_name}", {expression})' + + def _is_standalone_equation(self, line: str) -> bool: + """ + Determina si una línea es una ecuación standalone usando análisis AST + """ + try: + tree = ast.parse(line.strip()) + + if not tree.body: + return False + + node = tree.body[0] + + # Solo consideramos expresiones (no asignaciones) + if not isinstance(node, ast.Expr): + return False + + # Verificar si es una comparación con operadores de ecuación + if isinstance(node.value, ast.Compare): + # Verificar que use operadores de ecuación + for op in node.value.ops: + if isinstance(op, (ast.Eq, ast.NotEq, ast.Lt, ast.LtE, ast.Gt, ast.GtE)): + return True + + # Verificar si contiene un '=' que no sea parte de una asignación + # Esto es para casos como "x + 2 = 5" que no parsea como Compare + if '=' in line and not any(op in line for op in ['==', '!=', '<=', '>=']): + # Es un '=' simple, puede ser una ecuación + return True + + return False + + except SyntaxError: + # Si hay error de sintaxis, puede ser una ecuación mal formada + # como "x + 2 = 5" que Python no puede parsear + if '=' in line: + return True + return False + except Exception: + return False + + def _transform_brackets(self, line: str) -> str: + """ + Transforma sintaxis Class[args] → Class("args") y maneja métodos + """ + # Pattern principal: ClassName[contenido] + pattern = r'(\b(?:' + '|'.join(self.BRACKET_CLASSES) + r')\b)\[([^\]]*)\]' + + def replace_match(match): + class_name = match.group(1) + args_content = match.group(2).strip() + + if not args_content: + # Caso: Class[] → Class() + return f'{class_name}()' + else: + # Caso: Class[args] → Class("args") + # Escapar comillas dobles en el contenido + escaped_content = args_content.replace('"', '\\"') + return f'{class_name}("{escaped_content}")' + + # Aplicar transformación repetidamente hasta que no haya más cambios + transformed = line + while True: + new_transformed = re.sub(pattern, replace_match, transformed) + if new_transformed == transformed: + break + transformed = new_transformed + + # Transformar corchetes vacíos en métodos: .método[] → .método() + method_pattern = r'\.([a-zA-Z_][a-zA-Z0-9_]*)\[\]' + transformed = re.sub(method_pattern, r'.\1()', transformed) + + return transformed + + +class EquationDetector: + """Detector específico para ecuaciones con análisis AST avanzado""" + + @staticmethod + def is_equation_in_context(code: str, context: str = "standalone") -> bool: + """ + Determina si el código contiene una ecuación considerando el contexto + + Args: + code: Código a analizar + context: Contexto ("standalone", "function_arg", "assignment") + """ + try: + tree = ast.parse(code.strip()) + + for node in ast.walk(tree): + if isinstance(node, ast.Compare): + # Verificar operadores de ecuación + for op in node.ops: + if isinstance(op, (ast.Eq, ast.NotEq, ast.Lt, ast.LtE, ast.Gt, ast.GtE)): + if context == "standalone": + # En contexto standalone, es una ecuación + return True + elif context == "function_arg": + # En argumentos de función, generalmente NO es ecuación + return False + + # Verificar '=' simple (no comparación) + if '=' in code and context == "standalone": + # Verificar que no sea asignación Python válida + try: + ast.parse(code.strip()) + return False # Es sintaxis Python válida + except SyntaxError: + return True # Puede ser ecuación mal formada para Python + + return False + + except Exception: + return False + + +# Funciones de utilidad para testing +def test_bracket_parser(): + """Función de testing para el bracket parser""" + parser = BracketParser() + parser.debug = True + + test_cases = [ + # Sintaxis con corchetes + ("Hex[FF]", 'Hex("FF")', "bracket_transform"), + ("IP4[192.168.1.1/24]", 'IP4("192.168.1.1/24")', "bracket_transform"), + ("IP4[192.168.1.1/24].NetworkAddress[]", 'IP4("192.168.1.1/24").NetworkAddress()', "bracket_transform"), + ("Bin[1010]", 'Bin("1010")', "bracket_transform"), + + # Atajos solve + ("x=?", "solve(x)", "solve_shortcut"), + ("variable_name=?", "solve(variable_name)", "solve_shortcut"), + + # Asignaciones + ("z = 5", '_assign_variable("z", 5)', "assignment"), + ("w = z**2 + 3", '_assign_variable("w", z**2 + 3)', "assignment"), + ("result = Hex[FF]", '_assign_variable("result", Hex("FF"))', "assignment"), + + # Ecuaciones standalone + ("x + 2 = 5", '_add_equation("x + 2 = 5")', "equation"), + ("3*a + b = 10", '_add_equation("3*a + b = 10")', "equation"), + ("x > 5", "x > 5", "expression"), # Comparación válida de Python + ("a == b", "a == b", "expression"), # Comparación válida de Python + + # NO ecuaciones + ("result = solve(x + 2, x)", '_assign_variable("result", solve(x + 2, x))', "assignment"), # Asignación Python + ("2 + 3", "2 + 3", "expression"), # Expresión simple + ("sin(pi/2)", "sin(pi/2)", "expression"), # Función + + # Expresiones normales + ("x + 2*y", "x + 2*y", "expression"), + ("diff(x**2, x)", "diff(x**2, x)", "expression"), + ] + + print("=== Test Bracket Parser ===") + for test_input, expected_result, expected_info in test_cases: + result, info = parser.parse_line(test_input) + status = "✅" if result == expected_result and info == expected_info else "❌" + print(f"{status} '{test_input}' → '{result}' ({info})") + if result != expected_result or info != expected_info: + print(f" Esperado: '{expected_result}' ({expected_info})") + + +if __name__ == "__main__": + test_bracket_parser() diff --git a/comprehensive_documentation.md b/comprehensive_documentation.md new file mode 100644 index 0000000..4ffc85b --- /dev/null +++ b/comprehensive_documentation.md @@ -0,0 +1,469 @@ +# Calculadora MAV - CAS Híbrido + +## Descripción General + +La Calculadora MAV es un **Sistema de Álgebra Computacional (CAS) Híbrido** que combina la potencia de SymPy con clases especializadas para networking, programación y análisis numérico. + +### Características Principales + +- **Motor SymPy completo**: Todas las funciones de cálculo simbólico +- **Sintaxis simplificada**: `Class[args]` en lugar de `Class("args")` +- **Detección automática de ecuaciones**: Sin necesidad de comillas especiales +- **Resultados interactivos**: Plots, matrices y listas clickeables +- **Clases especializadas**: IP4, Hex, Bin, Date, Dec, Chr +- **Variables SymPy puras**: Todas las variables son símbolos automáticamente + +## Instalación + +### Método 1: Instalación Automática +```bash +python launcher.py --setup +``` + +### Método 2: Instalación Manual +```bash +# Instalar dependencias +pip install sympy matplotlib numpy + +# En Linux: instalar tkinter +sudo apt-get install python3-tk + +# Ejecutar tests (opcional) +python test_suite.py + +# Iniciar aplicación +python launcher.py +``` + +### Dependencias Requeridas +- **Python 3.8+** +- **SymPy ≥ 1.12** (motor algebraico) +- **Matplotlib ≥ 3.7.0** (plotting) +- **NumPy ≥ 1.24.0** (cálculos numéricos) +- **Tkinter** (interfaz gráfica, incluido con Python) + +### Dependencias Opcionales +- **Markdown ≥ 3.4.0** (ayuda mejorada) +- **pytest ≥ 7.0.0** (testing) + +## Guía de Uso + +### Sintaxis Básica + +#### Clases Especializadas (Solo Corchetes) +```python +# Direcciones IP con funcionalidad de red +IP4[192.168.1.100/24] +IP4[10.0.0.1, 8] +IP4[172.16.0.5, 255.255.0.0] + +# Números en diferentes bases +Hex[FF] # Hexadecimal: 0xFF +Bin[1010] # Binario: 0b1010 +Dec[10.5] # Decimal: 10.5 + +# Caracteres ASCII +Chr[A] # Carácter único: 'A' (ASCII 65) +Chr[Hello] # String: 'Hello' +``` + +#### Métodos de Clases Especializadas +```python +# Métodos de IP4 +ip = IP4[192.168.1.100/24] +ip.NetworkAddress[] # 192.168.1.0/24 +ip.BroadcastAddress[] # 192.168.1.255/24 +ip.Nodes() # 254 (hosts disponibles) + +# Conversiones +Hex[255].toDecimal() # 255 +Dec[66].toChr() # Chr('B') +``` + +### Ecuaciones y Álgebra + +#### Detección Automática de Ecuaciones +```python +# Ecuaciones simples (detectadas automáticamente) +x + 2 = 5 +3*a + b = 10 +y**2 = 16 + +# Desigualdades +x > 5 +a <= 10 +b != 0 + +# Ecuaciones complejas +sin(x) = 1/2 +log(y) + 3 = 5 +``` + +#### Resolución de Ecuaciones +```python +# Resolver ecuación específica +solve(x**2 + 2*x - 8, x) # [-4, 2] + +# Atajo para resolver variable +x=? # Equivale a solve(x) + +# Resolver sistema de ecuaciones +x + y = 10 +x - y = 2 +solve([x + y - 10, x - y - 2], [x, y]) # {x: 6, y: 4} +``` + +### Variables y Símbolos + +#### Variables SymPy Automáticas +```python +# Todas las variables son símbolos SymPy automáticamente +x + 2*y # Expresión simbólica +z = 5 # z es Symbol('z') con valor 5 +w = z**2 + 3 # w es expresión: Symbol('z')**2 + 3 + +# Evaluación automática cuando es posible +a = 10 +b = a + 5 # b = 15 (evaluado) +c = a + x # c = 10 + x (simbólico) +``` + +### Funciones Matemáticas + +#### Cálculo Diferencial e Integral +```python +# Derivadas +diff(x**3, x) # 3*x**2 +diff(sin(x)*cos(x), x) # -sin(x)**2 + cos(x)**2 + +# Integrales +integrate(x**2, x) # x**3/3 +integrate(sin(x), (x, 0, pi)) # 2 + +# Límites +limit(sin(x)/x, x, 0) # 1 + +# Series de Taylor +series(exp(x), x, 0, 5) # 1 + x + x**2/2 + x**3/6 + x**4/24 + O(x**5) +``` + +#### Funciones Trigonométricas +```python +# Funciones básicas +sin(pi/2) # 1 +cos(0) # 1 +tan(pi/4) # 1 + +# Funciones inversas +asin(1) # pi/2 +acos(0) # pi/2 +atan(1) # pi/4 + +# Funciones hiperbólicas +sinh(0) # 0 +cosh(0) # 1 +tanh(0) # 0 +``` + +#### Álgebra y Simplificación +```python +# Simplificación +simplify((x**2 - 1)/(x - 1)) # x + 1 +expand((x + 1)**3) # x**3 + 3*x**2 + 3*x + 1 +factor(x**2 - 1) # (x - 1)*(x + 1) + +# Manipulación de expresiones +collect(x**2 + 2*x + x**2, x) # 2*x**2 + 2*x +cancel((x**2 - 1)/(x - 1)) # x + 1 +``` + +### Álgebra Lineal + +#### Matrices +```python +# Crear matrices +M = Matrix([[1, 2], [3, 4]]) +N = Matrix([[5, 6], [7, 8]]) + +# Operaciones básicas +M + N # Suma de matrices +M * N # Multiplicación +M**2 # Potencia + +# Propiedades (clickeables en interfaz) +det(M) # Determinante: -2 +inv(M) # Matriz inversa +M.transpose() # Transpuesta +``` + +### Plotting Interactivo + +#### Plots 2D +```python +# Plot básico +plot(sin(x), (x, -2*pi, 2*pi)) + +# Múltiples funciones +plot(sin(x), cos(x), (x, 0, 2*pi)) + +# Plot con clases especializadas +plot(Hex[x]/256, (x, 0, 255)) +``` + +#### Plots 3D +```python +# Superficie 3D +plot3d(x**2 + y**2, (x, -5, 5), (y, -5, 5)) + +# Con funciones trigonométricas +plot3d(sin(x)*cos(y), (x, 0, 2*pi), (y, 0, 2*pi)) +``` + +### Resultados Interactivos + +#### Elementos Clickeables +- **📊 Ver Plot**: Abre ventana matplotlib para plots +- **📋 Ver Matriz**: Muestra matriz formateada con operaciones +- **📋 Ver Lista**: Expande listas largas +- **🔍 Ver Detalles**: Información completa de objetos + +#### Ejemplo de Uso +```python +# Estos resultados serán clickeables en la interfaz +Matrix([[1, 2, 3], [4, 5, 6]]) # 📋 Ver Matriz 2×3 +plot(x**2, (x, -10, 10)) # 📊 Ver Plot +solve(x**3 - 6*x**2 + 11*x - 6, x) # 📋 Ver Soluciones +``` + +## Casos de Uso Avanzados + +### Análisis de Redes +```python +# Definir red +network = IP4[192.168.0.0/24] +host = IP4[192.168.0.100/24] + +# Análisis +network.Nodes() # 254 hosts disponibles +host.NetworkAddress[] # 192.168.0.0/24 +host.BroadcastAddress[] # 192.168.0.255/24 + +# Cálculos con variables +base_ip = IP4[10.0.x.0/24] +solve(base_ip.Nodes() == 254, x) # Encuentra x para 254 hosts +``` + +### Programación y Conversiones +```python +# Conversiones entre bases +hex_val = Hex[FF] # 255 en decimal +bin_val = Bin[hex_val] # Convertir a binario +chr_val = Chr[hex_val] # Carácter ASCII + +# Análisis de caracteres +text = Chr[Hello World] +ascii_values = text.value # Lista de valores ASCII + +# Operaciones bit a bit (con SymPy) +a = Hex[F0] +b = Hex[0F] +a | b # OR bit a bit +a & b # AND bit a bit +a ^ b # XOR bit a bit +``` + +### Análisis Matemático Completo +```python +# Definir función compleja +f = sin(x) * exp(-x**2/2) + +# Análisis completo +df_dx = diff(f, x) # Derivada +critical_points = solve(df_dx, x) # Puntos críticos +integral = integrate(f, (x, -oo, oo)) # Integral impropia + +# Visualización +plot(f, df_dx, (x, -3, 3)) # Plot función y derivada + +# Serie de Taylor en punto específico +taylor_series = series(f, x, 0, 6) # Serie alrededor de x=0 +``` + +### Resolución de Sistemas Complejos +```python +# Sistema de ecuaciones no lineales +x**2 + y**2 = 25 +x*y = 12 + +# Resolver +solutions = solve([x**2 + y**2 - 25, x*y - 12], [x, y]) + +# Análisis paramétrico +# Ecuación con parámetro +a*x**2 + b*x + c = 0 + +# Resolver para diferentes valores +a_val = 1 +b_val = 2 +c_val = -3 +solve(a_val*x**2 + b_val*x + c_val, x) +``` + +## Interfaz de Usuario + +### Paneles +- **Panel Izquierdo**: Entrada de código +- **Panel Derecho**: Resultados con colores y elementos interactivos + +### Menús +- **Archivo**: Nuevo, Cargar, Guardar +- **Editar**: Limpiar, operaciones de texto +- **CAS**: Mostrar variables/ecuaciones, resolver sistema +- **Ayuda**: Guías, sintaxis, funciones SymPy + +### Atajos de Teclado +- **Ctrl+S**: Guardar archivo +- **Ctrl+O**: Abrir archivo +- **Ctrl+N**: Nueva sesión +- **F1**: Ayuda rápida + +### Menú Contextual (Clic Derecho) +- **Panel Entrada**: Cortar, Copiar, Pegar, Insertar ejemplo +- **Panel Salida**: Copiar todo, Limpiar salida + +## Configuración y Personalización + +### Archivos de Configuración +- **`hybrid_calc_settings.json`**: Configuración de ventana y UI +- **`hybrid_calc_history.txt`**: Historial de sesión anterior + +### Variables de Entorno +- **`PYTHONPATH`**: Asegurar que módulos sean encontrados +- **`MPLBACKEND`**: Backend de matplotlib (ej: `TkAgg`) + +## Resolución de Problemas + +### Errores Comunes + +#### Dependencias Faltantes +```bash +# Error: ModuleNotFoundError: No module named 'sympy' +pip install sympy matplotlib numpy + +# Linux: tkinter no disponible +sudo apt-get install python3-tk +``` + +#### Errores de Sintaxis +```python +# Incorrecto: sintaxis antigua +IP4("192.168.1.1/24") + +# Correcto: nueva sintaxis +IP4[192.168.1.1/24] +``` + +#### Problemas de Variables +```python +# Las variables son símbolos automáticamente +x = 5 # x es Symbol('x') con valor 5, no variable Python +y = x + 2 # y es Symbol('x') + 2, evaluado como 7 + +# Para variables Python tradicionales, usar eval explícito: +@eval: python_var = 5 # Sintaxis especial (si implementada) +``` + +### Performance + +#### Optimizaciones +- Las líneas anteriores se cachean (no se re-evalúan) +- Parsing de corchetes se cachea para expresiones repetidas +- `evalf()` es lazy (solo cuando se muestra resultado) + +#### Límites Conocidos +- Sistemas de ecuaciones muy grandes pueden ser lentos +- Plots 3D complejos requieren tiempo de renderizado +- Matrices muy grandes pueden consumir memoria + +## Desarrollo y Extensión + +### Estructura del Proyecto +``` +calculadora-mav-cas/ +├── launcher.py # Launcher principal +├── setup.py # Script de instalación +├── test_suite.py # Tests unitarios +├── requirements.txt # Dependencias +├── bracket_parser.py # Parser de sintaxis +├── hybrid_base_types.py # Clases especializadas +├── hybrid_evaluation_engine.py # Motor CAS +├── interactive_results.py # Resultados clickeables +└── hybrid_calc_app.py # Aplicación principal +``` + +### Agregar Nuevas Clases +```python +# En hybrid_base_types.py +class HybridNewType(HybridCalcType): + def __new__(cls, value_input): + obj = HybridCalcType.__new__(cls) + return obj + + def __init__(self, value_input): + # Procesar entrada + super().__init__(processed_value, original_str) + + def specialized_method(self): + # Funcionalidad específica + pass +``` + +### Extender Parser +```python +# En bracket_parser.py +class BracketParser: + BRACKET_CLASSES = {'IP4', 'Hex', 'Bin', 'Date', 'Dec', 'Chr', 'NewType'} +``` + +## Changelog + +### Versión 2.0 (CAS Híbrido) +- ✅ Motor SymPy completo como base +- ✅ Sintaxis con corchetes únicamente +- ✅ Detección automática de ecuaciones +- ✅ Variables SymPy puras +- ✅ Resultados interactivos clickeables +- ✅ Sistema de plotting integrado +- ✅ Arquitectura modular extensible + +### Diferencias vs Versión 1.0 +| Característica | v1.0 | v2.0 | +|---|---|---| +| Motor | Python eval/exec | SymPy completo | +| Sintaxis | `Class("args")` | `Class[args]` | +| Ecuaciones | `"x + 2 = 5"` | `x + 2 = 5` (automático) | +| Variables | Python mixto | SymPy puro | +| Resultados | Texto plano | Interactivos clickeables | +| Funciones | Básicas math | SymPy completo | + +## FAQ + +### ¿Puedo usar la sintaxis antigua? +No. La v2.0 usa exclusivamente sintaxis con corchetes para consistencia y simplicidad. + +### ¿Cómo evalúo código Python tradicional? +Todas las operaciones se manejan con SymPy. Para casos especiales, se puede implementar sintaxis `@eval:` en futuras versiones. + +### ¿Los resultados son exactos? +Sí. SymPy maneja aritmética exacta por defecto. Use `.evalf()` para aproximaciones decimales. + +### ¿Puedo crear plots personalizados? +Sí. Los resultados de `plot()` son clickeables y abren matplotlib completo con opciones de personalización. + +### ¿Cómo reportar bugs? +Ejecute `python test_suite.py` para diagnosticar problemas y reporte cualquier test que falle. + +--- + +*Calculadora MAV - CAS Híbrido v2.0* +*Desarrollado para cálculo matemático avanzado con soporte especializado* \ No newline at end of file diff --git a/corrections_readme.md b/corrections_readme.md new file mode 100644 index 0000000..166c104 --- /dev/null +++ b/corrections_readme.md @@ -0,0 +1,160 @@ +# Correcciones Implementadas - CAS Híbrido + +## 🔧 Problemas Identificados y Solucionados + +### 1. **Operaciones Aritméticas con Clases Híbridas** +**Problema:** `Error: unsupported operand type(s) for +: 'HybridHex' and 'One'` + +**Solución:** +- ✅ Implementados métodos mágicos de SymPy en `HybridCalcType` +- ✅ Agregados `__add__`, `__mul__`, `__sub__`, `__truediv__`, `__pow__` +- ✅ Integración completa con operaciones SymPy + +```python +# Ahora funciona: +Hex[FF] + 1 # Retorna expresión SymPy válida +Bin[1010] * 2 # Multiplicación correcta +``` + +### 2. **Parser de Corchetes - Métodos Vacíos** +**Problema:** `IP4("192.168.1.100/24").NetworkAddress[]` no parseaba correctamente + +**Solución:** +- ✅ Agregado patrón para `método[]` → `método()` +- ✅ Expresión regular mejorada para corchetes vacíos + +```python +# Ahora funciona: +IP4[192.168.1.100/24].NetworkAddress[] # → IP4("192.168.1.100/24").NetworkAddress() +``` + +### 3. **Asignaciones de Variables** +**Problema:** SymPy no puede parsear `z = 5` directamente + +**Solución:** +- ✅ Detección automática de asignaciones en parser +- ✅ Función especial `_assign_variable()` en motor +- ✅ Integración con tabla de símbolos + +```python +# Ahora funciona: +z = 5 # Detectado como asignación +w = z**2 + 3 # Evaluación correcta +``` + +### 4. **Integración SymPy Mejorada** +**Problema:** Conflictos entre evaluación SymPy y clases especializadas + +**Solución:** +- ✅ Evaluación híbrida: SymPy primero, fallback a eval +- ✅ Manejo de contexto mejorado +- ✅ Propiedades SymPy implementadas en clases base + +## 🧪 Verificar Correcciones + +### Ejecutar Tests de Debug +```bash +python debug_and_test.py +``` + +### Ejecutar Ejemplos Corregidos +```bash +python launcher.py +# Luego pegar el contenido de corrected_examples.py +``` + +### Verificar Suite Completa +```bash +python test_suite.py +``` + +## 📋 Resultados Esperados + +### Antes (Errores) +``` +Hex[FF] + 1 → Error: unsupported operand type(s) +IP4[...].NetworkAddress[] → SyntaxError: invalid syntax +z = 5 → SyntaxError: invalid syntax +``` + +### Después (Correcto) +``` +Hex[FF] + 1 → 256 (o expresión SymPy equivalente) +IP4[192.168.1.100/24].NetworkAddress[] → 192.168.1.0/24 +z = 5 → z = 5 (variable asignada) +w = z**2 + 3 → w = 28 (evaluado) +``` + +## 🔍 Cambios Técnicos Específicos + +### En `bracket_parser.py` +- ✅ Método `_is_assignment()` para detectar asignaciones +- ✅ Método `_transform_assignment()` para convertir a función especial +- ✅ Patrón regex para `método[]` → `método()` + +### En `hybrid_base_types.py` +- ✅ Métodos mágicos completos en `HybridCalcType` +- ✅ Propiedades SymPy: `is_number`, `is_real`, `is_integer` +- ✅ Método `_eval_evalf()` para evaluación numérica + +### En `hybrid_evaluation_engine.py` +- ✅ Función `_assign_variable()` para manejar asignaciones +- ✅ Método `_evaluate_assignment()` para procesar asignaciones +- ✅ Evaluación híbrida mejorada en `_eval_in_context()` + +### En `hybrid_calc_app.py` +- ✅ Manejo de resultado tipo "assignment" +- ✅ Tag de color "info" para asignaciones + +## 🚨 Problemas Conocidos y Limitaciones + +### Limitaciones Actuales +1. **Operaciones complejas**: Algunas operaciones muy complejas pueden requerir evaluación manual +2. **Performance**: Evaluación híbrida puede ser más lenta que SymPy puro +3. **Compatibilidad**: Algunas funciones SymPy avanzadas pueden requerir ajustes + +### Soluciones de Trabajo +```python +# Si una operación no funciona automáticamente: +result = sympify("Hex[FF] + 1") # Forzar evaluación SymPy + +# Para debugging: +engine.debug = True # Activar modo debug +``` + +## 📈 Próximos Pasos + +### Mejoras Sugeridas +1. **Performance**: Implementar caching más agresivo +2. **Operaciones**: Agregar más operaciones especializadas +3. **UI**: Mejorar feedback de errores en interfaz +4. **Testing**: Expandir suite de tests para casos edge + +### Testing Continuo +```bash +# Ejecutar antes de cada sesión: +python debug_and_test.py + +# Verificar funcionalidad específica: +python -c " +from hybrid_evaluation_engine import HybridEvaluationEngine +engine = HybridEvaluationEngine() +print(engine.evaluate_line('Hex[FF] + 1')) +" +``` + +## ✅ Checklist de Verificación + +- [ ] `python debug_and_test.py` pasa todos los tests +- [ ] `Hex[FF] + 1` retorna resultado válido +- [ ] `IP4[...].NetworkAddress[]` funciona correctamente +- [ ] `z = 5` se asigna correctamente +- [ ] Ejemplos de `corrected_examples.py` funcionan +- [ ] Interfaz gráfica inicia sin errores +- [ ] Resultados interactivos son clickeables + +--- + +**¡Las correcciones están implementadas y listas para usar!** 🎉 + +Para cualquier problema adicional, ejecutar `debug_and_test.py` para diagnosticar. \ No newline at end of file diff --git a/debug_and_test.py b/debug_and_test.py new file mode 100644 index 0000000..074d654 --- /dev/null +++ b/debug_and_test.py @@ -0,0 +1,181 @@ +#!/usr/bin/env python3 +""" +Script de debug y testing para verificar correcciones del error as_coeff_Mul +""" +import sys +from pathlib import Path + +# Agregar directorio actual al path +sys.path.insert(0, str(Path(__file__).parent)) + +from bracket_parser import BracketParser +from hybrid_evaluation_engine import HybridEvaluationEngine +from hybrid_base_types import Hex, Bin, IP4, HybridCalcType +import sympy + + +def test_sympy_integration(): + """Test específico de integración con SymPy""" + print("=== Test Integración SymPy ===") + + try: + # Test creación básica + h = Hex("FF") + print(f"✅ Hex[FF] creado: {h}") + print(f" Valor interno: {h._value}") + print(f" Es SymPy Basic: {isinstance(h, sympy.Basic)}") + print(f" Args: {h.args}") + print(f" Func: {h.func}") + + # Test métodos requeridos por SymPy + print(f"✅ as_coeff_Mul(): {h.as_coeff_Mul()}") + print(f"✅ as_coeff_Add(): {h.as_coeff_Add()}") + print(f"✅ as_base_exp(): {h.as_base_exp()}") + + # Test propiedades + print(f"✅ is_number: {h.is_number}") + print(f"✅ is_real: {h.is_real}") + print(f"✅ is_integer: {h.is_integer}") + + except Exception as e: + print(f"❌ Error en test básico: {e}") + import traceback + traceback.print_exc() + + +def test_arithmetic_operations(): + """Test específico de operaciones aritméticas""" + print("\n=== Test Operaciones Aritméticas ===") + + try: + h = Hex("FF") + print(f"Hex[FF] creado: {h} (valor: {h._value})") + + # Test suma simple + print("\n1. Probando h + 1...") + result1 = h + 1 + print(f" Resultado: {result1} (tipo: {type(result1)})") + + # Test con SymPy sympify + print("\n2. Probando sympify(h + 1)...") + try: + expr = sympy.sympify("h + 1", locals={'h': h}) + print(f" SymPy expr: {expr} (tipo: {type(expr)})") + except Exception as e: + print(f" Error en sympify: {e}") + + # Test multiplicación + print("\n3. Probando h * 2...") + result3 = h * 2 + print(f" Resultado: {result3} (tipo: {type(result3)})") + + # Test con Bin + print("\n4. Probando Bin[1010] * 2...") + b = Bin("1010") + result4 = b * 2 + print(f" Bin[1010]: {b} (valor: {b._value})") + print(f" Resultado: {result4} (tipo: {type(result4)})") + + except Exception as e: + print(f"❌ Error en operaciones aritméticas: {e}") + import traceback + traceback.print_exc() + + +def test_evaluation_engine(): + """Test del motor de evaluación con las correcciones""" + print("\n=== Test Motor de Evaluación ===") + + engine = HybridEvaluationEngine() + + test_expressions = [ + # Casos que fallaban antes + "Hex[FF] + 1", + "Bin[1010] * 2", + "IP4[192.168.1.100/24].NetworkAddress[]", + + # Casos básicos + "Hex[FF]", + "Bin[1010]", + + # Asignaciones + "z = 5", + "w = z + 3", + + # SymPy básico + "x + 2*y", + "diff(x**2, x)", + ] + + for expr in test_expressions: + try: + print(f"\nEvaluando: '{expr}'") + result = engine.evaluate_line(expr) + + if result.is_error: + print(f" ❌ Error: {result.error}") + else: + print(f" ✅ Resultado: {result.result} (tipo: {type(result.result)})") + print(f" Info: {result.result_type}") + if result.numeric_result: + print(f" Numérico: {result.numeric_result}") + + except Exception as e: + print(f" ❌ Excepción: {e}") + import traceback + traceback.print_exc() + + +def test_problematic_cases(): + """Test de casos específicos que causaban problemas""" + print("\n=== Test Casos Problemáticos ===") + + # Test directo de la operación que fallaba + print("1. Test directo Hex + int...") + try: + h = Hex("FF") + result = h.__add__(1) + print(f" h.__add__(1): {result} (tipo: {type(result)})") + + # Test usando operador + result2 = h + 1 + print(f" h + 1: {result2} (tipo: {type(result2)})") + + except Exception as e: + print(f" ❌ Error: {e}") + import traceback + traceback.print_exc() + + # Test con SymPy sympify directo + print("\n2. Test SymPy sympify directo...") + try: + h = Hex("FF") + sympified = sympy.sympify(h) + print(f" sympify(Hex[FF]): {sympified} (tipo: {type(sympified)})") + + # Test operaciones con sympified + result = sympified + 1 + print(f" sympified + 1: {result} (tipo: {type(result)})") + + except Exception as e: + print(f" ❌ Error: {e}") + import traceback + traceback.print_exc() + + +def main(): + """Función principal de testing""" + print("Debug y Testing - Corrección error as_coeff_Mul") + print("=" * 60) + + test_sympy_integration() + test_arithmetic_operations() + test_evaluation_engine() + test_problematic_cases() + + print("\n" + "=" * 60) + print("Testing completado.") + + +if __name__ == "__main__": + main() diff --git a/hybrid_base_types.py b/hybrid_base_types.py new file mode 100644 index 0000000..511afbe --- /dev/null +++ b/hybrid_base_types.py @@ -0,0 +1,606 @@ +""" +Clases base híbridas que combinan SymPy con funcionalidad especializada +""" +import sympy +from sympy import Basic, Symbol, sympify +from typing import Any, Optional, Dict +import re + + +class HybridCalcType(Basic): + """ + Clase base híbrida que combina SymPy Basic con funcionalidad de calculadora + Todas las clases especializadas deben heredar de esta + """ + + def __new__(cls, *args, **kwargs): + """Crear objeto SymPy válido""" + obj = Basic.__new__(cls) + return obj + + def __init__(self, value, original_str=""): + """Inicialización de funcionalidad especializada""" + self._value = value + self._original_str = original_str + self._init_specialized() + + def _init_specialized(self): + """Override en subclases para inicialización especializada""" + pass + + @property + def value(self): + """Acceso al valor interno""" + return self._value + + @property + def original_str(self): + """String original de entrada""" + return self._original_str + + # Propiedades requeridas por SymPy + @property + def args(self): + """Argumentos para SymPy - retorna valor como argumento""" + return (sympify(self._value),) + + @property + def func(self): + """Función constructora para SymPy""" + return self.__class__ + + def _sympystr(self, printer): + """Representación SymPy string""" + return f"{self.__class__.__name__}({self._original_str})" + + def _latex(self, printer): + """Representación LaTeX""" + return self._sympystr(printer) + + def __str__(self): + """Representación string para display""" + return str(self._value) + + def __repr__(self): + """Representación para debugging""" + return f"{self.__class__.__name__}('{self._original_str}')" + + def evalf(self, n=15, **options): + """Evaluación numérica si es posible""" + if isinstance(self._value, (int, float, complex)): + return sympy.Float(self._value) + return self + + def _eval_evalf(self, prec): + """Evaluación numérica para SymPy""" + if isinstance(self._value, (int, float, complex)): + return sympy.Float(self._value, prec) + return None + + def __dec__(self): + """Conversión a decimal para compatibilidad""" + if isinstance(self._value, (int, float, complex)): + return self._value + raise TypeError(f"Cannot convert {self.__class__.__name__} to decimal") + + # Métodos requeridos por SymPy para manipulación algebraica + def as_coeff_Mul(self, rational=True): + """Descomponer como coeficiente * términos""" + if isinstance(self._value, (int, float)): + return sympify(self._value), sympify(1) + return sympify(1), self + + def as_coeff_Add(self, rational=True): + """Descomponer como coeficiente + términos""" + if isinstance(self._value, (int, float)): + return sympify(self._value), sympify(0) + return sympify(0), self + + def as_base_exp(self): + """Descomponer como base^exponente""" + return self, sympify(1) + + def as_numer_denom(self): + """Descomponer como numerador/denominador""" + return self, sympify(1) + + # Propiedades de tipo para SymPy + @property + def is_number(self): + """Indica si es un número""" + return isinstance(self._value, (int, float, complex)) + + @property + def is_real(self): + """Indica si es real""" + return isinstance(self._value, (int, float)) and not isinstance(self._value, complex) + + @property + def is_integer(self): + """Indica si es entero""" + return isinstance(self._value, int) + + @property + def is_rational(self): + """Indica si es racional""" + return isinstance(self._value, (int, float)) + + @property + def is_irrational(self): + """Indica si es irracional""" + return False + + @property + def is_positive(self): + """Indica si es positivo""" + if isinstance(self._value, (int, float)): + return self._value > 0 + return None + + @property + def is_negative(self): + """Indica si es negativo""" + if isinstance(self._value, (int, float)): + return self._value < 0 + return None + + @property + def is_zero(self): + """Indica si es cero""" + if isinstance(self._value, (int, float)): + return self._value == 0 + return None + + @property + def is_finite(self): + """Indica si es finito""" + return isinstance(self._value, (int, float, complex)) + + @property + def is_infinite(self): + """Indica si es infinito""" + return False + + @property + def is_commutative(self): + """Indica si es conmutativo""" + return True + + # Operaciones aritméticas simples que retornan el valor numérico para integración con SymPy + def __add__(self, other): + """Suma que integra con SymPy""" + if isinstance(other, (int, float, complex)): + # Retornar valor numérico para que SymPy maneje la operación + return sympify(self._value + other) + elif hasattr(other, '_value'): + return sympify(self._value + other._value) + else: + # Dejar que SymPy maneje la operación + return sympify(self._value) + sympify(other) + + def __radd__(self, other): + """Suma reversa""" + return sympify(other) + sympify(self._value) + + def __mul__(self, other): + """Multiplicación que integra con SymPy""" + if isinstance(other, (int, float, complex)): + return sympify(self._value * other) + elif hasattr(other, '_value'): + return sympify(self._value * other._value) + else: + return sympify(self._value) * sympify(other) + + def __rmul__(self, other): + """Multiplicación reversa""" + return sympify(other) * sympify(self._value) + + def __sub__(self, other): + """Resta""" + if isinstance(other, (int, float, complex)): + return sympify(self._value - other) + elif hasattr(other, '_value'): + return sympify(self._value - other._value) + else: + return sympify(self._value) - sympify(other) + + def __rsub__(self, other): + """Resta reversa""" + return sympify(other) - sympify(self._value) + + def __truediv__(self, other): + """División""" + if isinstance(other, (int, float, complex)): + if other == 0: + raise ZeroDivisionError("Division by zero") + return sympify(self._value / other) + elif hasattr(other, '_value'): + if other._value == 0: + raise ZeroDivisionError("Division by zero") + return sympify(self._value / other._value) + else: + return sympify(self._value) / sympify(other) + + def __rtruediv__(self, other): + """División reversa""" + if self._value == 0: + raise ZeroDivisionError("Division by zero") + return sympify(other) / sympify(self._value) + + def __pow__(self, other): + """Potencia""" + if isinstance(other, (int, float, complex)): + return sympify(self._value ** other) + elif hasattr(other, '_value'): + return sympify(self._value ** other._value) + else: + return sympify(self._value) ** sympify(other) + + def __rpow__(self, other): + """Potencia reversa""" + return sympify(other) ** sympify(self._value) + + @staticmethod + def Helper(input_str): + """Override en subclases para ayuda contextual""" + return None + + +class HybridHex(HybridCalcType): + """Clase híbrida para números hexadecimales""" + + def __new__(cls, value_input): + obj = HybridCalcType.__new__(cls) + return obj + + def __init__(self, value_input): + processed_value = None + original_str = str(value_input) + + if isinstance(value_input, HybridCalcType): + processed_value = int(value_input.__dec__()) + elif isinstance(value_input, (int, float)): + processed_value = int(value_input) + else: + # String input + str_input = str(value_input).lower() + if str_input.startswith("0x"): + processed_value = int(str_input, 16) + else: + processed_value = int(str_input, 16) + + super().__init__(processed_value, original_str) + + def __str__(self): + return f"0x{self._value:X}" + + def _sympystr(self, printer): + return f"Hex({self._value})" + + @staticmethod + def Helper(input_str): + if re.match(r"^\s*Hex(\b|\s*[\[\(].*)?", input_str, re.IGNORECASE): + return 'Ej: Hex[FF] o Hex[255]' + return None + + def toDecimal(self): + """Convertir a decimal""" + return self._value + + +class HybridBin(HybridCalcType): + """Clase híbrida para números binarios""" + + def __new__(cls, value_input): + obj = HybridCalcType.__new__(cls) + return obj + + def __init__(self, value_input): + processed_value = None + original_str = str(value_input) + + if isinstance(value_input, HybridCalcType): + processed_value = int(value_input.__dec__()) + elif isinstance(value_input, (int, float)): + processed_value = int(value_input) + else: + # String input + str_input = str(value_input).lower() + if str_input.startswith("0b"): + processed_value = int(str_input, 2) + else: + processed_value = int(str_input, 2) + + super().__init__(processed_value, original_str) + + def __str__(self): + return f"0b{bin(self._value)[2:]}" + + def _sympystr(self, printer): + return f"Bin({self._value})" + + @staticmethod + def Helper(input_str): + if re.match(r"^\s*Bin(\b|\s*[\[\(].*)?", input_str, re.IGNORECASE): + return 'Ej: Bin[1011] o Bin[11]' + return None + + +class HybridDec(HybridCalcType): + """Clase híbrida para números decimales""" + + def __new__(cls, value_input): + obj = HybridCalcType.__new__(cls) + return obj + + def __init__(self, value_input): + processed_value = None + original_str = str(value_input) + + if isinstance(value_input, HybridCalcType): + processed_value = float(value_input.__dec__()) + elif isinstance(value_input, (int, float)): + processed_value = float(value_input) + else: + try: + processed_value = float(str(value_input)) + except (ValueError, TypeError) as e: + raise ValueError(f"Invalid decimal value: '{value_input}'") from e + + super().__init__(processed_value, original_str) + + def __str__(self): + if self._value == int(self._value): + return str(int(self._value)) + return str(self._value) + + def _sympystr(self, printer): + return f"Dec({self._value})" + + @staticmethod + def Helper(input_str): + if re.match(r"^\s*Dec(\b|\s*[\[\(].*)?", input_str, re.IGNORECASE): + return 'Ej: Dec[10.5] o Dec[10]' + return None + + +class HybridIP4(HybridCalcType): + """Clase híbrida para direcciones IPv4""" + + def __new__(cls, *args): + obj = HybridCalcType.__new__(cls) + return obj + + def __init__(self, *args): + import socket + import struct + + if not args: + raise ValueError("IP4() constructor requires at least one argument.") + + ip_str = None + prefix = None + + if len(args) == 1: + arg = args[0] + if not isinstance(arg, str): + raise ValueError(f"IP4 single argument must be a string, got '{arg}'") + + if "/" in arg: # CIDR notation + parts = arg.split('/', 1) + ip_str = parts[0] + if len(parts) == 2 and parts[1].isdigit(): + prefix_val = int(parts[1]) + if not self._is_valid_prefix(prefix_val): + raise ValueError(f"Invalid CIDR prefix '{parts[1]}' in '{arg}'") + prefix = prefix_val + else: + raise ValueError(f"Invalid CIDR format: '{arg}'") + else: + ip_str = arg + + elif len(args) == 2: + ip_arg, mask_arg = args[0], args[1] + if not isinstance(ip_arg, str): + raise ValueError(f"IP4 first argument must be an IP string") + ip_str = ip_arg + + if isinstance(mask_arg, int): + if not self._is_valid_prefix(mask_arg): + raise ValueError(f"Invalid prefix length: {mask_arg}") + prefix = mask_arg + elif isinstance(mask_arg, str): + if mask_arg.isdigit(): + prefix_val = int(mask_arg) + if self._is_valid_prefix(prefix_val): + prefix = prefix_val + else: + parsed_prefix = self._netmask_str_to_prefix(mask_arg) + if parsed_prefix is None: + raise ValueError(f"Invalid mask string: '{mask_arg}'") + prefix = parsed_prefix + else: + parsed_prefix = self._netmask_str_to_prefix(mask_arg) + if parsed_prefix is None: + raise ValueError(f"Invalid netmask string: '{mask_arg}'") + prefix = parsed_prefix + else: + raise ValueError(f"IP4() constructor takes 1 or 2 arguments, got {len(args)}") + + if not self._is_valid_ip_string(ip_str): + raise ValueError(f"Invalid IP address string: '{ip_str}'") + + self.ip_address = ip_str + self.prefix = prefix + + # Valor para SymPy - usar representación numérica de la IP + ip_as_int = struct.unpack("!I", socket.inet_aton(ip_str))[0] + canonical_repr = f"{ip_str}/{prefix}" if prefix is not None else ip_str + + super().__init__(ip_as_int, canonical_repr) + + def _is_valid_ip_string(self, ip_str: str) -> bool: + """Verifica si es una dirección IP válida""" + import socket + try: + socket.inet_aton(ip_str) + parts = ip_str.split(".") + return len(parts) == 4 and all(0 <= int(part) <= 255 for part in parts) + except (socket.error, ValueError, TypeError): + return False + + def _is_valid_prefix(self, prefix: int) -> bool: + """Verifica si es un prefijo CIDR válido""" + return isinstance(prefix, int) and 0 <= prefix <= 32 + + def _netmask_str_to_prefix(self, netmask_str: str) -> Optional[int]: + """Convierte máscara decimal a prefijo CIDR""" + import socket + import struct + + if not self._is_valid_ip_string(netmask_str): + return None + try: + mask_int = struct.unpack("!I", socket.inet_aton(netmask_str))[0] + except socket.error: + return None + + binary_mask = bin(mask_int)[2:].zfill(32) + if '0' in binary_mask and '1' in binary_mask[binary_mask.find('0'):]: + return None + + return binary_mask.count('1') + + def __str__(self): + if self.prefix is not None: + return f"{self.ip_address}/{self.prefix}" + return self.ip_address + + def _sympystr(self, printer): + return f"IP4({self.__str__()})" + + @staticmethod + def Helper(input_str): + if re.match(r"^\s*IP4(\b|\s*[\[\(].*)?", input_str, re.IGNORECASE): + return 'Ej: IP4[192.168.1.1/24] o IP4[10.0.0.1]' + return None + + def NetworkAddress(self): + """Retorna la dirección de red""" + import socket + import struct + + if self.prefix is None: + raise ValueError("Mask not set, cannot calculate network address.") + + ip_int = struct.unpack("!I", socket.inet_aton(self.ip_address))[0] + mask_int = (0xFFFFFFFF << (32 - self.prefix)) & 0xFFFFFFFF if self.prefix > 0 else 0 + net_addr_int = ip_int & mask_int + net_addr_str = socket.inet_ntoa(struct.pack("!I", net_addr_int)) + return HybridIP4(net_addr_str, self.prefix) + + def BroadcastAddress(self): + """Retorna la dirección de broadcast""" + import socket + import struct + + if self.prefix is None: + raise ValueError("Mask not set, cannot calculate broadcast address.") + + net_addr_obj = self.NetworkAddress() + net_addr_int = struct.unpack("!I", socket.inet_aton(net_addr_obj.ip_address))[0] + wildcard_mask = ((1 << (32 - self.prefix)) - 1) if self.prefix < 32 else 0 + bcast_addr_int = net_addr_int | wildcard_mask + bcast_addr_str = socket.inet_ntoa(struct.pack("!I", bcast_addr_int)) + return HybridIP4(bcast_addr_str, self.prefix) + + def Nodes(self): + """Retorna el número de hosts disponibles""" + if self.prefix is None: + raise ValueError("Mask not set, cannot calculate number of nodes.") + + host_bits = 32 - self.prefix + if host_bits < 2: + return 0 + return (1 << host_bits) - 2 + + +class HybridChr(HybridCalcType): + """Clase híbrida para caracteres ASCII""" + + def __new__(cls, str_input): + obj = HybridCalcType.__new__(cls) + return obj + + def __init__(self, str_input: str): + if not isinstance(str_input, str): + raise TypeError(f"Chr input must be a string, got {type(str_input).__name__}") + if not str_input: + raise ValueError("Chr input string cannot be empty.") + + if len(str_input) == 1: + processed_value = ord(str_input) + else: + processed_value = [ord(c) for c in str_input] + + super().__init__(processed_value, str_input) + + def __str__(self): + if isinstance(self._value, int): + return chr(self._value) + elif isinstance(self._value, list): + return "".join(map(chr, self._value)) + return super().__str__() + + def _sympystr(self, printer): + return f"Chr({self._original_str})" + + @staticmethod + def Helper(input_str): + if re.match(r"^\s*Chr(\b|\s*[\[\(].*)?", input_str, re.IGNORECASE): + return 'Ej: Chr[A] o Chr[Hello]. Representa caracteres ASCII.' + return None + + +# Alias para compatibilidad +Hex = HybridHex +Bin = HybridBin +Dec = HybridDec +IP4 = HybridIP4 +Chr = HybridChr + + +# Funciones de testing +def test_hybrid_classes(): + """Test de las clases híbridas""" + print("=== Test Clases Híbridas ===") + + # Test Hex + h = Hex("FF") + print(f"Hex('FF'): {h} (type: {type(h)})") + print(f" SymPy str: {h._sympystr(None)}") + print(f" Is SymPy Basic: {isinstance(h, sympy.Basic)}") + print(f" Value: {h._value}") + + # Test operación simple + try: + result = h + 1 + print(f" Hex + 1: {result} (type: {type(result)})") + except Exception as e: + print(f" Error en Hex + 1: {e}") + + # Test IP4 + ip = IP4("192.168.1.100/24") + print(f"IP4('192.168.1.100/24'): {ip}") + print(f" NetworkAddress: {ip.NetworkAddress()}") + print(f" Is SymPy Basic: {isinstance(ip, sympy.Basic)}") + + # Test Chr + c = Chr("A") + print(f"Chr('A'): {c}") + print(f" ASCII value: {c._value}") + print(f" Is SymPy Basic: {isinstance(c, sympy.Basic)}") + + +if __name__ == "__main__": + test_hybrid_classes() diff --git a/hybrid_calc_app.py b/hybrid_calc_app.py new file mode 100644 index 0000000..4f39f6c --- /dev/null +++ b/hybrid_calc_app.py @@ -0,0 +1,833 @@ +""" +Calculadora MAV CAS Híbrida - Aplicación principal +""" +import tkinter as tk +from tkinter import scrolledtext, messagebox, Menu, filedialog +import tkinter.font as tkFont +import json +import os +import threading +from typing import List, Dict, Any, Optional + +# Importar componentes del CAS híbrido +from hybrid_evaluation_engine import HybridEvaluationEngine, EvaluationResult +from interactive_results import InteractiveResultManager +from hybrid_base_types import Hex, Bin, Dec, IP4, Chr +import sympy + + +class HybridCalculatorApp: + """Aplicación principal del CAS híbrido""" + + SETTINGS_FILE = "hybrid_calc_settings.json" + HISTORY_FILE = "hybrid_calc_history.txt" + + def __init__(self, root: tk.Tk): + self.root = root + self.root.title("Calculadora MAV - CAS Híbrido") + + # Cargar configuración + self.settings = self._load_settings() + self.root.geometry(self.settings.get("window_geometry", "1000x700")) + self.root.configure(bg="#2b2b2b") + + # Configurar ícono + self._setup_icon() + + # Componentes principales + self.engine = HybridEvaluationEngine() + self.interactive_manager = None # Se inicializa después de crear widgets + + # Estado de la aplicación + self._debounce_job = None + self._syncing_yview = False + self._cached_input_font = None + self.output_buffer = [] + + # Crear interfaz + self.create_widgets() + self.setup_interactive_manager() + self.load_history() + + # Configurar eventos de cierre + self.root.protocol("WM_DELETE_WINDOW", self.on_close) + + def _setup_icon(self): + """Configura el ícono de la aplicación""" + try: + self.app_icon = tk.PhotoImage(file="icon.png") + self.root.iconphoto(True, self.app_icon) + except tk.TclError: + print("Advertencia: No se pudo cargar 'icon.png' como ícono.") + + def _load_settings(self) -> Dict[str, Any]: + """Carga configuración de la aplicación""" + if os.path.exists(self.SETTINGS_FILE): + try: + with open(self.SETTINGS_FILE, "r", encoding="utf-8") as f: + return json.load(f) + except (IOError, json.JSONDecodeError): + return {} + return {} + + def _save_settings(self): + """Guarda configuración de la aplicación""" + self.settings["window_geometry"] = self.root.winfo_geometry() + if hasattr(self, "paned_window"): + try: + sash_x_pos = self.paned_window.sash_coord(0)[0] + self.settings["sash_pos_x"] = sash_x_pos + except tk.TclError: + pass + + try: + with open(self.SETTINGS_FILE, "w", encoding="utf-8") as f: + json.dump(self.settings, f, indent=4) + except IOError: + messagebox.showwarning("Error", "No se pudieron guardar los ajustes.") + + def create_widgets(self): + """Crea la interfaz gráfica""" + # Frame principal + main_frame = tk.Frame(self.root, bg="#2b2b2b", bd=0) + main_frame.pack(fill=tk.BOTH, expand=True, padx=0, pady=0) + + # Panel dividido + self.paned_window = tk.PanedWindow( + main_frame, orient=tk.HORIZONTAL, bg="#2b2b2b", + sashrelief=tk.FLAT, sashwidth=4, bd=0, + showhandle=False, opaqueresize=True, + ) + self.paned_window.pack(fill=tk.BOTH, expand=True, padx=0, pady=0) + + # Panel de entrada + initial_input_width = self.settings.get("sash_pos_x", 450) + + self.input_text = scrolledtext.ScrolledText( + self.paned_window, + font=("Consolas", 11), + bg="#1e1e1e", + fg="#d4d4d4", + insertbackground="#ffffff", + selectbackground="#264f78", + undo=True, + wrap=tk.NONE, + borderwidth=0, + highlightthickness=0, + relief=tk.FLAT, + ) + self.paned_window.add( + self.input_text, + width=initial_input_width, + stretch="always", + minsize=200 + ) + + # Panel de salida + self.output_text = scrolledtext.ScrolledText( + self.paned_window, + font=("Consolas", 11), + bg="#0f0f0f", + fg="#00ff00", + state="disabled", + wrap=tk.NONE, + borderwidth=0, + highlightthickness=0, + relief=tk.FLAT, + ) + self.paned_window.add( + self.output_text, + stretch="always", + minsize=200 + ) + + # Configurar eventos + self.input_text.bind("", self.on_key_release) + self.input_text.bind("", lambda e: self._show_context_menu(e, "input")) + self.output_text.bind("", lambda e: self._show_context_menu(e, "output")) + + # Configurar scroll sincronizado + self.setup_scroll_sync() + + # Configurar tags de salida + self.setup_output_tags() + + # Crear menú + self.create_menu() + + def setup_interactive_manager(self): + """Configura el gestor de resultados interactivos""" + self.interactive_manager = InteractiveResultManager(self.root) + + def create_menu(self): + """Crea el menú de la aplicación""" + menubar = Menu(self.root) + self.root.config(menu=menubar) + + # Menú Archivo + file_menu = Menu(menubar, tearoff=0) + menubar.add_cascade(label="Archivo", menu=file_menu) + file_menu.add_command(label="Nuevo", command=self.new_session) + file_menu.add_separator() + file_menu.add_command(label="Cargar...", command=self.load_file) + file_menu.add_command(label="Guardar como...", command=self.save_file) + file_menu.add_separator() + file_menu.add_command(label="Salir", command=self.on_close) + + # Menú Editar + edit_menu = Menu(menubar, tearoff=0) + menubar.add_cascade(label="Editar", menu=edit_menu) + edit_menu.add_command(label="Limpiar entrada", command=self.clear_input) + edit_menu.add_command(label="Limpiar salida", command=self.clear_output) + edit_menu.add_separator() + edit_menu.add_command(label="Limpiar variables", command=self.clear_variables) + edit_menu.add_command(label="Limpiar ecuaciones", command=self.clear_equations) + edit_menu.add_command(label="Limpiar todo", command=self.clear_all) + + # Menú CAS + cas_menu = Menu(menubar, tearoff=0) + menubar.add_cascade(label="CAS", menu=cas_menu) + cas_menu.add_command(label="Mostrar variables", command=self.show_variables) + cas_menu.add_command(label="Mostrar ecuaciones", command=self.show_equations) + cas_menu.add_separator() + cas_menu.add_command(label="Resolver sistema", command=self.solve_system) + + # Menú Ayuda + help_menu = Menu(menubar, tearoff=0) + menubar.add_cascade(label="Ayuda", menu=help_menu) + help_menu.add_command(label="Guía rápida", command=self.show_quick_guide) + help_menu.add_command(label="Sintaxis", command=self.show_syntax_help) + help_menu.add_command(label="Funciones SymPy", command=self.show_sympy_functions) + help_menu.add_separator() + help_menu.add_command(label="Acerca de", command=self.show_about) + + def setup_scroll_sync(self): + """Configura scroll sincronizado entre paneles""" + def _yscroll_input_command(*args): + self.input_text.vbar.set(*args) + if not self._syncing_yview: + self._syncing_yview = True + self.output_text.yview_moveto(args[0]) + self._syncing_yview = False + + def _yscroll_output_command(*args): + self.output_text.vbar.set(*args) + if not self._syncing_yview: + self._syncing_yview = True + self.input_text.yview_moveto(args[0]) + self._syncing_yview = False + + def _unified_mouse_wheel(event): + if self._syncing_yview: + return "break" + if hasattr(event, "widget") and event.widget: + event.widget.yview_scroll(int(-1 * (event.delta / 120)), "units") + return "break" + + self.input_text.config(yscrollcommand=_yscroll_input_command) + self.output_text.config(yscrollcommand=_yscroll_output_command) + self.input_text.bind("", _unified_mouse_wheel) + 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("type_hint", foreground="#6a6a6a") + + # Tags para tipos especializados + 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") + + def on_key_release(self, event=None): + """Maneja eventos de teclado""" + if self._debounce_job: + self.root.after_cancel(self._debounce_job) + + # Autocompletado con punto + if event and event.char == '.' and self.input_text.focus_get() == self.input_text: + self._handle_dot_autocomplete() + + # Evaluación con debounce + self._debounce_job = self.root.after(300, self._evaluate_and_update) + + def _handle_dot_autocomplete(self): + """Maneja autocompletado con punto (simplificado por ahora)""" + # TODO: Implementar autocompletado para métodos de objetos + pass + + def _evaluate_and_update(self): + """Evalúa todas las líneas y actualiza la salida""" + try: + input_content = self.input_text.get("1.0", tk.END) + if not input_content.strip(): + self._clear_output() + return + + lines = input_content.splitlines() + self._evaluate_lines(lines) + + except Exception as e: + self._show_error(f"Error durante evaluación: {e}") + + def _evaluate_lines(self, lines: List[str]): + """Evalúa múltiples líneas de código""" + output_data = [] + + for line_num, line in enumerate(lines, 1): + line = line.strip() + + # Líneas vacías o comentarios + if not line or line.startswith('#'): + if line: + output_data.append([("comment", line)]) + else: + output_data.append([("", "")]) + continue + + # Evaluar línea + result = self.engine.evaluate_line(line) + line_output = self._process_evaluation_result(result) + output_data.append(line_output) + + self._display_output(output_data) + + def _process_evaluation_result(self, result: EvaluationResult) -> List[tuple]: + """Procesa el resultado de evaluación para display""" + output_parts = [] + + if result.is_error: + output_parts.append(("error", f"Error: {result.error}")) + elif result.result_type == "comment": + output_parts.append(("comment", result.original_line)) + elif result.result_type == "equation_added": + output_parts.append(("equation", result.symbolic_result)) + elif result.result_type == "assignment": + output_parts.append(("info", result.symbolic_result)) + else: + # Resultado normal + if result.result is not None: + # Determinar tag basado en tipo + tag = self._get_result_tag(result.result) + + # Verificar si es resultado interactivo + if self.interactive_manager and result.is_interactive: + interactive_tag, display_text = self.interactive_manager.create_interactive_tag( + result.result, self.output_text, "1.0" + ) + if interactive_tag: + output_parts.append((interactive_tag, display_text)) + else: + output_parts.append((tag, str(result.result))) + else: + output_parts.append((tag, str(result.result))) + + # Mostrar evaluación numérica si existe + if result.numeric_result is not None and result.numeric_result != result.result: + output_parts.append(("numeric", f" ≈ {result.numeric_result}")) + + # Mostrar información adicional + if result.info: + output_parts.append(("info", f" ({result.info})")) + + return output_parts + + def _get_result_tag(self, result: Any) -> str: + """Determina el tag de color para un resultado""" + if isinstance(result, Hex): + return "hex" + elif isinstance(result, Bin): + return "bin" + elif isinstance(result, IP4): + return "ip" + elif isinstance(result, Chr): + return "chr_type" + elif isinstance(result, sympy.Basic): + return "symbolic" + else: + return "result" + + def _display_output(self, output_data: List[List[tuple]]): + """Muestra los datos de salida en el widget""" + self.output_text.config(state="normal") + self.output_text.delete("1.0", tk.END) + + for line_idx, line_parts in enumerate(output_data): + # Línea vacía + if not line_parts or (len(line_parts) == 1 and line_parts[0][0] == "" and line_parts[0][1] == ""): + pass + else: + # Mostrar partes de la línea + first_part = True + for tag, content in line_parts: + if not first_part and content: + self.output_text.insert(tk.END, " ; ") + + if content: + self.output_text.insert(tk.END, str(content), tag) + first_part = False + + # Añadir nueva línea excepto para la última línea + if line_idx < len(output_data) - 1: + self.output_text.insert(tk.END, "\n") + + self.output_text.config(state="disabled") + + def _clear_output(self): + """Limpia el panel de salida""" + self.output_text.config(state="normal") + self.output_text.delete("1.0", tk.END) + self.output_text.config(state="disabled") + + def _show_error(self, error_msg: str): + """Muestra un error en el panel de salida""" + self.output_text.config(state="normal") + self.output_text.delete("1.0", tk.END) + self.output_text.insert("1.0", error_msg, "error") + self.output_text.config(state="disabled") + + def _show_context_menu(self, event, panel_type: str): + """Muestra menú contextual""" + context_menu = Menu( + self.root, tearoff=0, bg="#3c3c3c", fg="white", + activebackground="#007acc", activeforeground="white", + relief=tk.FLAT, bd=1, + ) + + if panel_type == "input": + context_menu.add_command(label="Cortar", command=lambda: self.input_text.event_generate("<>")) + context_menu.add_command(label="Copiar", command=lambda: self.input_text.event_generate("<>")) + context_menu.add_command(label="Pegar", command=lambda: self.input_text.event_generate("<>")) + context_menu.add_separator() + context_menu.add_command(label="Limpiar entrada", command=self.clear_input) + context_menu.add_separator() + context_menu.add_command(label="Insertar ejemplo", command=self.insert_example) + elif panel_type == "output": + context_menu.add_command(label="Copiar todo", command=self.copy_output) + context_menu.add_command(label="Limpiar salida", command=self.clear_output) + + context_menu.add_separator() + context_menu.add_command(label="Ayuda", command=self.show_quick_guide) + + try: + context_menu.tk_popup(event.x_root, event.y_root) + finally: + context_menu.grab_release() + + # Métodos de menú y comandos + def new_session(self): + """Inicia nueva sesión""" + self.clear_input() + self.clear_output() + self.engine.clear_all() + + def load_file(self): + """Carga archivo en el editor""" + filepath = filedialog.askopenfilename( + title="Cargar archivo", + filetypes=[ + ("Archivos de texto", "*.txt"), + ("Archivos Python", "*.py"), + ("Todos los archivos", "*.*") + ] + ) + + if filepath: + try: + with open(filepath, "r", encoding="utf-8") as f: + content = f.read() + + self.input_text.delete("1.0", tk.END) + self.input_text.insert("1.0", content) + self._evaluate_and_update() + + except Exception as e: + messagebox.showerror("Error", f"No se pudo cargar el archivo:\n{e}") + + def save_file(self): + """Guarda contenido del editor""" + filepath = filedialog.asksaveasfilename( + title="Guardar archivo", + defaultextension=".txt", + filetypes=[ + ("Archivos de texto", "*.txt"), + ("Archivos Python", "*.py"), + ("Todos los archivos", "*.*") + ] + ) + + if filepath: + try: + content = self.input_text.get("1.0", tk.END) + with open(filepath, "w", encoding="utf-8") as f: + f.write(content) + + messagebox.showinfo("Éxito", "Archivo guardado correctamente.") + + except Exception as e: + messagebox.showerror("Error", f"No se pudo guardar el archivo:\n{e}") + + def clear_input(self): + """Limpia panel de entrada""" + self.input_text.delete("1.0", tk.END) + self._clear_output() + + def clear_output(self): + """Limpia panel de salida""" + self._clear_output() + + def clear_variables(self): + """Limpia variables del motor""" + self.engine.clear_variables() + self._evaluate_and_update() + + def clear_equations(self): + """Limpia ecuaciones del motor""" + self.engine.clear_equations() + self._evaluate_and_update() + + def clear_all(self): + """Limpia variables y ecuaciones""" + self.engine.clear_all() + self._evaluate_and_update() + + def copy_output(self): + """Copia contenido de salida al clipboard""" + content = self.output_text.get("1.0", tk.END).strip() + if content: + self.root.clipboard_clear() + self.root.clipboard_append(content) + + def insert_example(self): + """Inserta código de ejemplo""" + example = """# Calculadora MAV - CAS Híbrido +# Sintaxis nueva con corchetes + +# Tipos especializados +Hex[FF] + 1 +IP4[192.168.1.100/24].NetworkAddress[] +Bin[1010] * 2 + +# Matemáticas simbólicas +x + 2*y +diff(x**2 + sin(x), x) +integrate(x**2, x) + +# Ecuaciones (detección automática) +x**2 + 2*x - 8 = 0 +3*a + b = 10 + +# Resolver ecuaciones +solve(x**2 + 2*x - 8, x) +a=? + +# Variables automáticas +z = 5 +w = z**2 + 3 + +# Plotting interactivo +plot(sin(x), (x, -2*pi, 2*pi)) + +# Matrices +Matrix([[1, 2], [3, 4]]) +""" + self.input_text.delete("1.0", tk.END) + self.input_text.insert("1.0", example) + self._evaluate_and_update() + + def show_variables(self): + """Muestra ventana con variables definidas""" + variables = self.engine.symbol_table + + window = tk.Toplevel(self.root) + window.title("Variables Definidas") + window.geometry("500x400") + window.configure(bg="#2b2b2b") + + text_widget = scrolledtext.ScrolledText( + window, + font=("Consolas", 11), + bg="#1e1e1e", + fg="#d4d4d4" + ) + text_widget.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) + + if variables: + content = "Variables definidas:\n\n" + for name, value in variables.items(): + content += f"{name} = {value}\n" + else: + content = "No hay variables definidas." + + text_widget.insert("1.0", content) + text_widget.config(state="disabled") + + def show_equations(self): + """Muestra ventana con ecuaciones definidas""" + equations = self.engine.equations + + window = tk.Toplevel(self.root) + window.title("Ecuaciones Definidas") + window.geometry("500x400") + window.configure(bg="#2b2b2b") + + text_widget = scrolledtext.ScrolledText( + window, + font=("Consolas", 11), + bg="#1e1e1e", + fg="#d4d4d4" + ) + text_widget.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) + + if equations: + content = "Ecuaciones en el sistema:\n\n" + for i, eq in enumerate(equations, 1): + content += f"{i}. {eq}\n" + else: + content = "No hay ecuaciones en el sistema." + + text_widget.insert("1.0", content) + text_widget.config(state="disabled") + + def solve_system(self): + """Resuelve el sistema de ecuaciones""" + try: + if not self.engine.equations: + messagebox.showinfo("Info", "No hay ecuaciones para resolver.") + return + + solutions = self.engine.solve_system() + + window = tk.Toplevel(self.root) + window.title("Soluciones del Sistema") + window.geometry("500x400") + window.configure(bg="#2b2b2b") + + text_widget = scrolledtext.ScrolledText( + window, + font=("Consolas", 11), + bg="#1e1e1e", + fg="#d4d4d4" + ) + text_widget.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) + + content = "Soluciones del sistema:\n\n" + if isinstance(solutions, dict): + for var, value in solutions.items(): + content += f"{var} = {value}\n" + else: + content += str(solutions) + + text_widget.insert("1.0", content) + text_widget.config(state="disabled") + + except Exception as e: + messagebox.showerror("Error", f"Error resolviendo sistema:\n{e}") + + def show_quick_guide(self): + """Muestra guía rápida""" + guide = """# Calculadora MAV - CAS Híbrido + +## Sintaxis Nueva con Corchetes +- IP4[192.168.1.1/24] en lugar de IP4("192.168.1.1/24") +- Hex[FF], Bin[1010], Dec[10.5], Chr[A] + +## Ecuaciones Automáticas +- x**2 + 2*x = 8 (detectado automáticamente) +- a + b = 10 (agregado al sistema) +- variable=? (atajo para solve(variable)) + +## Funciones SymPy Disponibles +- solve(), diff(), integrate(), limit(), series() +- sin(), cos(), tan(), exp(), log(), sqrt() +- Matrix(), plot(), plot3d() + +## Resultados Interactivos +- 📊 Ver Plot (click para ventana matplotlib) +- 📋 Ver Matriz (click para vista expandida) +- 📋 Ver Lista (click para contenido completo) + +## Variables Automáticas +- Todas las variables son símbolos SymPy +- x = 5 crea Symbol('x') con valor 5 +- Evaluación simbólica + numérica automática +""" + + self._show_help_window("Guía Rápida", guide) + + def show_syntax_help(self): + """Muestra ayuda de sintaxis""" + syntax = """# Sintaxis del CAS Híbrido + +## Clases Especializadas (solo corchetes) +IP4[dirección/prefijo] # IP4[192.168.1.1/24] +Hex[valor] # Hex[FF], Hex[255] +Bin[valor] # Bin[1010], Bin[10] +Dec[valor] # Dec[10.5], Dec[10] +Chr[carácter] # Chr[A], Chr[Hello] + +## Métodos Disponibles +IP4[...].NetworkAddress[] +IP4[...].BroadcastAddress[] +IP4[...].Nodes() +Hex[...].toDecimal() + +## Ecuaciones (detección automática) +expresión = expresión # Ecuación simple +expresión == expresión # Igualdad SymPy +expresión > expresión # Desigualdad SymPy + +## Resolver +solve(ecuación, variable) +variable=? # Atajo para solve(variable) + +## Variables SymPy Puras +x = valor # Crea Symbol('x') +expresión # Evaluación simbólica automática +""" + + self._show_help_window("Sintaxis", syntax) + + def show_sympy_functions(self): + """Muestra funciones SymPy disponibles""" + functions = """# Funciones SymPy Disponibles + +## Matemáticas Básicas +sin(x), cos(x), tan(x) +asin(x), acos(x), atan(x) +sinh(x), cosh(x), tanh(x) +exp(x), log(x), sqrt(x) +abs(x), sign(x), factorial(x) + +## Cálculo +diff(expr, var) # Derivada +integrate(expr, var) # Integral indefinida +integrate(expr, (var, a, b)) # Integral definida +limit(expr, var, punto) # Límite +series(expr, var, punto, n) # Serie de Taylor + +## Álgebra +solve(ecuación, variable) +simplify(expr), expand(expr) +factor(expr), collect(expr, var) +cancel(expr), apart(expr, var) + +## Álgebra Lineal +Matrix([[a, b], [c, d]]) +det(matrix), inv(matrix) + +## Plotting +plot(expr, (var, inicio, fin)) +plot3d(expr, (x, x1, x2), (y, y1, y2)) + +## Constantes +pi, E, I (imaginario), oo (infinito) +""" + + self._show_help_window("Funciones SymPy", functions) + + def show_about(self): + """Muestra información sobre la aplicación""" + about = """Calculadora MAV - CAS Híbrido + +Versión: 2.0 +Motor: SymPy + Clases Especializadas + +Características: +• Motor algebraico completo (SymPy) +• Sintaxis simplificada con corchetes +• Detección automática de ecuaciones +• Resultados interactivos clickeables +• Tipos especializados (IP4, Hex, Bin, etc.) +• Variables SymPy puras +• Plotting integrado + +Desarrollado para cálculo matemático avanzado +con soporte especializado para redes, +programación y análisis numérico. +""" + + messagebox.showinfo("Acerca de", about) + + def _show_help_window(self, title: str, content: str): + """Muestra ventana de ayuda""" + window = tk.Toplevel(self.root) + window.title(title) + window.geometry("700x500") + window.configure(bg="#2b2b2b") + + text_widget = scrolledtext.ScrolledText( + window, + font=("Consolas", 10), + bg="#1e1e1e", + fg="#d4d4d4", + wrap=tk.WORD + ) + text_widget.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) + + text_widget.insert("1.0", content) + text_widget.config(state="disabled") + + def load_history(self): + """Carga historial de entrada""" + try: + if os.path.exists(self.HISTORY_FILE): + with open(self.HISTORY_FILE, "r", encoding="utf-8") as f: + content = f.read() + + if content.strip(): + self.input_text.insert("1.0", content) + self.root.after_idle(self._evaluate_and_update) + return + except Exception as e: + print(f"Error cargando historial: {e}") + + # Cargar ejemplo por defecto si no hay historial + self.insert_example() + + def save_history(self): + """Guarda historial de entrada""" + try: + content = self.input_text.get("1.0", tk.END).rstrip("\n") + if content: + with open(self.HISTORY_FILE, "w", encoding="utf-8") as f: + f.write(content) + elif os.path.exists(self.HISTORY_FILE): + os.remove(self.HISTORY_FILE) + except Exception as e: + print(f"Error guardando historial: {e}") + + 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() + + +def main(): + """Función principal""" + root = tk.Tk() + app = HybridCalculatorApp(root) + + try: + root.iconname("Calculadora MAV CAS") + except tk.TclError: + pass + + root.mainloop() + + +if __name__ == "__main__": + main() diff --git a/hybrid_calc_history.txt b/hybrid_calc_history.txt new file mode 100644 index 0000000..7dc843a --- /dev/null +++ b/hybrid_calc_history.txt @@ -0,0 +1,30 @@ +# Calculadora MAV - CAS Híbrido +# Sintaxis nueva con corchetes + +# Tipos especializados +Hex[FF] + 1 +IP4[192.168.1.100/24].NetworkAddress[] +Bin[1010] * 2 + +# Matemáticas simbólicas +x + 2*y +diff(x**2 + sin(x), x) +integrate(x**2, x) + +# Ecuaciones (detección automática) +x**2 + 2*x - 8 = 0 +3*a + b = 10 + +# Resolver ecuaciones +solve(x**2 + 2*x - 8, x) +a=? + +# Variables automáticas +z = 5 +w = z**2 + 3 + +# Plotting interactivo +plot(sin(x), (x, -2*pi, 2*pi)) + +# Matrices +Matrix([[1, 2], [3, 4]]) \ No newline at end of file diff --git a/hybrid_calc_settings.json b/hybrid_calc_settings.json new file mode 100644 index 0000000..bb264fd --- /dev/null +++ b/hybrid_calc_settings.json @@ -0,0 +1,4 @@ +{ + "window_geometry": "1000x700+722+83", + "sash_pos_x": 341 +} \ No newline at end of file diff --git a/hybrid_evaluation_engine.py b/hybrid_evaluation_engine.py new file mode 100644 index 0000000..7597da7 --- /dev/null +++ b/hybrid_evaluation_engine.py @@ -0,0 +1,528 @@ +""" +Motor de evaluación híbrida que usa SymPy como base con clases especializadas +""" +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 + +from bracket_parser import BracketParser +from hybrid_base_types import ( + HybridCalcType, HybridHex, HybridBin, HybridDec, + HybridIP4, HybridChr, Hex, Bin, Dec, IP4, Chr +) + + +class HybridEvaluationEngine: + """ + Motor de evaluación híbrida que combina SymPy con clases especializadas + """ + + def __init__(self): + self.parser = BracketParser() + self.symbol_table: Dict[str, Any] = {} + self.equations: List[sympy.Eq] = [] + self.last_result = None + + # Contexto base con funciones y clases + self._setup_base_context() + + # Debug mode + self.debug = False + + def _setup_base_context(self): + """Configura el contexto base con funciones matemáticas y clases""" + # Funciones matemáticas de SymPy + math_functions = { + 'pi': sympy.pi, + 'e': sympy.E, + 'I': sympy.I, + 'oo': sympy.oo, + 'sin': sympy.sin, + 'cos': sympy.cos, + 'tan': sympy.tan, + 'asin': sympy.asin, + 'acos': sympy.acos, + 'atan': sympy.atan, + 'sinh': sympy.sinh, + 'cosh': sympy.cosh, + 'tanh': sympy.tanh, + 'exp': sympy.exp, + 'log': sympy.log, + 'ln': sympy.log, + 'sqrt': sympy.sqrt, + 'abs': sympy.Abs, + 'sign': sympy.sign, + 'floor': sympy.floor, + 'ceiling': sympy.ceiling, + 'factorial': sympy.factorial, + # Funciones de cálculo + 'diff': sympy.diff, + 'integrate': sympy.integrate, + 'limit': sympy.limit, + 'series': sympy.series, + 'solve': sympy.solve, + 'simplify': sympy.simplify, + 'expand': sympy.expand, + 'factor': sympy.factor, + 'collect': sympy.collect, + 'cancel': sympy.cancel, + 'apart': sympy.apart, + 'together': sympy.together, + # Álgebra lineal + 'Matrix': sympy.Matrix, + 'det': lambda m: m.det() if hasattr(m, 'det') else sympy.det(m), + 'inv': lambda m: m.inv() if hasattr(m, 'inv') else sympy.Matrix(m).inv(), + # Plotting (será manejado por resultados interactivos) + 'plot': self._create_plot_placeholder, + 'plot3d': self._create_plot3d_placeholder, + } + + # Clases especializadas + specialized_classes = { + 'Hex': Hex, + 'Bin': Bin, + 'Dec': Dec, + 'IP4': IP4, + 'Chr': Chr, + # Alias en minúsculas + 'hex': Hex, + 'bin': Bin, + 'dec': Dec, + 'ip4': IP4, + 'chr': Chr, + } + + # Funciones de utilidad + utility_functions = { + '_add_equation': self._add_equation, + '_assign_variable': self._assign_variable, + 'help': self._help_function, + 'evalf': lambda expr, n=15: expr.evalf(n) if hasattr(expr, 'evalf') else float(expr), + } + + self.base_context = { + **math_functions, + **specialized_classes, + **utility_functions + } + + def _create_plot_placeholder(self, *args, **kwargs): + """Crear placeholder para plots que será manejado por resultados interactivos""" + return PlotResult('plot', args, kwargs) + + def _create_plot3d_placeholder(self, *args, **kwargs): + """Crear placeholder para plots 3D""" + return PlotResult('plot3d', args, kwargs) + + def _help_function(self, obj=None): + """Función de ayuda integrada""" + if obj is None: + return "Ayuda disponible. Use help(función) para ayuda específica." + + if hasattr(obj, '__doc__') and obj.__doc__: + return obj.__doc__ + elif hasattr(obj, 'Helper'): + return obj.Helper("") + else: + return f"No hay ayuda disponible para {obj}" + + def evaluate_line(self, line: str) -> 'EvaluationResult': + """ + Evalúa una línea de código y retorna el resultado + """ + try: + # 1. Parsear la línea + parsed_line, parse_info = self.parser.parse_line(line) + + if self.debug: + print(f"Parse: '{line}' → '{parsed_line}' ({parse_info})") + + # 2. Manejar casos especiales + if parse_info == "comment": + return EvaluationResult(None, "comment", original_line=line) + elif parse_info == "equation": + return self._evaluate_equation_addition(parsed_line, line) + elif parse_info == "assignment": + return self._evaluate_assignment(parsed_line, line) + + # 3. Evaluación SymPy + return self._evaluate_sympy_expression(parsed_line, parse_info, line) + + except Exception as e: + return EvaluationResult( + None, "error", + error=str(e), + original_line=line + ) + + def _evaluate_assignment(self, parsed_line: str, original_line: str) -> 'EvaluationResult': + """Maneja la asignación de variables""" + try: + # Ejecutar _assign_variable + result = self._eval_in_context(parsed_line) + + # Extraer nombre de variable y valor del resultado + parts = original_line.split('=', 1) + var_name = parts[0].strip() + + # Obtener el valor asignado + assigned_value = self.symbol_table.get(var_name) + + return EvaluationResult( + assigned_value, "assignment", + symbolic_result=result, + original_line=original_line + ) + except Exception as e: + return EvaluationResult( + None, "error", + error=f"Error en asignación: {e}", + original_line=original_line + ) + + def _evaluate_equation_addition(self, parsed_line: str, original_line: str) -> 'EvaluationResult': + """Maneja la adición de ecuaciones al sistema""" + try: + # Ejecutar _add_equation + result = self._eval_in_context(parsed_line) + return EvaluationResult( + result, "equation_added", + symbolic_result=f"Ecuación agregada: {original_line}", + original_line=original_line + ) + except Exception as e: + return EvaluationResult( + None, "error", + error=f"Error agregando ecuación: {e}", + original_line=original_line + ) + + def _evaluate_sympy_expression(self, expression: str, parse_info: str, original_line: str) -> 'EvaluationResult': + """Evalúa una expresión usando SymPy""" + try: + # Evaluar en contexto SymPy + result = self._eval_in_context(expression) + + # Actualizar last_result + self.last_result = result + + # Intentar evaluación numérica si es posible + numeric_result = None + if hasattr(result, 'evalf'): + try: + numeric_eval = result.evalf() + if numeric_eval != result: + numeric_result = numeric_eval + except: + pass + + return EvaluationResult( + result, "expression", + symbolic_result=result, + numeric_result=numeric_result, + parse_info=parse_info, + original_line=original_line + ) + + except NameError as e: + # Intentar crear símbolos automáticamente + return self._handle_undefined_symbols(expression, original_line, e) + except Exception as e: + return EvaluationResult( + None, "error", + error=str(e), + original_line=original_line + ) + + def _handle_undefined_symbols(self, expression: str, original_line: str, error: Exception) -> 'EvaluationResult': + """Maneja símbolos no definidos creándolos automáticamente""" + try: + # Extraer nombres de variables de la expresión + var_names = self._extract_variable_names(expression) + + # Crear símbolos automáticamente + new_symbols = {} + for name in var_names: + if name not in self.symbol_table and name not in self.base_context: + new_symbols[name] = Symbol(name) + self.symbol_table[name] = Symbol(name) + + if new_symbols: + # Reintentar evaluación + result = self._eval_in_context(expression) + + symbol_names = list(new_symbols.keys()) + info_msg = f"Símbolos creados: {', '.join(symbol_names)}" + + return EvaluationResult( + result, "symbolic_with_new_vars", + symbolic_result=result, + info=info_msg, + original_line=original_line + ) + else: + raise error + + except Exception as e: + return EvaluationResult( + None, "error", + error=str(e), + original_line=original_line + ) + + def _extract_variable_names(self, expression: str) -> List[str]: + """Extrae nombres de variables de una expresión""" + try: + # Usar SymPy para extraer símbolos + expr = sympify(expression, locals=self._get_full_context()) + return [str(symbol) for symbol in expr.free_symbols] + except: + # Fallback: usar regex + pattern = r'\b[a-zA-Z_][a-zA-Z0-9_]*\b' + names = re.findall(pattern, expression) + # Filtrar funciones conocidas + return [name for name in names if name not in self.base_context] + + def _eval_in_context(self, expression: str) -> Any: + """Evalúa una expresión en el contexto completo""" + context = self._get_full_context() + + # Casos especiales para funciones del sistema + if expression.strip().startswith('_add_equation'): + return eval(expression, {"__builtins__": {}}, context) + elif expression.strip().startswith('_assign_variable'): + return eval(expression, {"__builtins__": {}}, context) + else: + 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 isinstance(result, HybridCalcType): + 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 _get_full_context(self) -> Dict[str, Any]: + """Obtiene el contexto completo para evaluación""" + context = self.base_context.copy() + context.update(self.symbol_table) + context['last'] = self.last_result + return context + + def _assign_variable(self, var_name: str, expression) -> str: + """Asigna un valor a una variable""" + try: + # Evaluar la expresión + if isinstance(expression, str): + value = sympify(expression, locals=self._get_full_context()) + 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 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() + + +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 "" + + +class PlotResult: + """Placeholder para resultados de plotting""" + + def __init__(self, plot_type: str, args: tuple, kwargs: dict): + self.plot_type = plot_type + self.args = args + self.kwargs = kwargs + + def __str__(self): + return f"📊 Ver {self.plot_type.title()}" + + def __repr__(self): + return f"PlotResult('{self.plot_type}', {self.args}, {self.kwargs})" + + +# Funciones de testing +def test_evaluation_engine(): + """Test del motor de evaluación""" + engine = HybridEvaluationEngine() + engine.debug = True + + test_cases = [ + # Expresiones básicas + "2 + 3", + "x + 2", + "sin(pi/2)", + + # Sintaxis con corchetes + "Hex[FF]", + "IP4[192.168.1.1/24]", + + # Ecuaciones + "x + 2 = 5", + "y**2 = 16", + + # Solve + "solve(x + 2 - 5, x)", + + # Variables + "a = 10", + "b = a + 5", + + # Funciones avanzadas + "diff(x**2, x)", + "integrate(x**2, x)", + ] + + print("=== Test Motor de Evaluación ===") + for test in test_cases: + result = engine.evaluate_line(test) + print(f"'{test}' → {result} (type: {result.result_type})") + if result.info: + print(f" Info: {result.info}") + + +if __name__ == "__main__": + test_evaluation_engine() diff --git a/interactive_results.py b/interactive_results.py new file mode 100644 index 0000000..0c351f3 --- /dev/null +++ b/interactive_results.py @@ -0,0 +1,441 @@ +""" +Sistema de resultados interactivos con tags clickeables +""" +import tkinter as tk +from tkinter import Toplevel, scrolledtext +import sympy +from typing import Any, Optional, Dict, List +import matplotlib.pyplot as plt +from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg +import numpy as np + + +class InteractiveResultManager: + """Maneja resultados interactivos con ventanas emergentes""" + + def __init__(self, parent_window: tk.Tk): + self.parent = parent_window + self.open_windows: Dict[str, Toplevel] = {} + + def create_interactive_tag(self, result: Any, text_widget: tk.Text, index: str) -> Optional[str]: + """ + Crea un tag interactivo para un resultado si es necesario + + Returns: + Tag name si se creó, None si no es necesario + """ + tag_name = None + display_text = "" + + if isinstance(result, PlotResult): + tag_name = f"plot_{id(result)}" + display_text = f"📊 Ver {result.plot_type.title()}" + + elif isinstance(result, sympy.Matrix): + tag_name = f"matrix_{id(result)}" + rows, cols = result.shape + display_text = f"📋 Ver Matriz {rows}×{cols}" + + elif isinstance(result, list) and len(result) > 5: + tag_name = f"list_{id(result)}" + display_text = f"📋 Ver Lista ({len(result)} elementos)" + + elif isinstance(result, dict) and len(result) > 3: + tag_name = f"dict_{id(result)}" + display_text = f"🔍 Ver Diccionario ({len(result)} entradas)" + + elif hasattr(result, '__dict__') and len(str(result)) > 100: + tag_name = f"object_{id(result)}" + display_text = f"🔍 Ver Detalles ({type(result).__name__})" + + if tag_name: + # Configurar tag + text_widget.tag_configure( + tag_name, + foreground="#4fc3f7", + underline=True, + font=("Consolas", 11, "underline") + ) + + # Bind click event + text_widget.tag_bind( + tag_name, + "", + lambda e, r=result: self._handle_interactive_click(r) + ) + + text_widget.tag_bind( + tag_name, + "", + lambda e: text_widget.config(cursor="hand2") + ) + + text_widget.tag_bind( + tag_name, + "", + lambda e: text_widget.config(cursor="") + ) + + return tag_name, display_text + + return None, str(result) + + def _handle_interactive_click(self, result: Any): + """Maneja clicks en elementos interactivos""" + window_key = f"{type(result).__name__}_{id(result)}" + + # Si ya existe la ventana, enfocarla + if window_key in self.open_windows: + window = self.open_windows[window_key] + if window.winfo_exists(): + window.lift() + window.focus_set() + return + else: + del self.open_windows[window_key] + + # Crear nueva ventana + if isinstance(result, PlotResult): + self._show_plot_window(result, window_key) + elif isinstance(result, sympy.Matrix): + self._show_matrix_window(result, window_key) + elif isinstance(result, list): + self._show_list_window(result, window_key) + elif isinstance(result, dict): + self._show_dict_window(result, window_key) + else: + self._show_object_window(result, window_key) + + def _show_plot_window(self, plot_result: 'PlotResult', window_key: str): + """Muestra ventana con plot matplotlib""" + window = self._create_base_window(f"Plot - {plot_result.plot_type}", "800x600") + self.open_windows[window_key] = window + + 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) + elif plot_result.plot_type == "plot3d": + self._create_3d_plot(fig, plot_result.args, plot_result.kwargs) + + # Embed en tkinter + canvas = FigureCanvasTkAgg(fig, window) + canvas.draw() + canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True) + + except Exception as e: + error_label = tk.Label( + window, + text=f"Error generando plot: {e}", + fg="red", + bg="#2b2b2b" + ) + error_label.pack(pady=20) + + def _create_2d_plot(self, fig, ax, args, kwargs): + """Crea plot 2D usando SymPy""" + if len(args) >= 1: + expr = args[0] + + if len(args) >= 2: + # Rango especificado: (variable, start, end) + var_range = args[1] + if isinstance(var_range, tuple) and len(var_range) == 3: + var, start, end = var_range + x_vals = np.linspace(float(start), float(end), 1000) + + # Evaluar expresión + f = sympy.lambdify(var, expr, 'numpy') + y_vals = f(x_vals) + + ax.plot(x_vals, y_vals, **kwargs) + ax.set_xlabel(str(var)) + ax.set_ylabel(str(expr)) + ax.grid(True) + ax.set_title(f"Plot: {expr}") + else: + # Rango por defecto + free_symbols = list(expr.free_symbols) + if free_symbols: + var = free_symbols[0] + x_vals = np.linspace(-10, 10, 1000) + f = sympy.lambdify(var, expr, 'numpy') + y_vals = f(x_vals) + + ax.plot(x_vals, y_vals, **kwargs) + ax.set_xlabel(str(var)) + ax.set_ylabel(str(expr)) + ax.grid(True) + ax.set_title(f"Plot: {expr}") + + def _create_3d_plot(self, fig, args, kwargs): + """Crea plot 3D""" + ax = fig.add_subplot(111, projection='3d') + + if len(args) >= 3: + expr = args[0] + x_range = args[1] # (x, x_start, x_end) + y_range = args[2] # (y, y_start, y_end) + + if isinstance(x_range, tuple) and isinstance(y_range, tuple): + x_var, x_start, x_end = x_range + y_var, y_start, y_end = y_range + + x_vals = np.linspace(float(x_start), float(x_end), 50) + y_vals = np.linspace(float(y_start), float(y_end), 50) + X, Y = np.meshgrid(x_vals, y_vals) + + f = sympy.lambdify([x_var, y_var], expr, 'numpy') + Z = f(X, Y) + + ax.plot_surface(X, Y, Z, **kwargs) + ax.set_xlabel(str(x_var)) + ax.set_ylabel(str(y_var)) + ax.set_zlabel(str(expr)) + ax.set_title(f"3D Plot: {expr}") + + def _show_matrix_window(self, matrix: sympy.Matrix, window_key: str): + """Muestra ventana con matriz formateada""" + rows, cols = matrix.shape + window = self._create_base_window(f"Matriz {rows}×{cols}", "600x400") + self.open_windows[window_key] = window + + # Crear frame con scroll + frame = tk.Frame(window, bg="#2b2b2b") + frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) + + text_widget = scrolledtext.ScrolledText( + frame, + font=("Courier New", 12), + bg="#1e1e1e", + fg="#d4d4d4", + insertbackground="#ffffff", + wrap=tk.NONE + ) + text_widget.pack(fill=tk.BOTH, expand=True) + + # Formatear matriz + matrix_str = self._format_matrix(matrix) + text_widget.insert("1.0", matrix_str) + text_widget.config(state="disabled") + + # Botones de utilidad + button_frame = tk.Frame(window, bg="#2b2b2b") + button_frame.pack(fill=tk.X, padx=10, pady=5) + + det_btn = tk.Button( + button_frame, + text="Determinante", + command=lambda: self._show_matrix_property(matrix, "determinante", matrix.det()), + bg="#3c3c3c", + fg="white" + ) + det_btn.pack(side=tk.LEFT, padx=5) + + if matrix.is_square: + inv_btn = tk.Button( + button_frame, + text="Inversa", + command=lambda: self._show_matrix_property(matrix, "inversa", matrix.inv()), + bg="#3c3c3c", + fg="white" + ) + inv_btn.pack(side=tk.LEFT, padx=5) + + def _format_matrix(self, matrix: sympy.Matrix) -> str: + """Formatea una matriz para display""" + rows, cols = matrix.shape + + # Calcular ancho máximo de elementos + max_width = 0 + for i in range(rows): + for j in range(cols): + element_str = str(matrix[i, j]) + max_width = max(max_width, len(element_str)) + + # Construir representación + lines = [] + lines.append("┌" + " " * (max_width * cols + cols - 1) + "┐") + + for i in range(rows): + line = "│" + for j in range(cols): + element_str = str(matrix[i, j]) + padded = element_str.center(max_width) + line += padded + if j < cols - 1: + line += " " + line += "│" + lines.append(line) + + lines.append("└" + " " * (max_width * cols + cols - 1) + "┘") + + return "\n".join(lines) + + def _show_matrix_property(self, matrix: sympy.Matrix, prop_name: str, prop_value: Any): + """Muestra propiedad de matriz en ventana separada""" + prop_window = self._create_base_window(f"Matriz - {prop_name.title()}", "400x300") + + text_widget = scrolledtext.ScrolledText( + prop_window, + font=("Courier New", 12), + bg="#1e1e1e", + fg="#d4d4d4", + insertbackground="#ffffff" + ) + text_widget.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) + + if isinstance(prop_value, sympy.Matrix): + content = f"{prop_name.title()}:\n\n{self._format_matrix(prop_value)}" + else: + content = f"{prop_name.title()}: {prop_value}" + + text_widget.insert("1.0", content) + text_widget.config(state="disabled") + + def _show_list_window(self, lst: list, window_key: str): + """Muestra ventana con lista expandida""" + window = self._create_base_window(f"Lista ({len(lst)} elementos)", "500x400") + self.open_windows[window_key] = window + + text_widget = scrolledtext.ScrolledText( + window, + font=("Consolas", 11), + bg="#1e1e1e", + fg="#d4d4d4", + insertbackground="#ffffff" + ) + text_widget.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) + + content = "Elementos de la lista:\n\n" + for i, item in enumerate(lst): + content += f"[{i}] {item}\n" + + text_widget.insert("1.0", content) + text_widget.config(state="disabled") + + def _show_dict_window(self, dct: dict, window_key: str): + """Muestra ventana con diccionario expandido""" + window = self._create_base_window(f"Diccionario ({len(dct)} entradas)", "500x400") + self.open_windows[window_key] = window + + text_widget = scrolledtext.ScrolledText( + window, + font=("Consolas", 11), + bg="#1e1e1e", + fg="#d4d4d4", + insertbackground="#ffffff" + ) + text_widget.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) + + content = "Entradas del diccionario:\n\n" + for key, value in dct.items(): + content += f"{key}: {value}\n" + + text_widget.insert("1.0", content) + text_widget.config(state="disabled") + + def _show_object_window(self, obj: Any, window_key: str): + """Muestra ventana con detalles de objeto""" + window = self._create_base_window(f"Objeto - {type(obj).__name__}", "600x500") + self.open_windows[window_key] = window + + text_widget = scrolledtext.ScrolledText( + window, + font=("Consolas", 11), + bg="#1e1e1e", + fg="#d4d4d4", + insertbackground="#ffffff" + ) + text_widget.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) + + content = f"Objeto: {type(obj).__name__}\n\n" + content += f"Valor: {obj}\n\n" + content += f"Representación: {repr(obj)}\n\n" + + if hasattr(obj, '__dict__'): + content += "Atributos:\n" + for attr, value in obj.__dict__.items(): + content += f" {attr}: {value}\n" + + content += "\nMétodos disponibles:\n" + for attr in dir(obj): + if not attr.startswith('_') and callable(getattr(obj, attr, None)): + content += f" {attr}()\n" + + text_widget.insert("1.0", content) + text_widget.config(state="disabled") + + def _create_base_window(self, title: str, geometry: str = "500x400") -> Toplevel: + """Crea ventana base con estilo consistente""" + window = Toplevel(self.parent) + window.title(title) + window.geometry(geometry) + window.configure(bg="#2b2b2b") + window.transient(self.parent) + + # Centrar ventana + window.update_idletasks() + x = (window.winfo_screenwidth() // 2) - (window.winfo_width() // 2) + y = (window.winfo_screenheight() // 2) - (window.winfo_height() // 2) + window.geometry(f"+{x}+{y}") + + return window + + def close_all_windows(self): + """Cierra todas las ventanas interactivas""" + for window in self.open_windows.values(): + if window.winfo_exists(): + window.destroy() + self.open_windows.clear() + + +# Importar PlotResult desde el motor de evaluación +class PlotResult: + """Placeholder para resultados de plotting""" + + def __init__(self, plot_type: str, args: tuple, kwargs: dict): + self.plot_type = plot_type + self.args = args + self.kwargs = kwargs + + def __str__(self): + return f"📊 Ver {self.plot_type.title()}" + + def __repr__(self): + return f"PlotResult('{self.plot_type}', {self.args}, {self.kwargs})" + + +# Función de testing +def test_interactive_results(): + """Test del sistema de resultados interactivos""" + root = tk.Tk() + root.title("Test Interactive Results") + + manager = InteractiveResultManager(root) + + # Crear widget de texto de prueba + text_widget = tk.Text(root, height=20, width=80) + text_widget.pack(padx=10, pady=10) + + # Test con matriz + matrix = sympy.Matrix([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) + tag, display = manager.create_interactive_tag(matrix, text_widget, "1.0") + text_widget.insert("end", f"Matriz test: {display}\n", tag) + + # Test con lista + long_list = list(range(20)) + tag, display = manager.create_interactive_tag(long_list, text_widget, "2.0") + text_widget.insert("end", f"Lista test: {display}\n", tag) + + # Test con plot + plot_result = PlotResult("plot", (sympy.sin(sympy.Symbol('x')), (sympy.Symbol('x'), -10, 10)), {}) + tag, display = manager.create_interactive_tag(plot_result, text_widget, "3.0") + text_widget.insert("end", f"Plot test: {display}\n", tag) + + root.mainloop() + + +if __name__ == "__main__": + test_interactive_results() diff --git a/main_launcher.py b/main_launcher.py new file mode 100644 index 0000000..5da6b05 --- /dev/null +++ b/main_launcher.py @@ -0,0 +1,708 @@ +#!/usr/bin/env python3 +""" +Launcher principal para Calculadora MAV - CAS Híbrido +Este script maneja la inicialización y ejecución de la aplicación +""" +import sys +import os +import subprocess +import tkinter as tk +from tkinter import messagebox +from pathlib import Path +import importlib.util +import logging +import datetime +import traceback +import platform + + +import platform + + +def setup_logging(): + """Configura el sistema de logging completo""" + log_dir = Path("logs") + log_dir.mkdir(exist_ok=True) + + # Archivo de log con timestamp + timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") + log_file = log_dir / f"mav_calc_{timestamp}.log" + + # Configurar logging + logging.basicConfig( + level=logging.DEBUG, + format='%(asctime)s - %(levelname)s - %(funcName)s:%(lineno)d - %(message)s', + handlers=[ + logging.FileHandler(log_file, encoding='utf-8'), + logging.StreamHandler(sys.stdout) + ] + ) + + logger = logging.getLogger(__name__) + + # Log información del sistema + logger.info("=" * 60) + logger.info("CALCULADORA MAV - CAS HÍBRIDO - INICIO DE SESIÓN") + logger.info("=" * 60) + logger.info(f"Timestamp: {datetime.datetime.now()}") + logger.info(f"Python: {sys.version}") + logger.info(f"Plataforma: {platform.platform()}") + logger.info(f"Directorio de trabajo: {Path.cwd()}") + logger.info(f"Argumentos: {sys.argv}") + logger.info(f"Archivo de log: {log_file}") + logger.info("-" * 60) + + return logger, log_file + + +def log_system_info(logger): + """Registra información detallada del sistema""" + try: + logger.info("INFORMACIÓN DEL SISTEMA:") + logger.info(f" OS: {platform.system()} {platform.release()}") + logger.info(f" Arquitectura: {platform.machine()}") + logger.info(f" Procesador: {platform.processor()}") + logger.info(f" Python executable: {sys.executable}") + logger.info(f" Python path: {sys.path[:3]}...") # Solo primeros 3 elementos + + # Información de memoria si está disponible + try: + import psutil + memory = psutil.virtual_memory() + logger.info(f" RAM total: {memory.total // (1024**3)} GB") + logger.info(f" RAM disponible: {memory.available // (1024**3)} GB") + except ImportError: + logger.info(" Información de memoria: No disponible (psutil no instalado)") + + except Exception as e: + logger.error(f"Error obteniendo información del sistema: {e}") + + +def log_error_with_context(logger, error, context=""): + """Registra un error con contexto completo""" + logger.error("=" * 50) + logger.error("ERROR DETECTADO") + logger.error("=" * 50) + if context: + logger.error(f"Contexto: {context}") + logger.error(f"Tipo de error: {type(error).__name__}") + logger.error(f"Mensaje: {str(error)}") + logger.error("Traceback completo:") + logger.error(traceback.format_exc()) + logger.error("Variables locales en el momento del error:") + + # Intentar capturar variables locales del frame donde ocurrió el error + try: + tb = traceback.extract_tb(error.__traceback__) + if tb: + last_frame = tb[-1] + logger.error(f" Archivo: {last_frame.filename}") + logger.error(f" Línea: {last_frame.lineno}") + logger.error(f" Función: {last_frame.name}") + logger.error(f" Código: {last_frame.line}") + except Exception as frame_error: + logger.error(f"No se pudieron obtener detalles del frame: {frame_error}") + + logger.error("=" * 50) + + +def show_error_with_log_info(error, log_file, context=""): + """Muestra error al usuario con información del log""" + error_msg = f"""Error en Calculadora MAV: + +{context} + +Error: {type(error).__name__}: {str(error)} + +INFORMACIÓN DE DEBUGGING: +• Log completo guardado en: {log_file} +• Para soporte, enviar el archivo de log +• Timestamp: {datetime.datetime.now()} + +¿Qué hacer ahora? +1. Revisar el archivo de log para más detalles +2. Intentar reiniciar la aplicación +3. Verificar dependencias con: python launcher.py --setup +4. Ejecutar tests con: python launcher.py --test +""" + + try: + root = tk.Tk() + root.withdraw() + + # Crear ventana de error personalizada + error_window = tk.Toplevel(root) + error_window.title("Error - Calculadora MAV") + error_window.geometry("600x400") + error_window.configure(bg="#2b2b2b") + + # Hacer la ventana modal + error_window.transient(root) + error_window.grab_set() + + # Centrar ventana + error_window.update_idletasks() + x = (error_window.winfo_screenwidth() // 2) - (error_window.winfo_width() // 2) + y = (error_window.winfo_screenheight() // 2) - (error_window.winfo_height() // 2) + error_window.geometry(f"+{x}+{y}") + + # Contenido + from tkinter import scrolledtext + + text_widget = scrolledtext.ScrolledText( + error_window, + font=("Consolas", 10), + bg="#1e1e1e", + fg="#ff6b6b", + wrap=tk.WORD + ) + text_widget.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) + text_widget.insert("1.0", error_msg) + text_widget.config(state="disabled") + + # Botones + button_frame = tk.Frame(error_window, bg="#2b2b2b") + button_frame.pack(fill=tk.X, padx=10, pady=5) + + def open_log_folder(): + try: + if platform.system() == "Windows": + os.startfile(log_file.parent) + elif platform.system() == "Darwin": # macOS + subprocess.run(["open", str(log_file.parent)]) + else: # Linux + subprocess.run(["xdg-open", str(log_file.parent)]) + except Exception: + pass + + def copy_log_path(): + error_window.clipboard_clear() + error_window.clipboard_append(str(log_file)) + + tk.Button( + button_frame, + text="Abrir Carpeta de Logs", + command=open_log_folder, + bg="#4fc3f7", + fg="white" + ).pack(side=tk.LEFT, padx=5) + + tk.Button( + button_frame, + text="Copiar Ruta del Log", + command=copy_log_path, + bg="#82aaff", + fg="white" + ).pack(side=tk.LEFT, padx=5) + + tk.Button( + button_frame, + text="Cerrar", + command=error_window.destroy, + bg="#ff6b6b", + fg="white" + ).pack(side=tk.RIGHT, padx=5) + + # Esperar a que se cierre la ventana + error_window.wait_window() + root.destroy() + + except Exception as gui_error: + # Si falla la GUI, mostrar en consola + print("ERROR: No se pudo mostrar ventana de error") + print(error_msg) + print(f"Error adicional: {gui_error}") + + +# Variable global para el logger +logger = None +log_file = None + + +def check_dependencies(): + """Verifica que todas las dependencias estén disponibles""" + logger.info("Verificando dependencias...") + + required_modules = { + 'sympy': 'SymPy (motor algebraico)', + 'matplotlib': 'Matplotlib (plotting)', + 'numpy': 'NumPy (cálculos numéricos)', + 'tkinter': 'Tkinter (interfaz gráfica)' + } + + missing_modules = [] + + for module, description in required_modules.items(): + try: + if module == 'tkinter': + import tkinter + logger.info(f"✅ {module} - {description}") + else: + importlib.import_module(module) + logger.info(f"✅ {module} - {description}") + except ImportError as e: + missing_modules.append((module, description)) + logger.error(f"❌ {module} - {description} - Error: {e}") + + if missing_modules: + logger.warning(f"Módulos faltantes: {[m[0] for m in missing_modules]}") + else: + logger.info("Todas las dependencias están disponibles") + + return missing_modules + + +def install_missing_dependencies(missing_modules): + """Intenta instalar dependencias faltantes""" + logger.info("Intentando instalar dependencias faltantes...") + + installable_modules = [ + ('sympy', 'sympy>=1.12'), + ('matplotlib', 'matplotlib>=3.7.0'), + ('numpy', 'numpy>=1.24.0') + ] + + modules_to_install = [] + for module_name, _ in missing_modules: + for inst_name, inst_package in installable_modules: + if module_name == inst_name: + modules_to_install.append(inst_package) + break + + if not modules_to_install: + logger.info("No hay módulos para instalar automáticamente") + return True + + logger.info(f"Instalando: {', '.join(modules_to_install)}") + + try: + cmd = [sys.executable, "-m", "pip", "install"] + modules_to_install + logger.info(f"Ejecutando comando: {' '.join(cmd)}") + + result = subprocess.run( + cmd, + capture_output=True, + text=True, + timeout=300 # 5 minutos timeout + ) + + logger.info(f"Código de salida pip: {result.returncode}") + logger.info(f"Stdout pip: {result.stdout}") + + if result.stderr: + logger.warning(f"Stderr pip: {result.stderr}") + + return result.returncode == 0 + + except subprocess.TimeoutExpired: + logger.error("Timeout durante instalación de dependencias") + return False + except Exception as e: + logger.error(f"Error durante instalación: {e}") + log_error_with_context(logger, e, "Instalación de dependencias") + return False + + +def show_dependency_error(missing_modules): + """Muestra error de dependencias faltantes""" + error_msg = "Dependencias faltantes:\n\n" + for module, description in missing_modules: + error_msg += f"• {module}: {description}\n" + + error_msg += "\nPara instalar dependencias:\n" + error_msg += "pip install sympy matplotlib numpy\n\n" + + if any(module == 'tkinter' for module, _ in missing_modules): + error_msg += "Para tkinter en Linux:\n" + error_msg += "sudo apt-get install python3-tk\n" + + error_msg += "\nO ejecute: python launcher.py --setup" + + logger.error("DEPENDENCIAS FALTANTES:") + for module, description in missing_modules: + logger.error(f" • {module}: {description}") + + show_error_with_log_info( + Exception("Dependencias faltantes"), + log_file, + "Faltan dependencias requeridas para ejecutar la aplicación" + ) + + +def check_app_files(): + """Verifica que todos los archivos de la aplicación estén presentes""" + logger.info("Verificando archivos de la aplicación...") + + required_files = [ + 'bracket_parser.py', + 'hybrid_base_types.py', + 'hybrid_evaluation_engine.py', + 'interactive_results.py', + 'hybrid_calc_app.py' + ] + + current_dir = Path(__file__).parent + missing_files = [] + + for filename in required_files: + file_path = current_dir / filename + if file_path.exists(): + logger.info(f"✅ {filename} - Tamaño: {file_path.stat().st_size} bytes") + else: + missing_files.append(filename) + logger.error(f"❌ {filename} - No encontrado") + + if missing_files: + logger.error(f"Archivos faltantes: {missing_files}") + else: + logger.info("Todos los archivos de la aplicación están presentes") + + return missing_files + + +def show_file_error(missing_files): + """Muestra error de archivos faltantes""" + error_msg = "Archivos de aplicación faltantes:\n\n" + for filename in missing_files: + error_msg += f"• {filename}\n" + + error_msg += "\nAsegúrese de tener todos los archivos del proyecto." + + logger.error("ARCHIVOS FALTANTES:") + for filename in missing_files: + logger.error(f" • {filename}") + + show_error_with_log_info( + Exception("Archivos faltantes"), + log_file, + "Faltan archivos necesarios para la aplicación" + ) + + +def launch_application(): + """Lanza la aplicación principal""" + logger.info("Iniciando aplicación principal...") + + try: + # Importar y ejecutar la aplicación + logger.info("Importando módulo principal...") + from hybrid_calc_app import HybridCalculatorApp + + logger.info("Creando ventana principal...") + root = tk.Tk() + + logger.info("Inicializando aplicación...") + app = HybridCalculatorApp(root) + + logger.info("✅ Calculadora MAV - CAS Híbrido iniciada correctamente") + logger.info("Iniciando loop principal de tkinter...") + + root.mainloop() + + logger.info("Aplicación cerrada normalmente") + + except ImportError as e: + logger.error(f"Error de importación: {e}") + log_error_with_context(logger, e, "Importación de módulos de la aplicación") + show_error_with_log_info(e, log_file, "Error importando módulos de la aplicación") + except Exception as e: + logger.error(f"Error durante ejecución de la aplicación: {e}") + log_error_with_context(logger, e, "Ejecución de la aplicación principal") + show_error_with_log_info(e, log_file, "Error durante la ejecución de la aplicación") + + +def show_startup_splash(): + """Muestra splash screen durante la carga""" + if not logger: + return # Skip splash si no hay logger + + try: + logger.info("Mostrando splash screen...") + + splash = tk.Tk() + splash.title("Calculadora MAV") + splash.geometry("400x200") + splash.configure(bg="#2b2b2b") + splash.resizable(False, False) + + # Centrar ventana + splash.update_idletasks() + x = (splash.winfo_screenwidth() // 2) - (splash.winfo_width() // 2) + y = (splash.winfo_screenheight() // 2) - (splash.winfo_height() // 2) + splash.geometry(f"+{x}+{y}") + + # Contenido del splash + title_label = tk.Label( + splash, + text="Calculadora MAV", + font=("Arial", 20, "bold"), + fg="#ffffff", + bg="#2b2b2b" + ) + title_label.pack(pady=20) + + subtitle_label = tk.Label( + splash, + text="CAS Híbrido", + font=("Arial", 14), + fg="#82aaff", + bg="#2b2b2b" + ) + subtitle_label.pack() + + status_label = tk.Label( + splash, + text="Cargando componentes...", + font=("Arial", 10), + fg="#c8c8c8", + bg="#2b2b2b" + ) + status_label.pack(pady=20) + + # Barra de progreso simple + progress_frame = tk.Frame(splash, bg="#2b2b2b") + progress_frame.pack(pady=10) + + progress_bar = tk.Canvas( + progress_frame, + width=300, + height=10, + bg="#1e1e1e", + highlightthickness=0 + ) + progress_bar.pack() + + # Animar barra de progreso + def animate_progress(): + for i in range(0, 301, 10): + progress_bar.delete("all") + progress_bar.create_rectangle( + 0, 0, i, 10, + fill="#4fc3f7", + outline="" + ) + splash.update() + splash.after(50) + + splash.after(100, animate_progress) + splash.after(2000, splash.destroy) + + logger.info("Splash screen mostrado correctamente") + splash.mainloop() + + except Exception as e: + # Si hay error con splash, continuar sin él + logger.warning(f"Error mostrando splash screen: {e}") + pass + + +def main(): + """Función principal del launcher""" + global logger, log_file + + # Configurar logging al inicio + try: + logger, log_file = setup_logging() + log_system_info(logger) + except Exception as e: + print(f"ERROR: No se pudo configurar logging: {e}") + # Continuar sin logging si falla + logger = None + log_file = None + + logger.info("Iniciando verificaciones del sistema...") + + try: + # Verificar archivos de aplicación + logger.info("Verificando archivos de aplicación...") + missing_files = check_app_files() + if missing_files: + logger.error("❌ Archivos faltantes detectados") + show_file_error(missing_files) + logger.info("Cerrando debido a archivos faltantes") + sys.exit(1) + + logger.info("✅ Todos los archivos están presentes") + + # Verificar dependencias + logger.info("Verificando dependencias...") + missing_deps = check_dependencies() + if missing_deps: + logger.warning("❌ Dependencias faltantes detectadas") + + # Intentar instalación automática para módulos pip + installable_deps = [ + (module, desc) for module, desc in missing_deps + if module in ['sympy', 'matplotlib', 'numpy'] + ] + + if installable_deps: + logger.info("Preguntando al usuario sobre instalación automática...") + response = input("¿Instalar dependencias automáticamente? (s/n): ").lower().strip() + logger.info(f"Respuesta del usuario: {response}") + + if response in ['s', 'si', 'y', 'yes']: + logger.info("Iniciando instalación automática...") + if install_missing_dependencies(installable_deps): + logger.info("✅ Dependencias instaladas correctamente") + # Re-verificar después de instalación + missing_deps = check_dependencies() + else: + logger.error("❌ Error durante instalación automática") + else: + logger.info("Usuario declinó instalación automática") + + if missing_deps: + logger.error("Dependencias aún faltantes, mostrando error al usuario") + show_dependency_error(missing_deps) + logger.info("Cerrando debido a dependencias faltantes") + sys.exit(1) + + logger.info("✅ Todas las dependencias disponibles") + + # Mostrar splash screen + if "--no-splash" not in sys.argv: + logger.info("Mostrando splash screen...") + show_startup_splash() + else: + logger.info("Splash screen omitido por argumento --no-splash") + + # Lanzar aplicación + logger.info("Lanzando aplicación principal...") + launch_application() + + logger.info("Aplicación cerrada - fin de sesión") + logger.info("=" * 60) + + except KeyboardInterrupt: + logger.info("Aplicación interrumpida por el usuario (Ctrl+C)") + sys.exit(0) + except Exception as e: + logger.error("Error crítico en main():") + log_error_with_context(logger, e, "Función principal del launcher") + show_error_with_log_info(e, log_file, "Error crítico durante el inicio") + sys.exit(1) + + +def show_help(): + """Muestra ayuda del launcher""" + help_text = """ +Calculadora MAV - CAS Híbrido Launcher + +Uso: python launcher.py [opciones] + +Opciones: + --help Muestra esta ayuda + --no-splash Inicia sin splash screen + --test Ejecuta tests antes de iniciar + --setup Ejecuta setup de dependencias + --debug Activa logging detallado + +Descripción: + Sistema de álgebra computacional híbrido que combina + SymPy con clases especializadas para networking, + programación y cálculos numéricos. + +Dependencias: + - Python 3.8+ + - SymPy (motor algebraico) + - Matplotlib (plotting) + - NumPy (cálculos numéricos) + - Tkinter (interfaz gráfica) + +Logging: + - Los logs se guardan automáticamente en: ./logs/ + - Cada ejecución genera un archivo con timestamp + - En caso de error, se muestra la ubicación del log + - Los logs incluyen información del sistema y debugging + +Resolución de problemas: + 1. Revisar logs en ./logs/ para errores detallados + 2. Ejecutar: python launcher.py --test + 3. Verificar dependencias: python launcher.py --setup + 4. Modo debug: python launcher.py --debug + +Para más información, consulte la documentación. +""" + print(help_text) + + +if __name__ == "__main__": + # Configurar logging básico para manejo de argumentos + temp_logger = None + temp_log_file = None + + try: + temp_logger, temp_log_file = setup_logging() + temp_logger.info("Procesando argumentos de línea de comandos...") + except: + pass # Si falla el logging, continuar sin él + + # Manejar argumentos de línea de comandos + if "--help" in sys.argv or "-h" in sys.argv: + if temp_logger: + temp_logger.info("Mostrando ayuda") + show_help() + sys.exit(0) + + if "--test" in sys.argv: + if temp_logger: + temp_logger.info("Ejecutando tests...") + try: + from test_suite import run_all_tests + if not run_all_tests(): + error_msg = "❌ Tests fallaron - no se iniciará la aplicación" + print(error_msg) + if temp_logger: + temp_logger.error(error_msg) + sys.exit(1) + success_msg = "✅ Tests pasaron - iniciando aplicación" + print(success_msg) + if temp_logger: + temp_logger.info(success_msg) + except ImportError as e: + warning_msg = "⚠️ Tests no disponibles - continuando con inicio" + print(warning_msg) + if temp_logger: + temp_logger.warning(f"{warning_msg} - Error: {e}") + except Exception as e: + if temp_logger: + log_error_with_context(temp_logger, e, "Ejecución de tests") + print(f"❌ Error ejecutando tests: {e}") + sys.exit(1) + + if "--setup" in sys.argv: + if temp_logger: + temp_logger.info("Ejecutando setup...") + try: + import setup_script + setup_script.main() + sys.exit(0) + except ImportError: + error_msg = "❌ Setup script no disponible" + print(error_msg) + if temp_logger: + temp_logger.error(error_msg) + sys.exit(1) + except Exception as e: + if temp_logger: + log_error_with_context(temp_logger, e, "Ejecución de setup") + print(f"❌ Error en setup: {e}") + sys.exit(1) + + # Logging adicional para debug + if "--debug" in sys.argv: + if temp_logger: + temp_logger.setLevel(logging.DEBUG) + temp_logger.info("Modo debug activado") + + # Inicio normal + try: + main() + except Exception as e: + if temp_logger: + log_error_with_context(temp_logger, e, "Error crítico en __main__") + print(f"ERROR CRÍTICO: {e}") + if temp_log_file: + print(f"Ver log completo en: {temp_log_file}") + sys.exit(1) diff --git a/quick_start_readme.md b/quick_start_readme.md new file mode 100644 index 0000000..af495a2 --- /dev/null +++ b/quick_start_readme.md @@ -0,0 +1,200 @@ +# Calculadora MAV - CAS Híbrido + +Sistema de Álgebra Computacional híbrido que combina SymPy con clases especializadas. + +## 🚀 Inicio Rápido + +### Instalación Automática +```bash +python launcher.py --setup +``` + +### Instalación Manual +```bash +pip install sympy matplotlib numpy +python launcher.py +``` + +### En Linux (para tkinter) +```bash +sudo apt-get install python3-tk +``` + +## ✨ Características Principales + +- **🧮 Motor SymPy completo**: Cálculo simbólico avanzado +- **🔧 Sintaxis simplificada**: `IP4[192.168.1.1/24]` en lugar de `IP4("192.168.1.1/24")` +- **📐 Ecuaciones automáticas**: `x + 2 = 5` detectado automáticamente +- **📊 Resultados interactivos**: Plots y matrices clickeables +- **🌐 Clases especializadas**: IP4, Hex, Bin, Date, Dec, Chr + +## 📝 Ejemplos Básicos + +### Clases Especializadas +```python +# Redes +IP4[192.168.1.100/24].NetworkAddress[] # 192.168.1.0/24 +IP4[10.0.0.1/8].Nodes() # 16777214 hosts + +# Números +Hex[FF] + 1 # 0x100 +Bin[1010] * 2 # 0b10100 + +# Caracteres +Chr[A] # 'A' (ASCII 65) +``` + +### Matemáticas Simbólicas +```python +# Variables automáticas +x + 2*y # Expresión simbólica + +# Cálculo +diff(x**2 + sin(x), x) # 2*x + cos(x) +integrate(x**2, x) # x**3/3 + +# Ecuaciones (detección automática) +x**2 + 2*x - 8 = 0 # Agregada al sistema +solve(x**2 + 2*x - 8, x) # [-4, 2] +x=? # Atajo para solve(x) +``` + +### Plotting Interactivo +```python +plot(sin(x), (x, -2*pi, 2*pi)) # 📊 Ver Plot (clickeable) +Matrix([[1, 2], [3, 4]]) # 📋 Ver Matriz (clickeable) +``` + +## 🎯 Casos de Uso + +### Networking +```python +# Análisis de red +network = IP4[192.168.0.0/24] +network.Nodes() # 254 +network.BroadcastAddress[] # 192.168.0.255/24 + +# Cálculo con variables +base = IP4[10.0.x.0/24] +solve(base.Nodes() == 254, x) # Encuentra x +``` + +### Programación +```python +# Conversiones entre bases +Hex[255].toDecimal() # 255 +Dec[66].toChr() # Chr('B') + +# Análisis ASCII +Chr[Hello].value # [72, 101, 108, 108, 111] +``` + +### Matemáticas Avanzadas +```python +# Sistema de ecuaciones +x + y = 10 +x - y = 2 +solve([x + y - 10, x - y - 2], [x, y]) # {x: 6, y: 4} + +# Análisis completo +f = sin(x) * exp(-x**2) +diff(f, x) # Derivada +integrate(f, (x, -oo, oo)) # Integral impropia +series(f, x, 0, 5) # Serie de Taylor +``` + +## 🖥️ Interfaz + +### Paneles +- **Izquierda**: Editor de código con sintaxis nueva +- **Derecha**: Resultados coloreados e interactivos + +### Menús +- **Archivo**: Nuevo, Cargar, Guardar +- **CAS**: Variables, Ecuaciones, Resolver sistema +- **Ayuda**: Guías y documentación completa + +### Resultados Clickeables +- **📊 Ver Plot**: Abre ventana matplotlib +- **📋 Ver Matriz**: Vista expandida con operaciones +- **📋 Ver Lista**: Contenido completo de listas largas + +## 🔧 Archivos del Proyecto + +``` +calculadora-mav-cas/ +├── launcher.py # 🚀 Inicio principal +├── setup.py # 🛠️ Instalación +├── test_suite.py # 🧪 Tests +├── bracket_parser.py # 📝 Parser sintaxis +├── hybrid_base_types.py # 🏗️ Clases especializadas +├── hybrid_evaluation_engine.py # 🧮 Motor CAS +├── interactive_results.py # 📊 Resultados clickeables +├── hybrid_calc_app.py # 🖥️ Interfaz gráfica +└── requirements.txt # 📦 Dependencias +``` + +## 🆘 Resolución de Problemas + +### Errores Comunes +```bash +# Dependencias faltantes +pip install sympy matplotlib numpy + +# Linux: tkinter faltante +sudo apt-get install python3-tk + +# Verificar instalación +python test_suite.py +``` + +### Sintaxis Correcta +```python +# ✅ Correcto (nueva sintaxis) +IP4[192.168.1.1/24] +Hex[FF] + +# ❌ Incorrecto (sintaxis antigua) +IP4("192.168.1.1/24") +Hex("FF") +``` + +## 📚 Documentación Completa + +Ver `comprehensive_documentation.md` para: +- Guía completa de sintaxis +- Casos de uso avanzados +- API de desarrollo +- Ejemplos detallados + +## 🧪 Testing + +```bash +# Tests básicos +python test_suite.py + +# Tests con verbosidad +python test_suite.py --verbose + +# Setup con tests +python launcher.py --test +``` + +## 🚀 Ejecutar + +```bash +# Método recomendado +python launcher.py + +# Sin splash screen +python launcher.py --no-splash + +# Con verificación +python launcher.py --test +``` + +--- + +**¡Disfruta del poder del CAS híbrido!** 🎉 + +*Para soporte y documentación completa, consulta los archivos de documentación incluidos.* \ No newline at end of file diff --git a/requirements_and_setup.py b/requirements_and_setup.py new file mode 100644 index 0000000..150af77 --- /dev/null +++ b/requirements_and_setup.py @@ -0,0 +1,27 @@ +# requirements.txt +# Calculadora MAV - CAS Híbrido +# Dependencias requeridas + +# Motor algebraico principal +sympy>=1.12 + +# Interfaz gráfica (generalmente incluido con Python) +# tkinter - incluido con Python estándar + +# Plotting y visualización +matplotlib>=3.7.0 +numpy>=1.24.0 + +# Opcional: Para ayuda mejorada con Markdown +markdown>=3.4.0 + +# Opcional: Para visor HTML en ayuda +# tkinterweb>=3.24.0 # Descomenta si quieres soporte HTML completo +# tkhtmlview>=0.2.0 # Alternativa para HTML + +# Testing (opcional) +pytest>=7.0.0 + +# Documentación (opcional) +# sphinx>=7.0.0 +# sphinx-rtd-theme>=1.3.0 diff --git a/setup_script.py b/setup_script.py new file mode 100644 index 0000000..63736c3 --- /dev/null +++ b/setup_script.py @@ -0,0 +1,245 @@ +#!/usr/bin/env python3 +""" +Script de setup e instalación para Calculadora MAV - CAS Híbrido +""" +import sys +import subprocess +import os +from pathlib import Path +import importlib.util + + +def check_python_version(): + """Verifica que la versión de Python sea compatible""" + if sys.version_info < (3, 8): + print("❌ Error: Se requiere Python 3.8 o superior") + print(f" Versión actual: {sys.version}") + return False + + print(f"✅ Python {sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}") + return True + + +def check_module(module_name, package_name=None, optional=False): + """Verifica si un módulo está disponible""" + try: + importlib.import_module(module_name) + print(f"✅ {module_name}") + return True + except ImportError: + if optional: + print(f"⚠️ {module_name} (opcional)") + else: + print(f"❌ {module_name} - {'usar: pip install ' + (package_name or module_name)}") + return False + + +def install_requirements(): + """Instala las dependencias requeridas""" + print("\n=== Instalando dependencias ===") + + requirements = [ + "sympy>=1.12", + "matplotlib>=3.7.0", + "numpy>=1.24.0" + ] + + for req in requirements: + try: + print(f"Instalando {req}...") + subprocess.check_call([ + sys.executable, "-m", "pip", "install", req + ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + print(f"✅ {req}") + except subprocess.CalledProcessError: + print(f"❌ Error instalando {req}") + return False + + return True + + +def check_tkinter(): + """Verifica que tkinter esté disponible""" + try: + import tkinter + print("✅ tkinter") + return True + except ImportError: + print("❌ tkinter - instalar python3-tk en Linux/Ubuntu") + print(" Ubuntu/Debian: sudo apt-get install python3-tk") + print(" CentOS/RHEL: sudo yum install tkinter") + print(" macOS: tkinter debe estar incluido con Python") + return False + + +def check_optional_modules(): + """Verifica módulos opcionales""" + optional_modules = [ + ("markdown", "markdown", True), + ("tkinterweb", "tkinterweb", True), + ("tkhtmlview", "tkhtmlview", True), + ("pytest", "pytest", True) + ] + + for module_name, package_name, optional in optional_modules: + check_module(module_name, package_name, optional) + + +def create_desktop_shortcut(): + """Crea acceso directo en el escritorio (Linux/Windows)""" + current_dir = Path.cwd() + main_script = current_dir / "hybrid_calc_app.py" + + if not main_script.exists(): + print("⚠️ No se encontró hybrid_calc_app.py en el directorio actual") + return False + + try: + if sys.platform.startswith('linux'): + # Linux desktop entry + desktop_dir = Path.home() / "Desktop" + if desktop_dir.exists(): + shortcut_path = desktop_dir / "CalculadoraMAV.desktop" + + shortcut_content = f"""[Desktop Entry] +Version=1.0 +Type=Application +Name=Calculadora MAV - CAS Híbrido +Comment=Sistema de álgebra computacional híbrido +Exec={sys.executable} "{main_script}" +Icon=calculator +Terminal=false +Categories=Education;Science;Math; +""" + + with open(shortcut_path, 'w') as f: + f.write(shortcut_content) + + # Hacer ejecutable + os.chmod(shortcut_path, 0o755) + print(f"✅ Acceso directo creado: {shortcut_path}") + return True + + elif sys.platform == 'win32': + # Windows shortcut (requiere pywin32) + try: + import win32com.client + + desktop = Path.home() / "Desktop" + shortcut_path = desktop / "Calculadora MAV.lnk" + + shell = win32com.client.Dispatch("WScript.Shell") + shortcut = shell.CreateShortCut(str(shortcut_path)) + shortcut.Targetpath = sys.executable + shortcut.Arguments = f'"{main_script}"' + shortcut.WorkingDirectory = str(current_dir) + shortcut.IconLocation = sys.executable + shortcut.save() + + print(f"✅ Acceso directo creado: {shortcut_path}") + return True + + except ImportError: + print("⚠️ Para crear acceso directo en Windows instalar: pip install pywin32") + + except Exception as e: + print(f"⚠️ Error creando acceso directo: {e}") + + return False + + +def run_tests(): + """Ejecuta tests básicos""" + print("\n=== Ejecutando tests básicos ===") + + try: + # Test de importación + from bracket_parser import BracketParser + from hybrid_base_types import Hex, Bin, IP4 + from hybrid_evaluation_engine import HybridEvaluationEngine + + print("✅ Importaciones básicas") + + # Test de parser + parser = BracketParser() + result, info = parser.parse_line("Hex[FF]") + assert result == 'Hex("FF")', f"Parser test failed: {result}" + print("✅ Bracket parser") + + # Test de clases híbridas + h = Hex("FF") + assert str(h) == "0xFF", f"Hex test failed: {h}" + print("✅ Clases híbridas") + + # Test de motor de evaluación + engine = HybridEvaluationEngine() + result = engine.evaluate_line("2 + 3") + assert result.result == 5, f"Engine test failed: {result.result}" + print("✅ Motor de evaluación") + + print("✅ Todos los tests básicos pasaron") + return True + + except Exception as e: + print(f"❌ Error en tests: {e}") + return False + + +def main(): + """Función principal de setup""" + print("=== Calculadora MAV - CAS Híbrido - Setup ===\n") + + # Verificar Python + if not check_python_version(): + sys.exit(1) + + print("\n=== Verificando dependencias ===") + + # Verificar tkinter + if not check_tkinter(): + print("\n❌ tkinter es requerido para la interfaz gráfica") + sys.exit(1) + + # Verificar dependencias principales + deps_ok = True + deps_ok &= check_module("sympy") + deps_ok &= check_module("matplotlib") + deps_ok &= check_module("numpy") + + # Si faltan dependencias, intentar instalar + if not deps_ok: + print("\n=== Faltan dependencias requeridas ===") + response = input("¿Instalar automáticamente? (s/n): ").lower().strip() + + if response in ['s', 'si', 'y', 'yes']: + if not install_requirements(): + print("❌ Error instalando dependencias") + sys.exit(1) + else: + print("❌ Instale las dependencias manualmente:") + print(" pip install sympy matplotlib numpy") + sys.exit(1) + + # Verificar módulos opcionales + print("\n=== Módulos opcionales ===") + check_optional_modules() + + # Ejecutar tests + if not run_tests(): + print("❌ Tests fallaron") + sys.exit(1) + + # Crear acceso directo + print("\n=== Configuración adicional ===") + response = input("¿Crear acceso directo en el escritorio? (s/n): ").lower().strip() + if response in ['s', 'si', 'y', 'yes']: + create_desktop_shortcut() + + print("\n✅ ¡Setup completado exitosamente!") + print("\nPara ejecutar la calculadora:") + print(f" python {Path.cwd() / 'hybrid_calc_app.py'}") + print("\nO usa el acceso directo si lo creaste.") + + +if __name__ == "__main__": + main() diff --git a/test_suite.py b/test_suite.py new file mode 100644 index 0000000..3976b0f --- /dev/null +++ b/test_suite.py @@ -0,0 +1,410 @@ +#!/usr/bin/env python3 +""" +Suite de tests unitarios para Calculadora MAV - CAS Híbrido +""" +import unittest +import sys +import os +from pathlib import Path + +# Agregar directorio actual al path para importar módulos +sys.path.insert(0, str(Path(__file__).parent)) + +import sympy +from bracket_parser import BracketParser, EquationDetector +from hybrid_base_types import ( + HybridHex, HybridBin, HybridDec, HybridIP4, HybridChr, + Hex, Bin, Dec, IP4, Chr +) +from hybrid_evaluation_engine import HybridEvaluationEngine, EvaluationResult + + +class TestBracketParser(unittest.TestCase): + """Tests para el bracket parser""" + + def setUp(self): + self.parser = BracketParser() + + def test_bracket_transformation(self): + """Test transformación de sintaxis con corchetes""" + test_cases = [ + ("Hex[FF]", 'Hex("FF")'), + ("IP4[192.168.1.1/24]", 'IP4("192.168.1.1/24")'), + ("Bin[1010]", 'Bin("1010")'), + ("Dec[10.5]", 'Dec("10.5")'), + ("Chr[A]", 'Chr("A")'), + ] + + for input_expr, expected in test_cases: + with self.subTest(input_expr=input_expr): + result, info = self.parser.parse_line(input_expr) + self.assertEqual(result, expected) + self.assertEqual(info, "bracket_transform") + + def test_solve_shortcut(self): + """Test transformación de atajo solve""" + test_cases = [ + ("x=?", "solve(x)"), + ("variable_name=?", "solve(variable_name)"), + ("a=?", "solve(a)"), + ] + + for input_expr, expected in test_cases: + with self.subTest(input_expr=input_expr): + result, info = self.parser.parse_line(input_expr) + self.assertEqual(result, expected) + self.assertEqual(info, "solve_shortcut") + + def test_equation_detection(self): + """Test detección de ecuaciones standalone""" + equation_cases = [ + "x + 2 = 5", + "3*a + b = 10", + "x**2 == 4", + "y > 5", + ] + + for equation in equation_cases: + with self.subTest(equation=equation): + result, info = self.parser.parse_line(equation) + self.assertEqual(info, "equation") + self.assertTrue(result.startswith('_add_equation(')) + + def test_non_equation_cases(self): + """Test casos que NO deben detectarse como ecuaciones""" + non_equation_cases = [ + "result = solve(x + 2, x)", # Asignación Python + "2 + 3", # Expresión simple + "sin(pi/2)", # Función + ] + + for expr in non_equation_cases: + with self.subTest(expr=expr): + result, info = self.parser.parse_line(expr) + self.assertNotEqual(info, "equation") + + def test_comments(self): + """Test manejo de comentarios""" + comment_cases = [ + "# Esto es un comentario", + " # Comentario con espacios", + "", # Línea vacía + ] + + for comment in comment_cases: + with self.subTest(comment=comment): + result, info = self.parser.parse_line(comment) + self.assertEqual(info, "comment") + + +class TestHybridBaseTypes(unittest.TestCase): + """Tests para las clases base híbridas""" + + def test_hybrid_hex(self): + """Test clase HybridHex""" + # Creación desde string + h1 = Hex("FF") + self.assertEqual(h1.value, 255) + self.assertEqual(str(h1), "0xFF") + + # Creación desde entero + h2 = Hex(255) + self.assertEqual(h2.value, 255) + self.assertEqual(str(h2), "0xFF") + + # Verificar que es instancia de SymPy Basic + self.assertIsInstance(h1, sympy.Basic) + + # Test conversión decimal + self.assertEqual(h1.__dec__(), 255) + + def test_hybrid_bin(self): + """Test clase HybridBin""" + # Creación desde string binario + b1 = Bin("1010") + self.assertEqual(b1.value, 10) + self.assertEqual(str(b1), "0b1010") + + # Creación desde entero + b2 = Bin(10) + self.assertEqual(b2.value, 10) + self.assertEqual(str(b2), "0b1010") + + # Verificar SymPy Basic + self.assertIsInstance(b1, sympy.Basic) + + def test_hybrid_ip4(self): + """Test clase HybridIP4""" + # IP con CIDR + ip1 = IP4("192.168.1.100/24") + self.assertEqual(ip1.ip_address, "192.168.1.100") + self.assertEqual(ip1.prefix, 24) + self.assertEqual(str(ip1), "192.168.1.100/24") + + # IP sin máscara + ip2 = IP4("10.0.0.1") + self.assertEqual(ip2.ip_address, "10.0.0.1") + self.assertIsNone(ip2.prefix) + + # Verificar SymPy Basic + self.assertIsInstance(ip1, sympy.Basic) + + # Test métodos especializados + network = ip1.NetworkAddress() + self.assertEqual(str(network), "192.168.1.0/24") + + broadcast = ip1.BroadcastAddress() + self.assertEqual(str(broadcast), "192.168.1.255/24") + + nodes = ip1.Nodes() + self.assertEqual(nodes, 254) # 2^8 - 2 + + def test_hybrid_chr(self): + """Test clase HybridChr""" + # Carácter único + c1 = Chr("A") + self.assertEqual(c1.value, 65) + self.assertEqual(str(c1), "A") + + # String múltiple + c2 = Chr("Hello") + self.assertEqual(c2.value, [72, 101, 108, 108, 111]) + self.assertEqual(str(c2), "Hello") + + # Verificar SymPy Basic + self.assertIsInstance(c1, sympy.Basic) + + def test_hybrid_dec(self): + """Test clase HybridDec""" + # Desde string decimal + d1 = Dec("10.5") + self.assertEqual(d1.value, 10.5) + self.assertEqual(str(d1), "10.5") + + # Desde entero + d2 = Dec(10) + self.assertEqual(d2.value, 10.0) + self.assertEqual(str(d2), "10") + + # Verificar SymPy Basic + self.assertIsInstance(d1, sympy.Basic) + + +class TestHybridEvaluationEngine(unittest.TestCase): + """Tests para el motor de evaluación híbrida""" + + def setUp(self): + self.engine = HybridEvaluationEngine() + + def test_basic_expressions(self): + """Test expresiones básicas""" + test_cases = [ + ("2 + 3", 5), + ("10 * 2", 20), + ("15 / 3", 5), + ] + + for expr, expected in test_cases: + with self.subTest(expr=expr): + result = self.engine.evaluate_line(expr) + self.assertFalse(result.is_error) + self.assertEqual(result.result, expected) + + def test_sympy_functions(self): + """Test funciones de SymPy""" + # Test sin con pi/2 + result = self.engine.evaluate_line("sin(pi/2)") + self.assertFalse(result.is_error) + self.assertEqual(result.result, 1) + + # Test diferenciación + result = self.engine.evaluate_line("diff(x**2, x)") + self.assertFalse(result.is_error) + self.assertEqual(str(result.result), "2*x") + + def test_bracket_syntax(self): + """Test sintaxis con corchetes""" + # Test Hex + result = self.engine.evaluate_line("Hex[FF]") + self.assertFalse(result.is_error) + self.assertIsInstance(result.result, Hex) + self.assertEqual(result.result.value, 255) + + # Test IP4 + result = self.engine.evaluate_line("IP4[192.168.1.1/24]") + self.assertFalse(result.is_error) + self.assertIsInstance(result.result, IP4) + self.assertEqual(result.result.ip_address, "192.168.1.1") + + def test_equation_handling(self): + """Test manejo de ecuaciones""" + # Agregar ecuación + result = self.engine.evaluate_line("x + 2 = 5") + self.assertFalse(result.is_error) + self.assertEqual(result.result_type, "equation_added") + + # Verificar que la ecuación se agregó + self.assertEqual(len(self.engine.equations), 1) + + def test_variable_creation(self): + """Test creación automática de variables""" + # Usar variable no definida + result = self.engine.evaluate_line("x + y") + self.assertFalse(result.is_error) + + # Verificar que las variables se crearon como símbolos + self.assertIn("x", self.engine.symbol_table) + self.assertIn("y", self.engine.symbol_table) + self.assertIsInstance(self.engine.symbol_table["x"], sympy.Symbol) + + def test_solve_shortcut(self): + """Test atajo de solve""" + # Agregar ecuación + self.engine.evaluate_line("x + 2 = 5") + + # Usar atajo solve + result = self.engine.evaluate_line("x=?") + self.assertFalse(result.is_error) + + # Verificar que x se resolvió + self.assertIn("x", self.engine.symbol_table) + + def test_error_handling(self): + """Test manejo de errores""" + # División por cero + result = self.engine.evaluate_line("1/0") + self.assertTrue(result.is_error) + + # Sintaxis inválida + result = self.engine.evaluate_line("2 +") + self.assertTrue(result.is_error) + + def test_clear_operations(self): + """Test operaciones de limpieza""" + # Agregar datos + self.engine.evaluate_line("x = 5") + self.engine.evaluate_line("y + 2 = 7") + + # Verificar que hay datos + self.assertTrue(len(self.engine.symbol_table) > 0) + self.assertTrue(len(self.engine.equations) > 0) + + # Limpiar variables + self.engine.clear_variables() + self.assertEqual(len(self.engine.symbol_table), 0) + + # Limpiar ecuaciones + self.engine.clear_equations() + self.assertEqual(len(self.engine.equations), 0) + + +class TestIntegration(unittest.TestCase): + """Tests de integración del sistema completo""" + + def setUp(self): + self.engine = HybridEvaluationEngine() + + def test_specialized_class_methods(self): + """Test métodos de clases especializadas""" + # IP4 NetworkAddress + result = self.engine.evaluate_line("IP4[192.168.1.100/24].NetworkAddress[]") + self.assertFalse(result.is_error) + self.assertIsInstance(result.result, IP4) + self.assertEqual(str(result.result), "192.168.1.0/24") + + def test_mixed_operations(self): + """Test operaciones mixtas""" + # Hex + entero + result = self.engine.evaluate_line("Hex[FF] + 1") + self.assertFalse(result.is_error) + # El resultado depende de cómo implementemos las operaciones + + def test_equation_solving_workflow(self): + """Test flujo completo de resolución de ecuaciones""" + # Agregar ecuaciones + self.engine.evaluate_line("x + y = 10") + self.engine.evaluate_line("x - y = 2") + + # Resolver sistema + try: + solutions = self.engine.solve_system() + self.assertIsInstance(solutions, dict) + # Verificar que x = 6, y = 4 + self.assertEqual(solutions.get("x"), 6) + self.assertEqual(solutions.get("y"), 4) + except Exception as e: + self.fail(f"Solve system failed: {e}") + + def test_sympy_integration(self): + """Test integración con SymPy""" + # Diferenciación de expresión con variables + result = self.engine.evaluate_line("diff(x**3 + 2*x**2 + x, x)") + self.assertFalse(result.is_error) + expected = "3*x**2 + 4*x + 1" + self.assertEqual(str(result.result), expected) + + # Integración + result = self.engine.evaluate_line("integrate(2*x, x)") + self.assertFalse(result.is_error) + self.assertEqual(str(result.result), "x**2") + + +def run_all_tests(): + """Ejecuta todos los tests""" + print("=== Ejecutando Suite de Tests ===\n") + + # Crear suite de tests + loader = unittest.TestLoader() + suite = unittest.TestSuite() + + # Agregar test classes + test_classes = [ + TestBracketParser, + TestHybridBaseTypes, + TestHybridEvaluationEngine, + TestIntegration + ] + + for test_class in test_classes: + tests = loader.loadTestsFromTestCase(test_class) + suite.addTests(tests) + + # Ejecutar tests + runner = unittest.TextTestRunner(verbosity=2) + result = runner.run(suite) + + # Mostrar resumen + print(f"\n=== Resumen ===") + print(f"Tests ejecutados: {result.testsRun}") + print(f"Errores: {len(result.errors)}") + print(f"Fallos: {len(result.failures)}") + + if result.errors: + print("\nErrores:") + for test, error in result.errors: + print(f" {test}: {error}") + + if result.failures: + print("\nFallos:") + for test, failure in result.failures: + print(f" {test}: {failure}") + + success = len(result.errors) == 0 and len(result.failures) == 0 + print(f"\n{'✅ Todos los tests pasaron' if success else '❌ Algunos tests fallaron'}") + + return success + + +def main(): + """Función principal""" + if len(sys.argv) > 1 and sys.argv[1] == "--verbose": + # Ejecutar tests individuales con más detalle + unittest.main(verbosity=2) + else: + # Ejecutar suite completa + success = run_all_tests() + sys.exit(0 if success else 1) + + +if __name__ == "__main__": + main()