Skip to content

Commit b093888

Browse files
committed
Diff mode: voxel stats (+/- counts, net sum) under heatmap histogram
Adds a small 3-column readout below the heatmap cutoff scrubber in diff mode: count sum + voxels 12,345 +123.4 − voxels 11,987 -120.1 net 358 +3.3 The sum-net (Σpos + Σneg) is near zero when the model conserves charge globally; a sustained nonzero net flags a systematic bias. Voxel counts are computed in the same single pass as \`maxDensity\` / \`dataAbsMax\`, so no perf hit. Hidden outside diff mode (where signed math is moot).
1 parent 4338120 commit b093888

2 files changed

Lines changed: 34 additions & 3 deletions

File tree

pkgs/core/src/components/Controls.tsx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,10 @@ interface ControlsProps {
107107
/** Symmetric absolute-max for the heatmap histogram x-axis. Defaults to
108108
* `maxDensity` (unsigned data); diff mode passes `max(|min|, |max|)`. */
109109
dataAbsMax?: number
110+
/** Signed-volume voxel statistics, surfaced under the heatmap histogram in
111+
* diff mode. `sumPos + sumNeg` (a near-zero number for a well-conserved
112+
* diff) is shown as "net". */
113+
voxelStats?: { posCount: number; negCount: number; sumPos: number; sumNeg: number }
110114
heatmapStepCount?: number
111115
onHeatmapStepCountChange?: (v: number) => void
112116
/** Zarr data-source toggle (prefer chunked multi-res zarr where available). */
@@ -202,6 +206,7 @@ export function Controls({
202206
heatmapCutoffAnim,
203207
onHeatmapCutoffAnimChange,
204208
dataAbsMax,
209+
voxelStats,
205210
heatmapStepCount = 256,
206211
onHeatmapStepCountChange,
207212
useZarr,
@@ -500,6 +505,27 @@ export function Controls({
500505
Animate cutoff changes
501506
</label>
502507
)}
508+
{isDiff && voxelStats && (
509+
<div
510+
title="Voxel-level sums for the signed diff. Net (≈ sumPos + sumNeg) should be near zero when the model conserves charge globally."
511+
style={{
512+
display: 'grid', gridTemplateColumns: 'auto 1fr auto', columnGap: 8, rowGap: 2,
513+
fontSize: 11, color: '#aaa', fontVariantNumeric: 'tabular-nums',
514+
padding: '4px 6px', marginTop: 4,
515+
background: '#1a1a2a', border: '1px solid #2a2a3e', borderRadius: 3,
516+
}}
517+
>
518+
<span style={{ color: '#5be080' }}>+ voxels</span>
519+
<span style={{ textAlign: 'right' }}>{voxelStats.posCount.toLocaleString()}</span>
520+
<span style={{ color: '#5be080', textAlign: 'right' }}>+{voxelStats.sumPos.toFixed(1)}</span>
521+
<span style={{ color: '#ff5040' }}>− voxels</span>
522+
<span style={{ textAlign: 'right' }}>{voxelStats.negCount.toLocaleString()}</span>
523+
<span style={{ color: '#ff5040', textAlign: 'right' }}>{voxelStats.sumNeg.toFixed(1)}</span>
524+
<span>net</span>
525+
<span style={{ textAlign: 'right' }}>{(voxelStats.posCount - voxelStats.negCount).toLocaleString()}</span>
526+
<span style={{ textAlign: 'right' }}>{((voxelStats.sumPos + voxelStats.sumNeg) >= 0 ? '+' : '') + (voxelStats.sumPos + voxelStats.sumNeg).toFixed(1)}</span>
527+
</div>
528+
)}
503529
<div className={styles.controlLabel}>
504530
<div className={styles.sliderHeader}>
505531
<span title="Ray-march sample count — higher is smoother but slower">Steps: {heatmapStepCount}</span>

pkgs/static/src/App.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1529,18 +1529,22 @@ export default function App() {
15291529
e.target.value = ''
15301530
}, [])
15311531

1532-
const { maxDensity, dataAbsMax } = useMemo(() => {
1533-
if (!primaryFile) return { maxDensity: 1, dataAbsMax: 1 }
1532+
const { maxDensity, dataAbsMax, posCount, negCount, sumPos, sumNeg } = useMemo(() => {
1533+
if (!primaryFile) return { maxDensity: 1, dataAbsMax: 1, posCount: 0, negCount: 0, sumPos: 0, sumNeg: 0 }
15341534
let max = 0
15351535
let absMax = 0
1536+
let posCount = 0, negCount = 0
1537+
let sumPos = 0, sumNeg = 0
15361538
const arr = primaryFile.data.grid.data
15371539
for (let i = 0; i < arr.length; i++) {
15381540
const v = arr[i]
15391541
if (v > max) max = v
15401542
const a = Math.abs(v)
15411543
if (a > absMax) absMax = a
1544+
if (v > 0) { posCount++; sumPos += v }
1545+
else if (v < 0) { negCount++; sumNeg += v }
15421546
}
1543-
return { maxDensity: max, dataAbsMax: absMax || 1 }
1547+
return { maxDensity: max, dataAbsMax: absMax || 1, posCount, negCount, sumPos, sumNeg }
15441548
}, [primaryFile])
15451549

15461550
const densityQuantiles = useMemo(() => {
@@ -1986,6 +1990,7 @@ export default function App() {
19861990
heatmapCutoffAnim={settings.heatmapCutoffAnim}
19871991
onHeatmapCutoffAnimChange={v => updateSettings({ heatmapCutoffAnim: v })}
19881992
dataAbsMax={dataAbsMax}
1993+
voxelStats={srcRole === 'diff' ? { posCount, negCount, sumPos, sumNeg } : undefined}
19891994
heatmapStepCount={heatmapStepCount}
19901995
onHeatmapStepCountChange={setHeatmapStepCount}
19911996
heatmapOpacity={heatmapOpacity}

0 commit comments

Comments
 (0)