hbauzan's picture
Release v4.17.7 — sync from GitHub main
819a47f
Raw
History Blame Contribute Delete
12.7 kB
/**
* 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 };
}