diff --git a/admin/app/services/benchmark_service.ts b/admin/app/services/benchmark_service.ts index 87042c7..077e4d8 100644 --- a/admin/app/services/benchmark_service.ts +++ b/admin/app/services/benchmark_service.ts @@ -429,9 +429,16 @@ export class BenchmarkService { } } - // Calculate NOMAD score + // Calculate NOMAD score. Only pass systemScores when the system benchmarks + // actually ran, so an AI-only run renormalizes to just the AI weights + // (an AI-only pass with default-zero system scores would otherwise be + // scaled against the full NOMAD 100). this._updateStatus('calculating_score', 'Calculating NOMAD score...') - const nomadScore = this._calculateNomadScore(systemScores, aiScores) + const systemMeasured = type === 'full' || type === 'system' + const nomadScore = this._calculateNomadScore( + systemMeasured ? systemScores : null, + aiScores + ) // Save result const result = await BenchmarkResult.create({ @@ -749,23 +756,34 @@ export class BenchmarkService { /** * Calculate weighted NOMAD score */ - private _calculateNomadScore(systemScores: SystemScores, aiScores: Partial): number { + private _calculateNomadScore( + systemScores: SystemScores | null, + aiScores: Partial + ): number { let totalWeight = 0 let weightedSum = 0 - // CPU score - weightedSum += systemScores.cpu_score * SCORE_WEIGHTS.cpu - totalWeight += SCORE_WEIGHTS.cpu + // System scores (only when the system benchmarks actually ran). Passing null + // for an AI-only run keeps the system weights OUT of the denominator so the + // score renormalizes to just the AI portion's 0-100 range, rather than being + // scaled against the full NOMAD 100 (where an excellent AI setup would cap + // at ~40). System-only already renormalizes correctly because aiScores is + // empty and its weights are likewise skipped below. + if (systemScores) { + // CPU score + weightedSum += systemScores.cpu_score * SCORE_WEIGHTS.cpu + totalWeight += SCORE_WEIGHTS.cpu - // Memory score - weightedSum += systemScores.memory_score * SCORE_WEIGHTS.memory - totalWeight += SCORE_WEIGHTS.memory + // Memory score + weightedSum += systemScores.memory_score * SCORE_WEIGHTS.memory + totalWeight += SCORE_WEIGHTS.memory - // Disk scores - weightedSum += systemScores.disk_read_score * SCORE_WEIGHTS.disk_read - totalWeight += SCORE_WEIGHTS.disk_read - weightedSum += systemScores.disk_write_score * SCORE_WEIGHTS.disk_write - totalWeight += SCORE_WEIGHTS.disk_write + // Disk scores + weightedSum += systemScores.disk_read_score * SCORE_WEIGHTS.disk_read + totalWeight += SCORE_WEIGHTS.disk_read + weightedSum += systemScores.disk_write_score * SCORE_WEIGHTS.disk_write + totalWeight += SCORE_WEIGHTS.disk_write + } // AI scores (if available) if (aiScores.ai_tokens_per_second !== undefined && aiScores.ai_tokens_per_second !== null) { diff --git a/admin/inertia/components/benchmark/ScoreReveal.tsx b/admin/inertia/components/benchmark/ScoreReveal.tsx index 7c3b5e4..590c8cd 100644 --- a/admin/inertia/components/benchmark/ScoreReveal.tsx +++ b/admin/inertia/components/benchmark/ScoreReveal.tsx @@ -2,6 +2,7 @@ import { useEffect, useRef, useState } from 'react' import { IconChartBar, IconCpu, IconDatabase, IconRobot, IconServer } from '@tabler/icons-react' import CircularGauge from '~/components/systeminfo/CircularGauge' import StyledButton from '~/components/StyledButton' +import { getScoreDisplay } from '~/lib/benchmarkScore' import BenchmarkResult from '#models/benchmark_result' interface ScoreRevealProps { @@ -52,6 +53,8 @@ export default function ScoreReveal({ onDone, }: ScoreRevealProps) { const displayScore = useCountUp(result.nomad_score) + // A partial (System/AI Only) run is not the NOMAD Score -- relabel + flag it. + const scoreInfo = getScoreDisplay(result.benchmark_type) // Sub-score gauges, mirroring the System Performance / AI Performance grids. const gauges: { @@ -104,6 +107,11 @@ export default function ScoreReveal({
Report + {scoreInfo.isPartial && ( + + Partial + + )}
@@ -111,21 +119,26 @@ export default function ScoreReveal({
} />
{displayScore.toFixed(1)}

- Your NOMAD Score is a weighted composite of all benchmark results. + {scoreInfo.isPartial + ? scoreInfo.cta + : 'Your NOMAD Score is a weighted composite of all benchmark results.'}

diff --git a/admin/inertia/components/systeminfo/CircularGauge.tsx b/admin/inertia/components/systeminfo/CircularGauge.tsx index 2be7c47..31091e9 100644 --- a/admin/inertia/components/systeminfo/CircularGauge.tsx +++ b/admin/inertia/components/systeminfo/CircularGauge.tsx @@ -9,6 +9,8 @@ interface CircularGaugeProps { variant?: 'cpu' | 'memory' | 'disk' | 'default' subtext?: string animated?: boolean + /** Render the ring in a neutral tone (e.g. for a partial, non-NOMAD score). */ + muted?: boolean } export default function CircularGauge({ @@ -19,6 +21,7 @@ export default function CircularGauge({ variant = 'default', subtext, animated = true, + muted = false, }: CircularGaugeProps) { const [animatedValue, setAnimatedValue] = useState(animated ? 0 : value) @@ -61,6 +64,8 @@ export default function CircularGauge({ const offset = circumference - (displayValue / 100) * circumference const getColor = () => { + // Neutral tone signals this isn't a real NOMAD Score (partial run). + if (muted) return 'desert-stone' // For benchmarks: higher scores = better = green if (value >= 75) return 'desert-green' if (value >= 50) return 'desert-olive' diff --git a/admin/inertia/lib/benchmarkScore.ts b/admin/inertia/lib/benchmarkScore.ts new file mode 100644 index 0000000..18e75d3 --- /dev/null +++ b/admin/inertia/lib/benchmarkScore.ts @@ -0,0 +1,33 @@ +import type { BenchmarkType } from '../../types/benchmark' + +/** + * How a benchmark result's headline score should be presented. A partial run + * (System Only / AI Only) is NOT the NOMAD Score -- the NOMAD Score is the full + * benchmark composite -- so partial results are relabelled and flagged to avoid + * users mistaking a partial number for their NOMAD Score. The stored score for + * a partial run is already renormalized to that category's own 0-100 range + * (see BenchmarkService._calculateNomadScore). + */ +export function getScoreDisplay(type: BenchmarkType): { + label: string + isPartial: boolean + /** Which full benchmark to run to get the real NOMAD Score, for the CTA copy. */ + cta: string +} { + switch (type) { + case 'system': + return { + label: 'System Score', + isPartial: true, + cta: 'This is a partial result, not your NOMAD Score. Run a Full Benchmark to get your NOMAD Score.', + } + case 'ai': + return { + label: 'AI Score', + isPartial: true, + cta: 'This is a partial result, not your NOMAD Score. Run a Full Benchmark to get your NOMAD Score.', + } + default: + return { label: 'NOMAD Score', isPartial: false, cta: '' } + } +} diff --git a/admin/inertia/pages/settings/benchmark.tsx b/admin/inertia/pages/settings/benchmark.tsx index 12e4ede..feef628 100644 --- a/admin/inertia/pages/settings/benchmark.tsx +++ b/admin/inertia/pages/settings/benchmark.tsx @@ -25,6 +25,7 @@ import { SERVICE_NAMES } from '../../../constants/service_names' import { useBenchmarkRun } from '~/hooks/useBenchmarkRun' import BenchmarkRunView from '~/components/benchmark/BenchmarkRunView' import ScoreReveal from '~/components/benchmark/ScoreReveal' +import { getScoreDisplay } from '~/lib/benchmarkScore' export default function BenchmarkPage(props: { benchmark: { @@ -175,6 +176,10 @@ export default function BenchmarkPage(props: { latestResult.ai_tokens_per_second > 0 && !latestResult.submitted_to_repository + // How to present the headline score: partial (System/AI Only) runs are NOT the + // NOMAD Score and are relabelled + flagged so users don't mistake them for it. + const scoreInfo = latestResult ? getScoreDisplay(latestResult.benchmark_type) : null + // Handle Full Benchmark click with pre-flight check const handleFullBenchmarkClick = () => { if (!aiInstalled) { @@ -330,7 +335,12 @@ export default function BenchmarkPage(props: {

- NOMAD Score + {scoreInfo?.label ?? 'NOMAD Score'} + {scoreInfo?.isPartial && ( + + Partial + + )}

@@ -338,21 +348,35 @@ export default function BenchmarkPage(props: {
} />
-
- {latestResult.nomad_score.toFixed(1)} +
+
+ {latestResult.nomad_score.toFixed(1)} +
+ {scoreInfo?.isPartial && ( + + Partial + + )}

- Your NOMAD Score is a weighted composite of all benchmark results. + {scoreInfo?.isPartial + ? scoreInfo.cta + : 'Your NOMAD Score is a weighted composite of all benchmark results.'}

{/* Share with Community - Only for full benchmarks with AI data */}