@@ -222,7 +222,8 @@ export default function App() {
222222 const [ cam , setCam ] = useUrlState ( 'c' , camParam )
223223 const [ camTarget , setCamTarget ] = useUrlState ( 'ct' , camTargetParam , { debounce : 500 } )
224224 const [ materialId , setMaterialId ] = useUrlState ( 'm' , stringParam ( DEFAULT_MP_ID ) , { push : true } )
225- const [ srcRole , setSrcRole ] = useUrlState ( 'src' , stringParam ( 'label' ) ) as [ 'input' | 'label' , ( v : 'input' | 'label' ) => void ]
225+ type SrcRole = 'input' | 'label' | 'diff'
226+ const [ srcRole , setSrcRole ] = useUrlState ( 'src' , stringParam ( 'label' ) ) as [ SrcRole , ( v : SrcRole ) => void ]
226227 const [ currentVolumeId , setCurrentVolumeIdRaw ] = useState < string | null > (
227228 ( ) => sessionStorage . getItem ( 'elvis-active-volume' ) ,
228229 )
@@ -503,22 +504,29 @@ export default function App() {
503504 defaultBindings : [ 'shift+z' ] ,
504505 handler : ( ) => setUseZarr ( ! useZarr ) ,
505506 } )
506- useAction ( 'data:toggle -src' , {
507- label : 'Toggle SAD input vs DFT label ' ,
508- description : 'Switch between SAD initial guess ( input) and converged DFT (label) density ' ,
509- keywords : [ 'input' , 'label' , 'sad' , 'dft' , 'source' , 'role' ] ,
507+ useAction ( 'data:cycle -src' , {
508+ label : 'Cycle source: label → input → diff ' ,
509+ description : 'Switch between DFT label, SAD input, and |label − input| difference ' ,
510+ keywords : [ 'input' , 'label' , 'diff' , ' sad', 'dft' , 'source' , 'role' ] ,
510511 group : 'Data' ,
511512 defaultBindings : [ 'i' ] ,
512513 handler : ( ) => {
513- const next : 'input' | 'label' = srcRole === 'input' ? 'label ' : 'input '
514+ const next : SrcRole = srcRole === 'label' ? 'input' : srcRole === 'input' ? 'diff ' : 'label '
514515 setSrcRole ( next )
515516 // The URL param `m` may be a material_id (mp-573119) OR a task_id (mp-1775579) —
516517 // ElectrAI S3 uses task IDs while the corpora manifest indexes by material ID.
517518 const record = MATERIALS_MANIFEST . records . find ( r =>
518519 r . id === materialId ||
519520 Object . values ( r . datasets ) . some ( d => d ?. task_ids ?. includes ( materialId ) ) ,
520521 )
521- if ( record ) {
522+ if ( ! record ) return
523+ if ( next === 'diff' ) {
524+ if ( ! useZarr ) {
525+ setFetchStatus ( 'Diff view requires Zarr mode (Shift+Z)' )
526+ return
527+ }
528+ loadDiff ( record )
529+ } else {
522530 const url = resolveLoadUrl ( record , next , useZarr ? 'zarr' : 'chgcar' )
523531 if ( url ) handleUrlSubmit ( url )
524532 }
@@ -1097,6 +1105,43 @@ export default function App() {
10971105 setFetchStatus ( null )
10981106 } , [ setCurrentVolumeId ] )
10991107
1108+ const loadDiff = useCallback ( async ( record : { id : string ; datasets : Record < string , { task_ids : string [ ] } | undefined > } ) => {
1109+ setUrlLoading ( true )
1110+ setFiles ( [ ] )
1111+ setFetchStatus ( 'Loading input + label for diff...' )
1112+ try {
1113+ const inputUrl = resolveLoadUrl ( record , 'input' , 'zarr' )
1114+ const labelUrl = resolveLoadUrl ( record , 'label' , 'zarr' )
1115+ if ( ! inputUrl || ! labelUrl ) {
1116+ setFetchStatus ( 'Diff requires both input and label Zarr URLs' )
1117+ return
1118+ }
1119+ const [ inp , lbl ] = await Promise . all ( [
1120+ fetchZarrVolume ( s3UriToHttps ( inputUrl ) ) ,
1121+ fetchZarrVolume ( s3UriToHttps ( labelUrl ) ) ,
1122+ ] )
1123+ const dInp = inp . grid . dims , dLbl = lbl . grid . dims
1124+ if ( dInp [ 0 ] !== dLbl [ 0 ] || dInp [ 1 ] !== dLbl [ 1 ] || dInp [ 2 ] !== dLbl [ 2 ] ) {
1125+ setFetchStatus ( `Diff dim mismatch: input ${ dInp . join ( '×' ) } vs label ${ dLbl . join ( '×' ) } ` )
1126+ return
1127+ }
1128+ const n = lbl . grid . data . length
1129+ const data = new Float32Array ( n )
1130+ for ( let i = 0 ; i < n ; i ++ ) data [ i ] = Math . abs ( lbl . grid . data [ i ] - inp . grid . data [ i ] )
1131+ const diff : VolumeData = {
1132+ ...lbl ,
1133+ title : `${ record . id } /diff` ,
1134+ grid : { dims : lbl . grid . dims , data } ,
1135+ }
1136+ handleLoad ( diff , `${ record . id } -diff` )
1137+ setFetchStatus ( null )
1138+ } catch ( e ) {
1139+ setFetchStatus ( `Diff failed: ${ e instanceof Error ? e . message : String ( e ) } ` )
1140+ } finally {
1141+ setUrlLoading ( false )
1142+ }
1143+ } , [ handleLoad ] )
1144+
11001145 const handleUrlSubmit = useCallback ( async ( url : string ) => {
11011146 setUrlLoading ( true )
11021147 setFetchStatus ( null )
@@ -1327,7 +1372,7 @@ export default function App() {
13271372 />
13281373 ) : (
13291374 < DensityViewer
1330- label = { srcRole === 'input' ? 'Input (SAD)' : 'Label (DFT)' }
1375+ label = { srcRole === 'input' ? 'Input (SAD)' : srcRole === 'diff' ? '|Label − Input|' : 'Label (DFT)' }
13311376 volume = { primaryFile . data }
13321377 isoLevel = { effectiveIsoLevel }
13331378 opacity = { opacity }
0 commit comments