@@ -1185,4 +1185,163 @@ mod tests {
11851185 assert ! ( accuracy > 1.0 / n_classes as f64 , "should beat random" ) ;
11861186 assert ! ( accuracy_compressed > 1.0 / n_classes as f64 , "compressed should beat random too" ) ;
11871187 }
1188+ #[ test]
1189+ #[ ignore]
1190+ fn test_hip_multi_object_detection ( ) {
1191+ // HIP bundles for multi-object detection:
1192+ // Given an image, detect if it contains features of MULTIPLE classes
1193+ // by unbinding one class archetype and checking residual against others.
1194+ //
1195+ // Bird/fence scenario: if unbind(image, bird) correlates with fence → both present.
1196+
1197+ let bytes = match std:: fs:: read ( "/tmp/tiny_imagenet_labeled.bin" ) {
1198+ Ok ( b) => b,
1199+ Err ( _) => { eprintln ! ( "SKIP: /tmp/tiny_imagenet_labeled.bin not found" ) ; return ; }
1200+ } ;
1201+
1202+ let n = u32:: from_le_bytes ( [ bytes[ 0 ] , bytes[ 1 ] , bytes[ 2 ] , bytes[ 3 ] ] ) as usize ;
1203+ let d = u32:: from_le_bytes ( [ bytes[ 4 ] , bytes[ 5 ] , bytes[ 6 ] , bytes[ 7 ] ] ) as usize ;
1204+ let n_classes = u32:: from_le_bytes ( [ bytes[ 8 ] , bytes[ 9 ] , bytes[ 10 ] , bytes[ 11 ] ] ) as usize ;
1205+
1206+ let mut labels = Vec :: with_capacity ( n) ;
1207+ for i in 0 ..n {
1208+ let off = 12 + i * 4 ;
1209+ labels. push ( u32:: from_le_bytes ( [ bytes[ off] , bytes[ off+1 ] , bytes[ off+2 ] , bytes[ off+3 ] ] ) as usize ) ;
1210+ }
1211+
1212+ let pixel_start = 12 + n * 4 ;
1213+ let img_w = 64usize ;
1214+ let img_h = 64usize ;
1215+ let ch = 3usize ;
1216+
1217+ // Extract grid-line features (768D)
1218+ let features: Vec < Vec < f64 > > = ( 0 ..n) . map ( |i| {
1219+ let v_start = pixel_start + i * d * 4 ;
1220+ let pixel = |r : usize , c : usize , channel : usize | -> f64 {
1221+ let off = v_start + ( r * img_w * ch + c * ch + channel) * 4 ;
1222+ f32:: from_le_bytes ( [ bytes[ off] , bytes[ off+1 ] , bytes[ off+2 ] , bytes[ off+3 ] ] ) as f64
1223+ } ;
1224+ let mut f = Vec :: with_capacity ( 768 ) ;
1225+ for & r in & [ img_h / 3 , 2 * img_h / 3 ] {
1226+ for c in 0 ..img_w { for channel in 0 ..ch { f. push ( pixel ( r, c, channel) ) ; } }
1227+ }
1228+ for & c in & [ img_w / 3 , 2 * img_w / 3 ] {
1229+ for r in 0 ..img_h { for channel in 0 ..ch { f. push ( pixel ( r, c, channel) ) ; } }
1230+ }
1231+ f
1232+ } ) . collect ( ) ;
1233+
1234+ let feat_d = features[ 0 ] . len ( ) ;
1235+
1236+ // ── Build HEEL archetypes per class ──
1237+ let mut archetypes: Vec < Vec < f64 > > = vec ! [ vec![ 0.0 ; feat_d] ; n_classes] ;
1238+ let mut counts = vec ! [ 0usize ; n_classes] ;
1239+ for ( i, & label) in labels. iter ( ) . enumerate ( ) {
1240+ for j in 0 ..feat_d { archetypes[ label] [ j] += features[ i] [ j] ; }
1241+ counts[ label] += 1 ;
1242+ }
1243+ for c in 0 ..n_classes {
1244+ if counts[ c] > 0 {
1245+ for j in 0 ..feat_d { archetypes[ c] [ j] /= counts[ c] as f64 ; }
1246+ }
1247+ }
1248+
1249+ // ── Cosine similarity helper ──
1250+ let cosine = |a : & [ f64 ] , b : & [ f64 ] | -> f64 {
1251+ let dot: f64 = a. iter ( ) . zip ( b) . map ( |( x, y) | x * y) . sum ( ) ;
1252+ let mag_a: f64 = a. iter ( ) . map ( |x| x * x) . sum :: < f64 > ( ) . sqrt ( ) ;
1253+ let mag_b: f64 = b. iter ( ) . map ( |x| x * x) . sum :: < f64 > ( ) . sqrt ( ) ;
1254+ if mag_a < 1e-10 || mag_b < 1e-10 { 0.0 } else { dot / ( mag_a * mag_b) }
1255+ } ;
1256+
1257+ // ── HIP: within-class variance (how spread is each class?) ──
1258+ let mut hip_variance = vec ! [ 0.0f64 ; n_classes] ;
1259+ for ( i, & label) in labels. iter ( ) . enumerate ( ) {
1260+ let dist: f64 = features[ i] . iter ( ) . zip ( & archetypes[ label] )
1261+ . map ( |( a, b) | ( a - b) * ( a - b) )
1262+ . sum :: < f64 > ( )
1263+ . sqrt ( ) ;
1264+ hip_variance[ label] += dist;
1265+ }
1266+ for c in 0 ..n_classes {
1267+ if counts[ c] > 0 { hip_variance[ c] /= counts[ c] as f64 ; }
1268+ }
1269+
1270+ // ── Multi-object simulation: "subtract" one class, check residual ──
1271+ // For each image, compute: residual = image_features - nearest_archetype
1272+ // Then check: does the residual correlate with ANY other archetype?
1273+ // High correlation → multi-object (or class confusion at the boundary)
1274+
1275+ let mut multi_object_candidates = Vec :: new ( ) ;
1276+ for ( i, & true_label) in labels. iter ( ) . enumerate ( ) {
1277+ // Subtract the true class archetype (simulates "removing" the primary object)
1278+ let residual: Vec < f64 > = features[ i] . iter ( ) . zip ( & archetypes[ true_label] )
1279+ . map ( |( a, b) | a - b)
1280+ . collect ( ) ;
1281+
1282+ // Check residual against all OTHER class archetypes
1283+ let mut best_other_class = 0 ;
1284+ let mut best_other_sim = f64:: NEG_INFINITY ;
1285+ for c in 0 ..n_classes {
1286+ if c == true_label || counts[ c] == 0 { continue ; }
1287+ let sim = cosine ( & residual, & archetypes[ c] ) ;
1288+ if sim > best_other_sim {
1289+ best_other_sim = sim;
1290+ best_other_class = c;
1291+ }
1292+ }
1293+
1294+ if best_other_sim > 0.3 {
1295+ multi_object_candidates. push ( ( i, true_label, best_other_class, best_other_sim) ) ;
1296+ }
1297+ }
1298+
1299+ // ── BRANCH: intersection features between class pairs ──
1300+ // For the top multi-object candidates, the residual IS the intersection
1301+ // features — what's left after removing the primary class IS the secondary class.
1302+ let mut pair_counts: std:: collections:: HashMap < ( usize , usize ) , usize > = std:: collections:: HashMap :: new ( ) ;
1303+ for & ( _, primary, secondary, _) in & multi_object_candidates {
1304+ let key = if primary < secondary { ( primary, secondary) } else { ( secondary, primary) } ;
1305+ * pair_counts. entry ( key) . or_insert ( 0 ) += 1 ;
1306+ }
1307+
1308+ // ── CHAODA: outliers are images that don't fit ANY archetype well ──
1309+ // (far from primary AND residual doesn't match secondary)
1310+ let mut outliers = Vec :: new ( ) ;
1311+ for ( i, & true_label) in labels. iter ( ) . enumerate ( ) {
1312+ let primary_dist: f64 = features[ i] . iter ( ) . zip ( & archetypes[ true_label] )
1313+ . map ( |( a, b) | ( a - b) * ( a - b) )
1314+ . sum :: < f64 > ( )
1315+ . sqrt ( ) ;
1316+
1317+ // If far from own class AND not detected as multi-object
1318+ let is_multi = multi_object_candidates. iter ( ) . any ( |& ( idx, _, _, _) | idx == i) ;
1319+ if primary_dist > hip_variance[ true_label] * 2.0 && !is_multi {
1320+ outliers. push ( ( i, true_label, primary_dist) ) ;
1321+ }
1322+ }
1323+
1324+ eprintln ! ( "=== HIP Multi-Object Detection ===" ) ;
1325+ eprintln ! ( " Images: {}, Classes: {}" , n, n_classes) ;
1326+ eprintln ! ( " Multi-object candidates (residual sim > 0.3): {}" , multi_object_candidates. len( ) ) ;
1327+ eprintln ! ( " Top class-pair intersections (BRANCH traversals):" ) ;
1328+ let mut pairs: Vec < _ > = pair_counts. iter ( ) . collect ( ) ;
1329+ pairs. sort_by ( |a, b| b. 1 . cmp ( a. 1 ) ) ;
1330+ for ( ( c1, c2) , count) in pairs. iter ( ) . take ( 5 ) {
1331+ eprintln ! ( " class {} × class {}: {} images share features" , c1, c2, count) ;
1332+ }
1333+ eprintln ! ( " CHAODA outliers (far from all archetypes): {}" , outliers. len( ) ) ;
1334+ for ( idx, label, dist) in outliers. iter ( ) . take ( 3 ) {
1335+ eprintln ! ( " image {} (class {}): dist={:.3} (>{:.3} threshold)" ,
1336+ idx, label, dist, hip_variance[ * label] * 2.0 ) ;
1337+ }
1338+ eprintln ! ( " Per-class HIP spread (intra-class variance):" ) ;
1339+ for c in 0 ..n_classes {
1340+ if counts[ c] > 0 {
1341+ eprintln ! ( " class {}: variance={:.3}, count={}" , c, hip_variance[ c] , counts[ c] ) ;
1342+ }
1343+ }
1344+
1345+ assert ! ( multi_object_candidates. len( ) > 0 , "should find some multi-object candidates" ) ;
1346+ }
11881347}
0 commit comments