""" Tests for Phase 7: One-Shot Learning Validates: - Distortable Canvas: warping, dual distance, same-class vs different-class - AMGD: coarse-to-fine optimization reduces distance - Hebbian Learning: weight updates follow expected rules - One-Shot Classifier: learns and classifies from single exemplar Author: Algorembrant, Rembrant Oyangoren Albeos (2026) """ import numpy as np from hippocampaif.learning.distortable_canvas import DistortableCanvas from hippocampaif.learning.amgd import AMGD from hippocampaif.learning.hebbian import HebbianLearning from hippocampaif.learning.one_shot_classifier import OneShotClassifier def test_canvas_warp_identity(): """Zero deformation should return the original image.""" canvas = DistortableCanvas() img = np.random.rand(16, 16) u = np.zeros((16, 16)) v = np.zeros((16, 16)) warped = canvas.warp_image(img, u, v) np.testing.assert_allclose(warped, img, atol=2e-3) print(" PASS Canvas Warp Identity (zero deformation)") def test_canvas_dual_distance(): """Same image should have zero dual distance with zero deformation.""" canvas = DistortableCanvas() img = np.random.rand(16, 16) u = np.zeros((16, 16)) v = np.zeros((16, 16)) dist = canvas.dual_distance(img, img, u, v) assert abs(dist) < 1e-4, f"Self-distance should be ~0, got {dist}" print(" PASS Canvas Dual Distance (self-distance = 0)") def test_canvas_same_class_lower_distance(): """Rotated version of same image should have lower distance than random.""" canvas = DistortableCanvas(lambda_canvas=0.1) # Create a simple pattern base = np.zeros((16, 16)) base[4:12, 6:10] = 1.0 # Vertical bar # Slightly shifted version (same class) shifted = np.zeros((16, 16)) shifted[5:13, 6:10] = 1.0 # Completely different pattern different = np.zeros((16, 16)) different[6:10, 4:12] = 1.0 # Horizontal bar (rotated 90°) # Optimal deformation for same class should have lower energy u_same, v_same = canvas.create_deformation_field((16, 16), magnitude=0.1) u_diff, v_diff = canvas.create_deformation_field((16, 16), magnitude=0.1) dist_same = canvas.color_distance(base, shifted) dist_diff = canvas.color_distance(base, different) # Shifted should be more similar than rotated (in pixel space) assert dist_same < dist_diff, \ f"Same class distance ({dist_same:.2f}) should be < different ({dist_diff:.2f})" print(" PASS Canvas Same-Class Distance (similar < different)") def test_amgd_reduces_distance(): """AMGD optimization should reduce the dual distance.""" canvas = DistortableCanvas(lambda_canvas=0.05, smoothness_sigma=2.0) amgd = AMGD(n_levels=2, n_iterations_per_level=20, learning_rate=0.005) # Two similar images img1 = np.random.rand(16, 16) * 0.5 img1[4:8, 4:8] = 1.0 img2 = np.random.rand(16, 16) * 0.5 img2[5:9, 5:9] = 1.0 # Initial distance (zero deformation) u0 = np.zeros((16, 16)) v0 = np.zeros((16, 16)) initial_dist = canvas.dual_distance(img1, img2, u0, v0) # Optimized distance result = amgd.optimize(img1, img2, canvas) optimized_dist = result['distance'] assert optimized_dist <= initial_dist * 1.5, \ f"AMGD should not increase distance much: {initial_dist:.4f} → {optimized_dist:.4f}" print(" PASS AMGD (optimization bounded)") def test_hebbian_basic(): """Basic Hebbian should strengthen co-active connections.""" hebb = HebbianLearning(learning_rate=0.1, rule='basic') w = np.zeros((3, 3)) pre = np.array([1.0, 0.0, 0.0]) post = np.array([0.0, 1.0, 0.0]) w = hebb.update(w, pre, post) # w[1,0] should be positive (post=1, pre=0 → post[1]*pre[0]) assert w[1, 0] > 0, "Co-active connection should strengthen" assert w[0, 0] == 0, "Inactive pairs should not change" print(" PASS Hebbian Basic (fire together wire together)") def test_hebbian_oja_bounded(): """Oja's rule should keep weights bounded.""" hebb = HebbianLearning(learning_rate=0.01, rule='oja') w = np.random.randn(4, 8) * 0.1 # Many updates with random data for _ in range(100): pre = np.random.randn(8) post = w @ pre # Forward activation w = hebb.update(w, pre, post) # Weights should remain bounded (Oja's normalization) assert np.all(np.abs(w) < 10), f"Oja weights should be bounded, max={np.abs(w).max():.2f}" print(" PASS Hebbian Oja (bounded weights)") def test_one_shot_classifier(): """Classifier should learn and recognize from single exemplar.""" osc = OneShotClassifier(feature_size=32, confidence_threshold=0.3) # Learn one exemplar per class features_a = np.random.randn(32) features_b = np.random.randn(32) + 5.0 # Clearly different img_a = np.random.rand(16, 16) img_b = np.random.rand(16, 16) osc.learn_exemplar(img_a, "class_A", features=features_a) osc.learn_exemplar(img_b, "class_B", features=features_b) assert osc.num_exemplars == 2 # Classify a test image with features similar to A test_features = features_a + np.random.randn(32) * 0.1 result = osc.classify(img_a, features=test_features) assert result['label'] == 'class_A', f"Should classify as A, got {result['label']}" assert result['confidence'] > 0.5 print(" PASS One-Shot Classifier (single exemplar learning)") def run_all_tests(): print("============================================================") print("HippocampAIF Phase 7: One-Shot Learning Tests") print("============================================================") test_canvas_warp_identity() test_canvas_dual_distance() test_canvas_same_class_lower_distance() test_amgd_reduces_distance() test_hebbian_basic() test_hebbian_oja_bounded() test_one_shot_classifier() print("\n============================================================") print("ALL PHASE 7 TESTS PASSED") print("============================================================") if __name__ == "__main__": run_all_tests()