Primera version funcionante
This commit is contained in:
commit
ebb0ac82fa
|
@ -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
|
|
@ -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()
|
|
@ -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*
|
|
@ -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.
|
|
@ -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()
|
|
@ -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()
|
|
@ -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()
|
|
@ -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]])
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"window_geometry": "1000x700+722+83",
|
||||
"sash_pos_x": 341
|
||||
}
|
|
@ -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()
|
|
@ -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()
|
|
@ -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)
|
|
@ -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.*
|
|
@ -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
|
|
@ -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()
|
|
@ -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()
|
Loading…
Reference in New Issue