/** * ReliefMath.js * Funciones puras (sin Three.js, sin DOM) para alinear y combinar matrices de * relieve multi-documento. Aisladas aquí para que sean testeables sin un canvas. * * Una "matriz" es `number[Z][X]`: Z = chunk_index, X = dimensión (768). * Documentos distintos tienen distinto número de chunks (Z), así que para * compararlos celda a celda (modos Overlay/Delta/Inspector) hay que alinear el * eje Z a una longitud común. Dos estrategias: * - 'NORMALIZED': re-muestrea todas las matrices a la MAYOR cantidad de chunks * mediante interpolación lineal sobre [0,1] (preserva la forma * global aunque los largos difieran). Estrategia por defecto. * - 'INDEX' : alinea por índice crudo de chunk y recorta a la MENOR. */ // Re-muestrea una matriz a `targetLen` filas por interpolación lineal sobre el // eje Z. Cada fila destino i mapea a la posición continua t = i/(targetLen-1) en // el rango [0, srcLen-1] y se interpola entre las dos filas vecinas. Las columnas // (dimensiones) se preservan 1:1. Devuelve una matriz nueva (no muta la entrada). export function resampleMatrixZ(matrix, targetLen) { const srcLen = matrix.length; if (srcLen === 0 || targetLen <= 0) return []; const cols = matrix[0].length; if (srcLen === targetLen) return matrix.map(row => row.slice()); const out = new Array(targetLen); for (let i = 0; i < targetLen; i++) { const t = targetLen === 1 ? 0 : (i * (srcLen - 1)) / (targetLen - 1); const lo = Math.floor(t); const hi = Math.min(lo + 1, srcLen - 1); const frac = t - lo; const rowLo = matrix[lo]; const rowHi = matrix[hi]; const row = new Array(cols); for (let c = 0; c < cols; c++) { row[c] = rowLo[c] * (1 - frac) + rowHi[c] * frac; } out[i] = row; } return out; } // Re-muestrea una serie 1D (perfil de una dimensión a lo largo de los chunks) a // `targetLen` puntos por interpolación lineal. Usado por el inspector (modo C) // para normalizar el eje X entre documentos de distinto largo. export function resampleSeries(series, targetLen) { const srcLen = series.length; if (srcLen === 0 || targetLen <= 0) return []; if (srcLen === targetLen) return series.slice(); const out = new Array(targetLen); for (let i = 0; i < targetLen; i++) { const t = targetLen === 1 ? 0 : (i * (srcLen - 1)) / (targetLen - 1); const lo = Math.floor(t); const hi = Math.min(lo + 1, srcLen - 1); const frac = t - lo; out[i] = series[lo] * (1 - frac) + series[hi] * frac; } return out; } // Alinea una lista de matrices a una longitud Z común según la estrategia. // Devuelve { aligned: matrix[], length }. No muta las entradas. export function alignMatrices(matrices, mode = 'NORMALIZED') { const valid = matrices.filter(m => m && m.length > 0); if (valid.length === 0) return { aligned: [], length: 0 }; const lengths = valid.map(m => m.length); const targetLen = mode === 'INDEX' ? Math.min(...lengths) : Math.max(...lengths); if (mode === 'INDEX') { const aligned = valid.map(m => m.slice(0, targetLen).map(row => row.slice())); return { aligned, length: targetLen }; } const aligned = valid.map(m => resampleMatrixZ(m, targetLen)); return { aligned, length: targetLen }; } // Resta celda a celda dos matrices (A - B) tras alinearlas en Z. // Devuelve la matriz delta (mismas dimensiones de columna, `length` filas). export function computeDelta(matrixA, matrixB, mode = 'NORMALIZED') { if (!matrixA || !matrixB || matrixA.length === 0 || matrixB.length === 0) return []; const { aligned } = alignMatrices([matrixA, matrixB], mode); const [a, b] = aligned; const rows = a.length; const cols = a[0].length; const delta = new Array(rows); for (let z = 0; z < rows; z++) { const rowA = a[z]; const rowB = b[z]; const row = new Array(cols); for (let x = 0; x < cols; x++) { row[x] = rowA[x] - rowB[x]; } delta[z] = row; } return delta; } // Extrae el perfil de una dimensión `dimIndex` a lo largo de los chunks de una // matriz → array de activaciones (eje X del inspector = chunk_index). export function dimensionSeries(matrix, dimIndex) { if (!matrix || matrix.length === 0) return []; const cols = matrix[0].length; if (dimIndex < 0 || dimIndex >= cols) return []; return matrix.map(row => row[dimIndex]); } // Máximo valor absoluto sobre toda una matriz (para normalización simétrica de // color/altura). Igual semántica que el helper interno del renderer, expuesto // para que el orquestador comparta el mismo |max| entre documentos. export function computeAbsMax(matrix) { let m = 0; for (let z = 0; z < matrix.length; z++) { const row = matrix[z]; for (let x = 0; x < row.length; x++) { const a = Math.abs(row[x]); if (a > m) m = a; } } return m; } // Computa la similitud de una dimensión entre dos series de tiempo / ejes latentes // basándose en un umbral de tolerancia absoluta elemento a elemento. // Devuelve el porcentaje de elementos (entre 0 y 100) donde la diferencia // absoluta es estrictamente menor que `tolerance`. export function computeDimensionSimilarity(seriesA, seriesB, tolerance = 0.01) { if (!seriesA || !seriesB) return 0; const len = seriesA.length; if (len === 0 || seriesB.length !== len) return 0; let count = 0; for (let i = 0; i < len; i++) { if (Math.abs(seriesA[i] - seriesB[i]) < tolerance) { count++; } } return (count / len) * 100; } // Computa la similitud de coseno entre los centroides semánticos promediados de // dos matrices de relieve. export function computeGlobalSimilarity(matrixA, matrixB) { const lenA = matrixA ? matrixA.length : 0; const lenB = matrixB ? matrixB.length : 0; const colsA = lenA > 0 ? matrixA[0].length : 0; const colsB = lenB > 0 ? matrixB[0].length : 0; const D = Math.max(colsA, colsB); if (D === 0) return 0; const centroidA = new Array(D).fill(0); if (lenA > 0) { for (let d = 0; d < D; d++) { let sum = 0; for (let z = 0; z < lenA; z++) { sum += matrixA[z][d] || 0; } centroidA[d] = sum / lenA; } } const centroidB = new Array(D).fill(0); if (lenB > 0) { for (let d = 0; d < D; d++) { let sum = 0; for (let z = 0; z < lenB; z++) { sum += matrixB[z][d] || 0; } centroidB[d] = sum / lenB; } } let dotProduct = 0; let normA2 = 0; let normB2 = 0; for (let d = 0; d < D; d++) { const valA = centroidA[d]; const valB = centroidB[d]; dotProduct += valA * valB; normA2 += valA * valA; normB2 += valB * valB; } const normA = Math.sqrt(normA2); const normB = Math.sqrt(normB2); return dotProduct / (normA * normB + 1e-9); } /** * Computa la varianza de cada dimensión de la matriz a lo largo de todos sus chunks. * Devuelve una lista ordenada de objetos `{ dimIndex, variance }` de mayor a menor varianza. * @param {number[][]} matrix - Matriz Z x D de activaciones. * @returns {{dimIndex: number, variance: number}[]} */ export function sortDimensionsByVariance(matrix) { if (!matrix || matrix.length === 0) return []; const Z = matrix.length; const D = matrix[0].length; const result = []; for (let d = 0; d < D; d++) { let sum = 0; let sumSq = 0; for (let z = 0; z < Z; z++) { const val = matrix[z][d] || 0; sum += val; sumSq += val * val; } const mean = sum / Z; const variance = (sumSq / Z) - (mean * mean); result.push({ dimIndex: d, variance }); } return result.sort((a, b) => b.variance - a.variance); } /** * Devuelve las primeras `limit` dimensiones con mayor varianza. * Si no hay matriz, devuelve un fallback secuencial de 0 a limit-1. * @param {number[][]} matrix - Matriz Z x D de activaciones. * @param {number} limit - Límite de dimensiones a devolver. * @returns {{dimIndex: number, variance: number}[]} */ export function getHighVarianceDimensions(matrix, limit = 100) { const dCount = matrix && matrix[0] ? matrix[0].length : 768; const actualLimit = Math.min(limit, dCount); if (!matrix || matrix.length === 0) { const fallback = []; for (let i = 0; i < actualLimit; i++) { fallback.push({ dimIndex: i, variance: 0 }); } return fallback; } const sorted = sortDimensionsByVariance(matrix); return sorted.slice(0, actualLimit); } // ============================================================================ // HELPERS DE LINEARIZACIÓN 1D (FLOAT32ARRAY) Y FALLBACKS SÍNCRONOS // ============================================================================ export function flattenMatrix2D(matrix) { if (!matrix || matrix.length === 0) return new Float32Array(0); const rows = matrix.length; const cols = matrix[0].length; const flat = new Float32Array(rows * cols); for (let r = 0; r < rows; r++) { const row = matrix[r]; const offset = r * cols; for (let c = 0; c < cols; c++) { flat[offset + c] = row[c] || 0; } } return flat; } export function unflattenMatrix2D(flatArray, rows, cols) { if (!flatArray || rows === 0 || cols === 0) return []; const matrix = new Array(rows); for (let r = 0; r < rows; r++) { const row = new Array(cols); const offset = r * cols; for (let c = 0; c < cols; c++) { row[c] = flatArray[offset + c]; } matrix[r] = row; } return matrix; } function sinc(x) { if (x === 0) return 1.0; const piX = Math.PI * x; return Math.sin(piX) / piX; } export function executeResampleZFallback(sourceData, sourceZ, sourceX, targetZ, targetX) { const output = new Float32Array(targetZ * targetX); const ratioZ = sourceZ / targetZ; const ratioX = sourceX / targetX; for (let tz = 0; tz < targetZ; tz++) { const srcFloatZ = tz * ratioZ; const minZ = Math.max(0, Math.floor(srcFloatZ) - 3); const maxZ = Math.min(sourceZ - 1, Math.floor(srcFloatZ) + 3); for (let tx = 0; tx < targetX; tx++) { const srcFloatX = tx * ratioX; const minX = Math.max(0, Math.floor(srcFloatX) - 3); const maxX = Math.min(sourceX - 1, Math.floor(srcFloatX) + 3); let accumulator = 0.0; let normalization = 0.0; for (let sz = minZ; sz <= maxZ; sz++) { const weightZ = sinc(srcFloatZ - sz); for (let sx = minX; sx <= maxX; sx++) { const weightX = sinc(srcFloatX - sx); const weight = weightZ * weightX; accumulator += sourceData[sz * sourceX + sx] * weight; normalization += weight; } } output[tz * targetX + tx] = normalization === 0 ? 0.0 : accumulator / normalization; } } return output; } export function executeDeltaAndSimilarityFallback(matrixA, matrixB, length) { const delta = new Float32Array(length); let dotProduct = 0.0; let normA = 0.0; let normB = 0.0; for (let i = 0; i < length; i++) { const valA = matrixA[i]; const valB = matrixB[i]; delta[i] = valA - valB; dotProduct += valA * valB; normA += valA * valA; normB += valB * valB; } const similarity = normA === 0 || normB === 0 ? 0.0 : dotProduct / (Math.sqrt(normA) * Math.sqrt(normB)); return { delta, similarity }; } export function executeVarianceSortFallback(data, rows, cols) { const variances = new Float32Array(cols); const indices = new Int32Array(cols); for (let c = 0; c < cols; c++) { indices[c] = c; let sum = 0.0; let sumSq = 0.0; for (let r = 0; r < rows; r++) { const val = data[r * cols + c]; sum += val; sumSq += val * val; } const mean = sum / rows; variances[c] = (sumSq / rows) - (mean * mean); } const indexArray = Array.from(indices); indexArray.sort((a, b) => variances[b] - variances[a]); const sortedIndices = new Int32Array(indexArray); return { indices: sortedIndices, variances }; }