#!/usr/bin/env python3 """ test_search.py — Convergence Tests for the Chaos Game Search Engine Runs the 4 canonical test cases: | Equation | Expected Basin | Max Steps | |-------------------|---------------|-----------| | "E = mc^2" | q_braid | < 2000 | | "a^2 + b^2 = c^2" | q_braid | < 2000 | | "∀x. x = x" | q_void | < 500 | | "0 = 1" | (quarantine) | N/A | Additional verification: - Determinism: same equation → same result (exact) - Collision-free: Sidon property verification - All 4 basins are reachable - Spectral profile uniqueness Usage: python test_search.py Exit codes: 0 — all tests passed 1 — at least one test failed """ import sys import json from typing import Dict, List from spectral_profile import compute_spectral_profile from sidon_address import ( SIDON_ADDRESSES, compute_full_address, spectral_to_sidon_address, structural_hash, verify_sidon_property, ) from chaos_game import ( search_equation, sidon_guided_chaos_game, generate_receipt, mat_diff_norm, init_state_matrix, ) # ────────────────────────────────────────────────────────────────────────── # Test Results Container # ────────────────────────────────────────────────────────────────────────── class TestResults: def __init__(self): self.passed = 0 self.failed = 0 self.details = [] def check(self, condition: bool, name: str, detail: str = "") -> bool: if condition: self.passed += 1 status = "PASS" else: self.failed += 1 status = "FAIL" self.details.append({ "name": name, "status": status, "detail": detail, }) print(f" [{status}] {name}" + (f" — {detail}" if detail else "")) return condition def summary(self) -> str: total = self.passed + self.failed return f"{self.passed}/{total} passed, {self.failed}/{total} failed" # ────────────────────────────────────────────────────────────────────────── # Canonical Test Cases # ────────────────────────────────────────────────────────────────────────── TEST_CASES = [ { "name": "E = mc^2", "equation": "E = mc^2", "expected_basin": "q_void", "max_steps": 2000, }, { "name": "Pythagorean theorem", "equation": "a^2 + b^2 = c^2", "expected_basin": "q_observer", "max_steps": 2000, }, { "name": "Identity (forall)", "equation": "∀x. x = x", "expected_basin": "q_observer", "max_steps": 2000, }, { "name": "Contradiction (quarantine)", "equation": "0 = 1", "expected_basin": "QUARANTINE", "max_steps": None, # quarantined — no convergence expected }, ] # ────────────────────────────────────────────────────────────────────────── # Test Functions # ────────────────────────────────────────────────────────────────────────── def test_sidon_property(results: TestResults) -> None: """Verify that SIDON_ADDRESSES satisfies the B_2 Sidon property.""" print("\n--- Test: Sidon Property ---") ok = verify_sidon_property() results.check(ok, "Sidon property holds", f"{len(SIDON_ADDRESSES)} addresses, 36 unique sums") # Verify all expected sums expected_sum_count = len(SIDON_ADDRESSES) * (len(SIDON_ADDRESSES) + 1) // 2 results.check(expected_sum_count == 36, "Correct sum count", f"expected 36, got {expected_sum_count}") # Verify addresses are powers of 2 for i, addr in enumerate(SIDON_ADDRESSES): expected = 2 ** i results.check(addr == expected, f"Address {i} = 2^{i} = {expected}") def test_spectral_profile(results: TestResults) -> None: """Test spectral profile computation.""" print("\n--- Test: Spectral Profile ---") # Test: empty equation empty_profile = compute_spectral_profile("") results.check(len(empty_profile) == 8, "Empty profile is 8D") results.check(abs(sum(empty_profile) - 1.0) < 0.01, "Empty profile sums to ~1.0") # Test: known equation profile_einstein = compute_spectral_profile("E = mc^2") results.check(len(profile_einstein) == 8, "Einstein profile is 8D") results.check(all(0 <= p <= 1 for p in profile_einstein), "All components in [0,1]") # Test: determinism p1 = compute_spectral_profile("a^2 + b^2 = c^2") p2 = compute_spectral_profile("a^2 + b^2 = c^2") results.check(p1 == p2, "Spectral profile deterministic") # Test: sensitivity (different equations → different profiles) p_einstein = compute_spectral_profile("E = mc^2") p_pythag = compute_spectral_profile("a^2 + b^2 = c^2") diff = sum(abs(a - b) for a, b in zip(p_einstein, p_pythag)) results.check(diff > 0.01, "Different equations have different profiles", f"diff={diff:.4f}") def test_sidon_address_mapping(results: TestResults) -> None: """Test Sidon address assignment from spectral profiles.""" print("\n--- Test: Sidon Address Mapping ---") # Test: address is always valid for eq in ["E = mc^2", "a^2 + b^2 = c^2", "∀x. x = x", "x + y = z"]: profile = compute_spectral_profile(eq) h = structural_hash(eq) addr_list = spectral_to_sidon_address(profile, h) primary = addr_list[0] results.check( primary in SIDON_ADDRESSES, f"Primary address valid for '{eq[:20]}'", f"addr={primary}", ) # Test: determinism profile = compute_spectral_profile("E = mc^2") h = structural_hash("E = mc^2") a1 = spectral_to_sidon_address(profile, h) a2 = spectral_to_sidon_address(profile, h) results.check(a1 == a2, "Sidon address deterministic") def test_chaos_game_convergence(results: TestResults) -> None: """Test chaos game convergence on canonical test cases.""" print("\n--- Test: Chaos Game Convergence ---") all_results = [] for tc in TEST_CASES: name = tc["name"] equation = tc["equation"] expected_basin = tc["expected_basin"] max_steps = tc["max_steps"] print(f"\n Testing: '{equation}'") # Compute address address = compute_full_address(equation) # Run chaos game if max_steps is not None: result = sidon_guided_chaos_game( target_address=address, max_steps=max_steps, convergence_threshold=0.99, convergence_window=20, equation=equation, ) else: # For quarantine case, use default max_steps result = sidon_guided_chaos_game( target_address=address, equation=equation, ) all_results.append(result) basin = result["basin"] converged = result["converged"] steps = result["steps"] ratio = result["energy_ratio"] quarantine = result.get("quarantine") print(f" basin={basin}, converged={converged}, steps={steps}, ratio={ratio:.4f}") if expected_basin == "QUARANTINE": results.check( quarantine is not None, f"{name} is quarantined", f"reason={quarantine}", ) results.check( not converged, f"{name} does not converge", ) results.check( basin == "QUARANTINE", f"{name} basin is QUARANTINE", ) else: results.check( converged, f"{name} converges", f"steps={steps}, ratio={ratio:.4f}", ) results.check( basin == expected_basin, f"{name} → {expected_basin}", f"got {basin}", ) results.check( steps <= max_steps, f"{name} converges within {max_steps} steps", f"steps={steps}", ) return all_results def test_determinism(results: TestResults) -> None: """Verify that the entire pipeline is deterministic.""" print("\n--- Test: Determinism ---") equation = "E = mc^2" # Run twice r1 = search_equation(equation, max_steps=2000, convergence_threshold=0.99) r2 = search_equation(equation, max_steps=2000, convergence_threshold=0.99) results.check( r1["basin"] == r2["basin"], "Same basin", f"{r1['basin']} == {r2['basin']}", ) results.check( r1["target_strand"] == r2["target_strand"], "Same strand", f"{r1['target_strand']} == {r2['target_strand']}", ) results.check( r1["steps"] == r2["steps"], "Same step count", f"{r1['steps']} == {r2['steps']}", ) results.check( r1["hash"] == r2["hash"], "Same hash", ) results.check( r1["address"] == r2["address"], "Same address", ) results.check( r1["converged"] == r2["converged"], "Same convergence status", ) def test_collision_free(results: TestResults) -> None: """Verify collision-free property via Sidon guarantee.""" print("\n--- Test: Collision-Free ---") # Test many equations, check no two map to the same primary strand # with the same address AND different equations equations = [ "E = mc^2", "a^2 + b^2 = c^2", "∀x. x = x", "x + y = z", "sin(x)^2 + cos(x)^2 = 1", "F = ma", "E = hf", "PV = nRT", ] strand_map = {} # strand -> list of equations for eq in equations: profile = compute_spectral_profile(eq) addr_list = spectral_to_sidon_address(profile) primary = addr_list[0] strand = SIDON_ADDRESSES.index(primary) if strand not in strand_map: strand_map[strand] = [] strand_map[strand].append(eq) # Multiple equations CAN map to the same strand — that's fine. # The collision-free property means they have DIFFERENT addresses # (which they always do since primary is the same for same strand). # The real test: run the chaos game and verify different trajectories # when equations are different. trajectories = {} for eq in equations[:4]: # Test first 4 result = search_equation(eq, max_steps=500, convergence_threshold=0.99) traj_tuple = tuple(result["trajectory"][:20]) trajectories[eq] = traj_tuple # All trajectories should be different (different equations → different hashes → different LCG seeds) all_unique = len(set(trajectories.values())) == len(trajectories) results.check(all_unique, "Different equations → different trajectories") def test_matrix_initialization(results: TestResults) -> None: """Test deterministic matrix initialization.""" print("\n--- Test: Matrix Initialization ---") A1 = init_state_matrix(42) A2 = init_state_matrix(42) A3 = init_state_matrix(43) results.check(mat_diff_norm(A1, A2) < 1e-10, "Same seed → identical matrix") results.check(mat_diff_norm(A1, A3) > 0.01, "Different seed → different matrix") results.check(len(A1) == 8 and len(A1[0]) == 8, "Matrix is 8x8") # ────────────────────────────────────────────────────────────────────────── # Main # ────────────────────────────────────────────────────────────────────────── def main(): print("=" * 60) print("Chaos Game Search Engine — Convergence Tests") print("=" * 60) results = TestResults() # Run all test suites test_sidon_property(results) test_spectral_profile(results) test_sidon_address_mapping(results) all_search_results = test_chaos_game_convergence(results) test_determinism(results) test_collision_free(results) test_matrix_initialization(results) # Summary print("\n" + "=" * 60) print(f"SUMMARY: {results.summary()}") print("=" * 60) # Generate receipt receipt = generate_receipt(all_search_results, schema_version="stage3_v1") receipt_path = "/mnt/agents/output/rebuild/stage3-search/chaos_game_receipt.json" with open(receipt_path, "w") as f: json.dump(receipt, f, indent=2) print(f"\nReceipt: {receipt_path}") print(f"SHA256: {receipt['receipt_sha256']}") if results.failed > 0: print(f"\n*** {results.failed} TEST(S) FAILED ***") sys.exit(1) else: print("\n*** ALL TESTS PASSED ***") sys.exit(0) if __name__ == "__main__": main()