Sistema de Validación Cruzada MATLAB ↔ Python¶
Estado 2026-05-26¶
La validacion directa de fixtures se ejecuto con python3 scripts/validate_matlab_fixtures.py sobre los fixtures anonimizados/sinteticos disponibles.
Resultado:
- Fixtures comparables: 48
- Passing: 48
- Failing: 0
- Skipped: 0
El suite Python completo tambien queda cubierto por PYTHONPATH=python pytest tests/test_matlab_validation.py tests/test_lateral_synthetic_parity.py.
🎯 Objetivo Crítico¶
Preservar la validez matemática del código MATLAB original, previamente revisado por el equipo de investigación.
La migración a Python NO debe introducir errores numéricos ni cambios en los algoritmos biomecánicos. Cada cálculo debe replicar exactamente el comportamiento de MATLAB.
📋 Estado Actual¶
Procedimiento reproducible para paper¶
Este documento describe el procedimiento tecnico reproducible para comparar la salida raw de MATLAB, la implementacion raw equivalente en Python y la salida clinica que se muestra en EPPA. No constituye aprobacion regulatoria ni certificacion clinica; es evidencia tecnica para revision del equipo LABIS.
Fuentes versionadas:
- Codigo MATLAB legacy:
datos/data/matlab/InterfazMedicionFrenteJulio2024v2 (1).m,datos/data/matlab/InterfazMedicionEspaldaJulio2024v2 (1).m,datos/data/matlab/InterfazMedicionPerfilDerechoJulio2024 (1).m,datos/data/matlab/InterfazMedicionPerfilIzquierdoJulio2024 (1).m. - Implementacion Python:
python/calculations.py,python/calculations_posterior.py,python/calculations_lateral.py. - Fixtures reproducibles:
tests/fixtures/matlab_test_cases_anterior.json,tests/fixtures/matlab_test_cases_posterior.json,tests/fixtures/matlab_test_cases_lateral.json,tests/fixtures/matlab_test_cases_lateral_left.json,tests/fixtures/matlab_test_cases_lateral_synthetic_profiles.json. - Tests de CI:
tests/test_matlab_validation.py,tests/test_lateral_synthetic_parity.py,scripts/validate_matlab_fixtures.py.
Metodo:
- Extraer desde MATLAB los valores raw y las lineas fuente de cada ecuacion.
- Guardar fixtures con coordenadas sinteticas o anonimizadas y
expected_outputMATLAB raw. - Ejecutar la funcion Python raw equivalente con tolerancia absoluta
1e-10. - Separar cualquier decision clinica de presentacion en campos o funciones distintas, sin modificar el fixture raw.
- Registrar pendientes cuando no hay fixture seguro o cuando falta revision clinica.
Tabla MATLAB raw vs Python raw vs salida clinica¶
| Caso | Fuente fixture | MATLAB raw esperado | Python raw validado | Salida clinica EPPA | Estado |
|---|---|---|---|---|---|
| Vista anterior: cabeza, distancias EFM/EN/PM, tronco, pelvis, Q der/izq, hombros | matlab_test_cases_anterior.json |
9 casos raw MATLAB | mismas funciones Python en calculations.py |
igual al raw para estas metricas | Validado |
| Vista posterior: calcaneo izq/der, balances C7/T7/L5, pelvis, cabeza | matlab_test_cases_posterior.json |
7 casos raw MATLAB | mismas funciones Python en calculations_posterior.py |
igual al raw para estas metricas | Validado |
| Perfil derecho historico: cabeza, tronco, codo, rodilla | matlab_test_cases_lateral.json |
cabeza 49.001326554802318, tronco 2.37492340146431, codo 162.71915430122797, rodilla 174.57883417601818 |
coincide con Python raw | rodilla se transforma en la capa clinica; las otras metricas se muestran raw | Validado |
| Perfil izquierdo anonimizado: cabeza, tronco, codo, rodilla | matlab_test_cases_lateral_left.json |
cabeza -48.656501450723965, tronco 0.6721869383364066, codo 161.32970717661217, rodilla 175.13259527477706 |
coincide con Python raw | rodilla clinica 4.86740472522294 |
Validado |
| Perfil sintetico alineado derecho/izquierdo | matlab_test_cases_lateral_synthetic_profiles.json |
rodilla raw MATLAB 180.0 |
knee_angle_raw_matlab(...) == 180.0 |
knee_angle_convention(...) == 0.0 |
Validado |
| Perfil sintetico flexo derecho/izquierdo | matlab_test_cases_lateral_synthetic_profiles.json |
rodilla raw MATLAB 171.4270016363887 |
coincide con Python raw | +8.572998363611305 |
Validado |
| Perfil sintetico recurvatum derecho/izquierdo | matlab_test_cases_lateral_synthetic_profiles.json |
rodilla raw MATLAB -171.4270016363887 |
coincide con Python raw | -8.572998363611305 |
Validado |
Decision clinica para rodilla¶
MATLAB calcula el angulo interno/geometrico de rodilla. En una rodilla alineada, ese angulo raw puede ser 180°; en los casos de hiperextension puede aparecer con signo negativo por la orientacion del perfil. Para la lectura clinica acordada en EPPA, la posicion neutra se muestra como 0° y se informa la desviacion respecto de la extension neutral:
- raw MATLAB/Python
180°-> salida clinica0°. - raw MATLAB/Python entre
0°y180°-> salida clinica180 - raw, positiva para Genu Flexo. - raw MATLAB/Python negativa -> salida clinica
-(180 - abs(raw)), negativa para Genu Recurvatum.
La separacion es intencional: knee_angle_raw_matlab conserva paridad auditable con MATLAB, mientras knee_angle_convention es la salida clinica que debe ver el usuario. La decision esta cubierta por tests/test_matlab_validation.py::test_rodilla_alineada_documenta_transformacion_180_a_0_clinico y por tests/test_lateral_synthetic_parity.py.
Casos validados y pendientes¶
Validados sin datos sensibles:
- Anterior: 9 casos raw MATLAB/Python, incluyendo
TC_ANT_009 shoulder_angle. - Posterior: 7 casos raw MATLAB/Python.
- Perfil derecho historico: 4 casos raw MATLAB/Python.
- Perfil izquierdo anonimizado: 4 casos raw MATLAB/Python.
- Perfiles sinteticos derecho/izquierdo: 6 perfiles con cabeza, tronco, codo, rodilla raw y convencion clinica de rodilla.
Pendientes:
- Review documental por Cristina/Luis antes de usar esta seccion en paper o material externo.
- Ampliar fixtures reales solo si se entregan como datos anonimizados aprobados; no se deben agregar nombres, imagenes identificables, Excel clinicos reales, credenciales ni payloads de pacientes.
- Revisar metricas laterales no cubiertas por los fixtures actuales: tangente dorsal, tangente sacro, apex cervical/lumbar, alineacion sagital y traslacion pelvica.
Vista Perfil Derecho e Izquierdo: validacion raw y convencion clinica¶
Para CALC-003 / issue #223, la validacion nueva usa solo fixtures sinteticos hasta que el equipo apruebe explicitamente fixtures reales o anonimizados. No se incorporan Excel reales, imagenes, nombres, marker CSVs reales ni datos clinicos derivados.
Fixture sintetico activo:
- Perfiles derecho e izquierdo:
tests/fixtures/matlab_test_cases_lateral_synthetic_profiles.json
Cobertura:
- Paridad raw MATLAB-equivalente para cabeza, tronco, codo y rodilla.
- Perfiles derecho e izquierdo con sistema de coordenadas espejado.
- Rodilla raw MATLAB
180°alineada y salida clinica EPPA0°. - Casos limite de landmarks faltantes, landmarks intercambiados, segmentos de longitud cero, rangos de angulos y tolerancia de redondeo.
Fixtures historicos existentes:
- Perfil derecho:
tests/fixtures/matlab_test_cases_lateral.json - Perfil izquierdo:
tests/fixtures/matlab_test_cases_lateral_left.json
La rodilla conserva dos salidas separadas:
knee_angle_raw_matlab: paridad auditada con MATLAB, donde una rodilla alineada puede reportar180°.knee_angle_convention: salida clínica para EPPA, donde esa misma rodilla alineada se transforma intencionalmente a0°; valores positivos representan Genu Flexo y negativos Genu Recurvatum.
Los fixtures sinteticos nuevos no incluyen nombres reales, imagenes identificables ni datos clinicos sensibles. La solicitud de datos reales/anonimizados queda documentada como borrador no enviado en docs/testing/draft-cristina-lateral-fixtures-request.md.
Vista Anterior: ✅ VALIDADA¶
| Cálculo | MATLAB Líneas | Python Función | Estado | Precisión |
|---|---|---|---|---|
| Ángulo de cabeza | 1526-1543 | angle_with_horizontal |
✅ PASS | <1e-10 |
| Distancia EFM | - | horizontal_distance_cm |
✅ PASS | <1e-10 |
| Distancia EN | - | horizontal_distance_cm |
✅ PASS | <1e-10 |
| Distancia PM | - | horizontal_distance_cm |
✅ PASS | <1e-10 |
| Ángulo de tronco | 554-560 | trunk_angle |
✅ PASS | <1e-10 |
| Ángulo de pelvis | 797-820 | pelvis_angle |
✅ PASS | <1e-10 |
| Ángulo Q derecho | 910-914 | q_angle |
✅ PASS | <1e-10 |
| Ángulo Q izquierdo | 946-950 | q_angle |
✅ PASS | <1e-10 |
| Ángulo de hombros | 682-696 | angle_with_horizontal |
✅ PASS | <1e-10 |
Total: 9/9 tests pasando ✅
Vista Posterior: VALIDADA¶
Archivos fuente:
- old/matlab/InterfazMedicionEspaldaJulio2024v2 (1).m
Fixture activo: tests/fixtures/matlab_test_cases_posterior.json
Cálculos validados (7 casos): - TC_POST_001: Ángulo calcáneo izquierdo (líneas 1499-1522) - TC_POST_002: Ángulo calcáneo derecho (líneas 1673-1697) - TC_POST_003: Desbalance coronal C7 (líneas 942-948) - TC_POST_004: Desbalance coronal T7 (líneas 1013-1017) - TC_POST_005: Desbalance coronal L5 (líneas 1084-1088) - TC_POST_006: Ángulo pelvis posterior (líneas 1238-1251) - TC_POST_007: Ángulo cabeza posterior (líneas 866-890)
Resultado: paridad MATLAB/Python con tolerancia 1e-10.
Vista Perfil Derecho: VALIDADA PARCIALMENTE¶
Archivos fuente:
- old/matlab/InterfazMedicionPerfilDerechoJulio2024 (1).m
Fixture activo: tests/fixtures/matlab_test_cases_lateral.json
Cálculos validados: - Ángulo horizontal de cabeza - Ángulo vertical de tronco - Ángulo de codo - Ángulo raw de rodilla
Cálculos pendientes: - Tangente dorsal - Tangente sacro - Ápex cervical - Ápex lumbar - Alineación sagital - Traslación pélvica
Vista Perfil Izquierdo: VALIDADA PARCIALMENTE¶
Archivos fuente:
- old/matlab/InterfazMedicionPerfilIzquierdoJulio2024 (1).m
Fixture activo: tests/fixtures/matlab_test_cases_lateral_left.json
Cálculos validados: - Ángulo horizontal de cabeza - Ángulo vertical de tronco - Ángulo de codo - Ángulo raw de rodilla y conversion clinica de rodilla
Cálculos pendientes: - Los mismos pendientes de perfil derecho, con verificacion de simetria izquierda/derecha.
🛠️ Metodología de Validación¶
Paso 1: Extraer Algoritmo MATLAB¶
- Localizar función en código
.m(líneas específicas) - Identificar variables de entrada
- Copiar algoritmo EXACTO (incluyendo estructuras
if/else, signos, etc.) - Documentar número de líneas para referencia
Ejemplo:
% InterfazMedicionFrenteJulio2024v2.m, líneas 554-560
A = [XEE-XPSP, YEE-YPSP];
B = [XPSP-XPSP, YEE-YPSP]; % Vector vertical
gradosTronco = rad2deg(acos(dot(A,B)/(norm(A)*norm(B))));
if XEE < XPSP
gradosTronco = gradosTronco; % Positivo
else
gradosTronco = -gradosTronco; % Negativo
end
Paso 2: Implementar en Python¶
def trunk_angle(p_sp: Point, p_ee: Point) -> float:
"""Angle between the vertical through the Escotadura and the line EE-PSP.
MATLAB Reference: InterfazMedicionFrenteJulio2024v2.m, líneas 554-560
Validated by: Clinical research team (original)
"""
dx = p_ee[0] - p_sp[0]
dy = p_ee[1] - p_sp[1]
if dy == 0:
return 90.0 if dx < 0 else -90.0
# Vector from PSP to EE and a vertical vector through PSP
a = (dx, dy)
b = (0.0, dy) # Vertical
dot = a[0] * b[0] + a[1] * b[1]
mag_a = math.hypot(*a)
mag_b = math.hypot(*b)
angle = math.degrees(math.acos(dot / (mag_a * mag_b)))
return angle if dx < 0 else -angle
Paso 3: Generar Caso de Prueba desde MATLAB¶
Ejecutar matlab_validation/generate_test_cases.m con datos reales:
% Cargar puntos de un caso validado
XPSP = 493.8188;
YPSP = 1182.7;
XEE = 486.1345;
YEE = 694.5977;
% Ejecutar algoritmo MATLAB
A = [XEE-XPSP, YEE-YPSP];
B = [XPSP-XPSP, YEE-YPSP];
gradosTronco = rad2deg(acos(dot(A,B)/(norm(A)*norm(B))));
if XEE < XPSP
gradosTronco = gradosTronco;
else
gradosTronco = -gradosTronco;
end
fprintf('Resultado MATLAB: %.15f\n', gradosTronco);
% Output: 0.901945329475723
Paso 4: Agregar a matlab_test_cases_anterior.json¶
{
"id": "TC_ANT_005",
"name": "trunk_angle",
"description": "Ángulo de inclinación del tronco",
"matlab_function": "Líneas 554-560 de InterfazMedicionFrenteJulio2024v2.m",
"inputs": {
"p_sp": [493.8188, 1182.7],
"p_ee": [486.1345, 694.5977]
},
"expected_output": 0.901945329475723,
"tolerance": 1e-10,
"unit": "grados"
}
Paso 5: Escribir Test Unitario¶
def test_tc_ant_005_trunk_angle(self, matlab_test_cases):
"""
TC_ANT_005: Ángulo de inclinación del tronco
MATLAB: Líneas 554-560
CRÍTICO: Algoritmo validado por estadistas
"""
tc = next(tc for tc in matlab_test_cases['test_cases'] if tc['id'] == 'TC_ANT_005')
p_sp = tuple(tc['inputs']['p_sp'])
p_ee = tuple(tc['inputs']['p_ee'])
resultado_python = trunk_angle(p_sp, p_ee)
esperado_matlab = tc['expected_output']
tolerancia = tc['tolerance']
assert math.isclose(resultado_python, esperado_matlab, abs_tol=tolerancia), \
f"Ángulo de tronco difiere: Python={resultado_python:.15f}, MATLAB={esperado_matlab:.15f}"
Paso 6: Ejecutar y Validar¶
python3 -m pytest tests/test_matlab_validation.py::TestMatlabValidationAnterior::test_tc_ant_005_trunk_angle -v
Criterio de éxito: Diferencia < 1e-10 (0.0000000001 grados)
📂 Estructura de Archivos¶
orthoposture/
├── matlab_validation/
│ └── generate_test_cases.m # Script MATLAB para generar casos
├── tests/
│ ├── fixtures/
│ │ ├── matlab_test_cases_anterior.json ✅ COMPLETO (9 casos)
│ │ ├── matlab_test_cases_posterior.json ✅ COMPLETO (7 casos)
│ │ ├── matlab_test_cases_lateral.json ✅ PARCIAL (4 casos perfil derecho)
│ │ ├── matlab_test_cases_lateral_left.json ✅ PARCIAL (4 casos perfil izquierdo)
│ │ └── matlab_test_cases_lateral_synthetic_profiles.json ✅ PARCIAL (6 perfiles sinteticos)
│ ├── test_matlab_validation.py # Suite de tests de validación
│ └── test_calculations.py # Tests legacy (mantener por compatibilidad)
├── python/
│ ├── calculations.py # Implementación Python
│ └── test_calculations.py # Tests originales
└── docs/
└── matlab-python-validation.md # Esta documentación
⚠️ Reglas de Oro¶
1. NUNCA modificar test para hacerlo pasar¶
Si un test falla: - ❌ MAL: Ajustar tolerancia o cambiar valor esperado - ✅ BIEN: Revisar implementación Python vs algoritmo MATLAB línea por línea
2. Precisión mínima: 1e-10¶
- Tolerancia absoluta:
1e-10(0.0000000001) - Tolerancia relativa:
1e-6solo para valores muy grandes (>1000) - Registrar precisión real alcanzada en cada test
3. Documentar TODAS las diferencias¶
Si algún algoritmo debe modificarse intencionalmente:
- Documentar en docs/memory.md con fecha y justificación
- Obtener aprobación del equipo de investigación clínica
- Actualizar test case con nota explicativa
4. Un caso de prueba = Un cálculo¶
No mezclar múltiples cálculos en un solo test case. Cada métrica debe validarse independientemente.
5. Trazabilidad completa¶
Cada test case debe incluir: - Número de líneas exactas del código MATLAB - Descripción del cálculo - Referencias anatómicas/biomecánicas - Nombre del archivo fuente MATLAB
🔄 Workflow para Nueva Vista¶
Template para Vista Posterior¶
1. Crear script MATLAB¶
matlab_validation/generate_test_cases_posterior.m:
clear all; clc;
% Cargar puntos de muestra (vista posterior)
puntos = struct();
puntos.LobuloIzq = [X, Y]; % POR COMPLETAR
puntos.LobuloDer = [X, Y];
% ... (resto de marcadores)
% CASO 1: Ángulo calcáneo izquierdo
% Extraer de InterfazMedicionEspaldaJulio2024v2.m, líneas XXX-YYY
% Código MATLAB original aquí
% CASO 2: Distancia gemelos
% ...
% Exportar a JSON
test_cases = {test_case_1, test_case_2, ...};
json_output = struct('vista', 'posterior', 'test_cases', test_cases);
% Guardar
2. Ejecutar MATLAB¶
cd matlab_validation
generate_test_cases_posterior
Output: tests/fixtures/matlab_test_cases_posterior.json
3. Implementar funciones Python¶
python/calculations_posterior.py:
def calcaneo_angle_left(...) -> float:
"""
MATLAB Ref: InterfazMedicionEspaldaJulio2024v2.m, líneas XXX-YYY
"""
# Implementación idéntica a MATLAB
pass
4. Crear tests¶
tests/test_matlab_validation.py (agregar clase):
class TestMatlabValidationPosterior:
def test_tc_post_001_calcaneo_izq(self, matlab_test_cases_post):
...
5. Ejecutar validación¶
python3 -m pytest tests/test_matlab_validation.py::TestMatlabValidationPosterior -v
📊 Dashboard de Validación¶
| Vista | Tests Implementados | Tests Pasando | Precisión Promedio | Estado |
|---|---|---|---|---|
| Anterior | 9/9 | 9/9 (100%) | <=1e-10 | Completo |
| Posterior | 7/7 | 7/7 (100%) | <=1e-10 | Completo |
| Perfil Der | 4/4 raw historico + sinteticos | 100% | <=1e-10 | Parcial: faltan metricas laterales avanzadas |
| Perfil Izq | 4/4 anonimizado + sinteticos | 100% | <=1e-10 | Parcial: faltan metricas laterales avanzadas |
Meta: 100% de tests pasando en las 4 vistas antes de desplegar a producción.
🚀 Próximos Pasos¶
Inmediatos¶
- Review documental por Cristina/Luis.
- Definir si las metricas laterales pendientes entran al paper actual o a una tanda posterior.
- Incorporar nuevos fixtures solo con datos sinteticos o anonimizados aprobados.
Antes de Deploy¶
- [ ] 100% de cálculos del alcance clinico final validados
- [ ] Documentación completa de algoritmos
- [ ] Tests automatizados en CI/CD
- [ ] Aprobación del equipo de investigación clínica (spot check de resultados)
📝 Log de Validaciones¶
2025-10-06: Vista Anterior¶
- Tests creados: 8
- Tests pasando: 8
- Precisión alcanzada: <1e-12 en todos los casos
- Tiempo invertido: 2 horas
- Bloqueadores: Ninguno
- Notas: Todos los algoritmos replican exactamente MATLAB. Sin modificaciones necesarias.
🔗 Referencias¶
old/matlab/InterfazMedicionFrenteJulio2024v2 (1).m- Vista Anteriorold/matlab/InterfazMedicionEspaldaJulio2024v2 (1).m- Vista Posteriorold/matlab/InterfazMedicionPerfilDerechoJulio2024 (1).m- Perfil Derechoold/matlab/InterfazMedicionPerfilIzquierdoJulio2024 (1).m- Perfil Izquierdotests/fixtures/matlab_test_cases_anterior.json- Casos de prueba Vista Anteriortests/fixtures/matlab_test_cases_posterior.json- Casos de prueba Vista Posteriortests/fixtures/matlab_test_cases_lateral.json- Casos de prueba Perfil Derechotests/fixtures/matlab_test_cases_lateral_left.json- Casos anonimizados Perfil Izquierdotests/fixtures/matlab_test_cases_lateral_synthetic_profiles.json- Casos sinteticos de convencion lateraltests/test_matlab_validation.py- Tests de paridad MATLAB/Pythontests/test_lateral_synthetic_parity.py- Tests de convencion raw/clinica lateralscripts/validate_matlab_fixtures.py- Validador directo de fixtures para CI
Última actualización: 2026-05-26 Autor: Sistema de validación EPPA Relacionado: EPPA-016 (Validar cálculos), EPPA-027 (Tests de integración), #225 (DOC-VAL-001)