import { isNumber } from '$lib/helpers/leaderboard'; import type { GroupScore, GuardModel } from '$lib/types/leaderboard'; import type { ToolsLanguageFilter, ToolsRadarScale, ToolsSortDirection, ToolsVizMetric } from '$lib/types/tools'; export const toolsPalette = [ '#3a55ff', '#0dce97', '#6b4bf5', '#f5a623', '#ff4d4f', '#21c1d6', '#b06bf5', '#5bd17a', '#ff8a3d', '#9aa3b2' ]; export function toolsDataGroups(models: readonly GuardModel[], allGroups: readonly string[]) { return allGroups.filter((group) => models.some((model) => isNumber(model.groups[group]?.score))); } export function toolsModelKey(model: GuardModel) { return model.modelId ?? model.name; } export function toolsModelColorMap(models: readonly GuardModel[]) { return new Map( models.map((model, index) => [toolsModelKey(model), toolsPalette[index % toolsPalette.length]]) ); } export function toolsModelColor(model: GuardModel, colorMap: ReadonlyMap) { return colorMap.get(toolsModelKey(model)) ?? '#0dce97'; } export function toolsSelectedModels( models: readonly GuardModel[], selectedModelKeys: readonly string[] ) { return models.filter((model) => selectedModelKeys.includes(toolsModelKey(model))); } export function toolsActiveGroups( selectedGroups: readonly string[], language: ToolsLanguageFilter, groupLanguages: Readonly> ) { return selectedGroups.filter( (group) => language === 'all' || groupLanguages[group]?.includes(language) ); } export function toolsPinnedModels( models: readonly GuardModel[], pinnedModelKeys: readonly string[] ) { return models.filter((model) => pinnedModelKeys.includes(toolsModelKey(model))); } export function toolsCell(model: GuardModel, group: string) { return model.groups[group] ?? null; } export function toolsMetricValue(cell: GroupScore | null, key: ToolsVizMetric) { if (!cell) return null; return cell[key]; } export function toolsRadarValue(cell: GroupScore | null, key: ToolsRadarScale) { if (!cell) return null; return cell[key]; } export function toolsLatencySortValue( model: GuardModel, key: 'model' | 'p50' | 'p95' | 'p99' | 'errorRate' ) { if (key === 'model') return model.short.toLowerCase(); return model[key]; } export function toolsSortLatencyRows( rows: readonly GuardModel[], key: 'model' | 'p50' | 'p95' | 'p99' | 'errorRate', direction: ToolsSortDirection ) { return [...rows].sort((a, b) => { const aValue = toolsLatencySortValue(a, key); const bValue = toolsLatencySortValue(b, key); const result = typeof aValue === 'string' && typeof bValue === 'string' ? aValue.localeCompare(bValue) : Number(aValue ?? Number.POSITIVE_INFINITY) - Number(bValue ?? Number.POSITIVE_INFINITY); return direction === 'asc' ? result : -result; }); } export function toolsHeatColor( value: number | null | undefined, min = 0, max = 1, curve: 'linear' | 'sqrt' = 'linear' ) { if (!isNumber(value)) return 'transparent'; const range = max - min; const normalized = range > 0 ? (value - min) / range : 0; const t = curve === 'sqrt' ? Math.sqrt(Math.max(0, Math.min(1, normalized))) : Math.max(0, Math.min(1, normalized)); if (t < 0.5) { const p = t / 0.5; return `rgb(${Math.round(13 + 242 * p)}, ${Math.round(206 - 36 * p)}, ${Math.round(151 - 111 * p)})`; } const p = (t - 0.5) / 0.5; return `rgb(${Math.round(255 - 25 * p)}, ${Math.round(170 - 93 * p)}, ${Math.round(40 + 39 * p)})`; } export function toolsRadarPoint(index: number, total: number, radius: number, cx = 260, cy = 214) { const angle = -Math.PI / 2 + (2 * Math.PI * index) / total; return { x: cx + Math.cos(angle) * radius, y: cy + Math.sin(angle) * radius }; } export function toolsRadarPolygon( model: GuardModel, activeGroups: readonly string[], radarScale: ToolsRadarScale ) { return activeGroups .map((group, index) => { const value = toolsRadarValue(toolsCell(model, group), radarScale); const point = toolsRadarPoint( index, activeGroups.length, 150 * Math.max(0, Math.min(1, value ?? 0)) ); return `${point.x.toFixed(1)},${point.y.toFixed(1)}`; }) .join(' '); } export function toolsLineScale(d0: number, d1: number, r0: number, r1: number) { return (value: number) => r0 + ((value - d0) / (d1 - d0 || 1)) * (r1 - r0); } export function toolsNiceTicks(min: number, max: number, count = 5) { let step = Math.pow(10, Math.floor(Math.log((max - min || 1) / count) / Math.LN10)); const error = (count * step) / (max - min || 1); if (error <= 0.15) step *= 10; else if (error <= 0.35) step *= 5; else if (error <= 0.75) step *= 2; const ticks: number[] = []; for (let tick = Math.ceil(min / step) * step; tick <= max + 1e-9; tick += step) { ticks.push(Math.round(tick * 1e6) / 1e6); } return ticks; } export function toolsScatterX(value: number, xMax: number) { return toolsLineScale(0, xMax, 52, 502)(value); } export function toolsScatterY(value: number, yMax: number) { return toolsLineScale(0, yMax, 368, 18)(value); } export function toolsBarY(value: number) { return toolsLineScale(0, 1, 268, 16)(value); } export function toolsBarLabelLines(label: string) { return label.split(/\s+/).reduce((lines, word) => { const last = lines.at(-1); if (!last || `${last} ${word}`.length > 12) { lines.push(word); return lines; } lines[lines.length - 1] = `${last} ${word}`; return lines; }, []); } export function toolsParetoX(value: number, xMax: number) { return toolsLineScale(0, xMax, 56, 742)(value); } export function toolsParetoY(value: number, y0: number) { return toolsLineScale(y0, 1, 330, 18)(value); } export function toolsScatterPoint(model: GuardModel, xMax: number, yMax: number) { return { x: toolsScatterX(model.fpr, xMax), y: toolsScatterY(model.fnr, yMax) }; } export function toolsParetoPoint(model: GuardModel, xMax: number, y0: number) { return { x: toolsParetoX(model.p95, xMax), y: toolsParetoY(model.integral, y0) }; } export function toolsGroupMean( group: string, key: ToolsVizMetric, selectedModels: readonly GuardModel[] ) { const values = selectedModels .map((model) => toolsMetricValue(toolsCell(model, group), key)) .filter(isNumber); if (!values.length) return null; return values.reduce((sum, value) => sum + value, 0) / values.length; } export function toolsScoreDelta(model: GuardModel) { const real = model.groups['Robustness Test (real)']; const robust = model.groups['Robustness Test (robust)']; if (!real || !robust) return 0; return robust.score - real.score; } export function toolsFnrDelta(model: GuardModel) { const real = model.groups['Robustness Test (real)']; const robust = model.groups['Robustness Test (robust)']; if (!real || !robust || !isNumber(real.fnr) || !isNumber(robust.fnr)) return null; return robust.fnr - real.fnr; } export function toolsRobustRows(selectedModels: readonly GuardModel[]) { return [...selectedModels] .filter( (model) => model.groups['Robustness Test (real)'] && model.groups['Robustness Test (robust)'] ) .sort((a, b) => toolsScoreDelta(a) - toolsScoreDelta(b)); } export function toolsRowsToCsv( selectedModels: readonly GuardModel[], activeGroups: readonly string[] ) { const rows = selectedModels.flatMap((model) => activeGroups.map((group) => { const cell = toolsCell(model, group); return [ model.name, group, cell?.score?.toFixed(4) ?? '', cell?.fnr?.toFixed(4) ?? '', cell?.fpr?.toFixed(4) ?? '', cell?.f1?.toFixed(4) ?? '' ]; }) ); return [['model', 'group', 'score', 'fnr', 'fpr', 'f1'], ...rows] .map((row) => row.map((cell) => `"${String(cell).replaceAll('"', '""')}"`).join(',')) .join('\n'); } export function toolsCsvDataUri( selectedModels: readonly GuardModel[], activeGroups: readonly string[] ) { return `data:text/csv;charset=utf-8,${encodeURIComponent(toolsRowsToCsv(selectedModels, activeGroups))}`; }