Pular para conteúdo

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:

  1. Extraer desde MATLAB los valores raw y las lineas fuente de cada ecuacion.
  2. Guardar fixtures con coordenadas sinteticas o anonimizadas y expected_output MATLAB raw.
  3. Ejecutar la funcion Python raw equivalente con tolerancia absoluta 1e-10.
  4. Separar cualquier decision clinica de presentacion en campos o funciones distintas, sin modificar el fixture raw.
  5. 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 y se informa la desviacion respecto de la extension neutral:

  • raw MATLAB/Python 180° -> salida clinica .
  • raw MATLAB/Python entre y 180° -> salida clinica 180 - 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 EPPA .
  • 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 reportar 180°.
  • knee_angle_convention: salida clínica para EPPA, donde esa misma rodilla alineada se transforma intencionalmente a ; 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

  1. Localizar función en código .m (líneas específicas)
  2. Identificar variables de entrada
  3. Copiar algoritmo EXACTO (incluyendo estructuras if/else, signos, etc.)
  4. 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-6 solo 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

  1. Review documental por Cristina/Luis.
  2. Definir si las metricas laterales pendientes entran al paper actual o a una tanda posterior.
  3. 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 Anterior
  • old/matlab/InterfazMedicionEspaldaJulio2024v2 (1).m - Vista Posterior
  • old/matlab/InterfazMedicionPerfilDerechoJulio2024 (1).m - Perfil Derecho
  • old/matlab/InterfazMedicionPerfilIzquierdoJulio2024 (1).m - Perfil Izquierdo
  • tests/fixtures/matlab_test_cases_anterior.json - Casos de prueba Vista Anterior
  • tests/fixtures/matlab_test_cases_posterior.json - Casos de prueba Vista Posterior
  • tests/fixtures/matlab_test_cases_lateral.json - Casos de prueba Perfil Derecho
  • tests/fixtures/matlab_test_cases_lateral_left.json - Casos anonimizados Perfil Izquierdo
  • tests/fixtures/matlab_test_cases_lateral_synthetic_profiles.json - Casos sinteticos de convencion lateral
  • tests/test_matlab_validation.py - Tests de paridad MATLAB/Python
  • tests/test_lateral_synthetic_parity.py - Tests de convencion raw/clinica lateral
  • scripts/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)