Primera version funcionante

This commit is contained in:
Miguel 2025-06-01 16:30:03 +02:00
commit ebb0ac82fa
16 changed files with 5305 additions and 0 deletions

194
.gitignore vendored Normal file
View File

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

269
bracket_parser.py Normal file
View File

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

View File

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

160
corrections_readme.md Normal file
View File

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

181
debug_and_test.py Normal file
View File

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

606
hybrid_base_types.py Normal file
View File

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

833
hybrid_calc_app.py Normal file
View File

@ -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("<KeyRelease>", self.on_key_release)
self.input_text.bind("<Button-3>", lambda e: self._show_context_menu(e, "input"))
self.output_text.bind("<Button-3>", 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("<MouseWheel>", _unified_mouse_wheel)
self.output_text.bind("<MouseWheel>", _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("<<Cut>>"))
context_menu.add_command(label="Copiar", command=lambda: self.input_text.event_generate("<<Copy>>"))
context_menu.add_command(label="Pegar", command=lambda: self.input_text.event_generate("<<Paste>>"))
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()

30
hybrid_calc_history.txt Normal file
View File

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

View File

@ -0,0 +1,4 @@
{
"window_geometry": "1000x700+722+83",
"sash_pos_x": 341
}

528
hybrid_evaluation_engine.py Normal file
View File

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

441
interactive_results.py Normal file
View File

@ -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,
"<Button-1>",
lambda e, r=result: self._handle_interactive_click(r)
)
text_widget.tag_bind(
tag_name,
"<Enter>",
lambda e: text_widget.config(cursor="hand2")
)
text_widget.tag_bind(
tag_name,
"<Leave>",
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()

708
main_launcher.py Normal file
View File

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

200
quick_start_readme.md Normal file
View File

@ -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.*

27
requirements_and_setup.py Normal file
View File

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

245
setup_script.py Normal file
View File

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

410
test_suite.py Normal file
View File

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