diff --git a/build.zig.zon b/build.zig.zon index 23d425b..b109ac5 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -7,7 +7,7 @@ .dependencies = .{ .zig_golden_float = .{ .url = "https://github.com/gHashTag/zig-golden-float/archive/main.tar.gz", - .hash = "golden_float-2.1.0-h7LKhQ3lCgBfkjryIT9MnOTWleMQrdrgOIxQOhxqzTVu", + .hash = "golden_float-2.1.0-h7LKhZMGCwBW0FS_zsli6CWxs9d5pElRE1pFFiy3WRSD", }, }, } diff --git a/src/vsa.zig b/src/vsa.zig index 747da43..390eee9 100644 --- a/src/vsa.zig +++ b/src/vsa.zig @@ -93,7 +93,12 @@ pub const TextCorpus = storage.TextCorpus; // Re-export concurrency & DAG pub const ChaseLevDeque = concurrency.ChaseLevDeque; -pub const LockFreePool = concurrency.LockFreePool; +// LockFreePool is not re-exported. It has never existed in this repository's +// concurrency module, nor in golden-float's -- both export the same +// twenty-three names and that is not one of them. The line sat here referring +// to nothing, and nothing complained, because lazy analysis never asked what it +// pointed at. Forcing the whole surface through the compiler asked, and the +// answer was that it points at nothing. pub const DependencyGraph = concurrency.DependencyGraph; pub const TaskNode = concurrency.TaskNode; pub const TaskState = concurrency.TaskState; @@ -144,3 +149,17 @@ test { } // φ² + 1/φ² = 3 | TRINITY + +test "every public declaration of this module is analysed" { + // Zig analyses top-level declarations lazily, so `zig build test` proves only + // that the declarations the tests happen to reference compile. A consumer + // referencing anything else got errors this package's own green CI could not + // see -- which is how five API-drift errors sat here while the badge stayed + // green, and how gHashTag/trinity#701 found them within a minute of trying + // to depend on this. + // + // With the duplicated files now re-exporting one repaired implementation, + // this is what proves the whole surface goes through the compiler rather + // than only the part the tests walk. + @import("std").testing.refAllDeclsRecursive(@This()); +} diff --git a/src/vsa/10k_vsa.zig b/src/vsa/10k_vsa.zig index 4453139..3e28efa 100644 --- a/src/vsa/10k_vsa.zig +++ b/src/vsa/10k_vsa.zig @@ -1,454 +1,29 @@ -// ╔════════════════════════════════════════════════════════════════════════════╗ -// ║ TRINITY VSA — 10K-DIMENSIONAL HYPERVECTORS ║ -// ║ Week 2 Day 1: Scalable VSA architecture for 10,000-dimensional vectors ║ -// ║ ║ -// ║ Features: ║ -// ║ - 10,000-dimensional ternary hypervectors ║ -// ║ - O(1) parallel bind operation ║ -// ║ - Bundle, similarity, permutation ║ -// ║ - FPGA-ready memory layout ║ -// ║ ║ -// ║ φ² + 1/φ² = 3 = TRINITY ║ -// ╚════════════════════════════════════════════════════════════════════════════╝ - -const std = @import("std"); -const builtin = @import("builtin"); -const common = @import("common.zig"); - -pub const Trit = common.Trit; // i8: -1, 0, +1 -pub const HybridBigInt = common.HybridBigInt; - -//========================================================================== -// CONSTANTS -//========================================================================== - -pub const DIM_10K = 10_000; -pub const BYTES_PER_10K = (DIM_10K * 2 + 7) / 8; // 20,000 bits = 2,500 bytes -pub const WORDS_32BIT = (DIM_10K * 2 + 31) / 32; // 625 words of 32 bits - -// FPGA BRAM sizing (32Kb = 4096 bytes) -pub const BRAM_SIZE = 4096; -pub const VECTORS_PER_BRAM = BRAM_SIZE / BYTES_PER_10K; // ~1.6 vectors - -// Trit values (matching HybridBigInt convention) -pub const TRIT_NEG: Trit = -1; -pub const TRIT_ZERO: Trit = 0; -pub const TRIT_POS: Trit = 1; - -//========================================================================== -// 10K-DIMENSIONAL HYPERVECTOR -//========================================================================== - -/// 10K-dimensional ternary hypervector -/// Storage: 2,500 bytes (20,000 bits) using packed trit encoding -pub const HyperVector10K = struct { - /// Packed trit storage (2 bits per trit) - /// Layout: [trit0:1, trit0:0][trit1:1, trit1:0]... - data: [BYTES_PER_10K]u8, - - const Self = @This(); - - /// Create a zero vector - pub inline fn zero() Self { - return .{ .data = [_]u8{0} ** BYTES_PER_10K }; - } - - /// Create a random vector - pub fn random(rng: *std.Random.DefaultPrng) !Self { - var self = Self.zero(); - var i: usize = 0; - while (i < DIM_10K) : (i += 1) { - const rand_val = rng.random().int(u3); - const trit_val: i8 = switch (rand_val & 0x03) { - 0 => TRIT_ZERO, - 1 => TRIT_POS, - 2 => TRIT_NEG, - else => TRIT_POS, - }; - try self.set(i, trit_val); - } - return self; - } - - /// Get trit at index (returns {-1, 0, +1}) - pub inline fn get(self: *const Self, index: usize) !Trit { - if (index >= DIM_10K) return error.IndexOutOfBounds; - const bit_idx = index * 2; - const byte_idx = bit_idx / 8; - const shift: u3 = @intCast(bit_idx % 8); - const trit_bits = (self.data[byte_idx] >> shift) & 0x03; - - return switch (trit_bits) { - 0b00 => TRIT_ZERO, - 0b01 => TRIT_POS, - 0b10 => TRIT_NEG, - else => TRIT_ZERO, // Invalid encoding - }; - } - - /// Set trit at index - pub inline fn set(self: *Self, index: usize, value: Trit) !void { - if (index >= DIM_10K) return error.IndexOutOfBounds; - const bit_idx = index * 2; - const byte_idx = bit_idx / 8; - const shift: u3 = @intCast(bit_idx % 8); - - const trit_bits: u2 = switch (value) { - TRIT_ZERO => 0b00, - TRIT_POS => 0b01, - TRIT_NEG => 0b10, - else => 0b00, - }; - - // Clear old bits and set new ones - self.data[byte_idx] &= ~(@as(u8, 0x03) << shift); - self.data[byte_idx] |= @as(u8, trit_bits) << shift; - } - - /// Parallel bind operation (O(1) on FPGA with 10,000 LUTs) - /// result[i] = a[i] * b[i] - pub fn bind(a: *const Self, b: *const Self) Self { - var result = Self.zero(); - - // Process 16 trits (32 bits) at a time for SIMD efficiency - const word_count = WORDS_32BIT; - var w: usize = 0; - - while (w < word_count) : (w += 1) { - const byte_idx = w * 4; - if (byte_idx + 4 > BYTES_PER_10K) break; - - // Load 32 bits (16 trits) from each vector - const a_words = std.mem.readInt(u32, a.data[byte_idx..][0..4], .little); - const b_words = std.mem.readInt(u32, b.data[byte_idx..][0..4], .little); - - var result_word: u32 = 0; - var t: usize = 0; - - // Trit-wise multiplication (16 parallel operations) - while (t < 16) : (t += 1) { - const shift_amt: u5 = @intCast(t * 2); - const a_trit: u2 = @truncate((a_words >> shift_amt) & 0x03); - const b_trit: u2 = @truncate((b_words >> shift_amt) & 0x03); - - const r_trit: u2 = tritMul(a_trit, b_trit); - result_word |= @as(u32, r_trit) << shift_amt; - } - - // Store result - std.mem.writeInt(u32, result.data[byte_idx..][0..4], result_word, .little); - } - - return result; - } - - /// Bundle operation (majority vote) - pub fn bundle(a: *const Self, b: *const Self) !Self { - var result = Self.zero(); - - var i: usize = 0; - while (i < DIM_10K) : (i += 1) { - const a_trit = try a.get(i); - const b_trit = try b.get(i); - - const r_trit: Trit = tritBundle(a_trit, b_trit); - try result.set(i, r_trit); - } - - return result; - } - - /// Cosine similarity (scaled to 0-65535) - pub fn cosineSimilarity(a: *const Self, b: *const Self) !u16 { - var dot_product: i64 = 0; - var norm_a: i64 = 0; - var norm_b: i64 = 0; - - var i: usize = 0; - while (i < DIM_10K) : (i += 1) { - const a_trit = try a.get(i); - const b_trit = try b.get(i); - - dot_product += @as(i64, a_trit) * @as(i64, b_trit); - norm_a += @as(i64, a_trit) * @as(i64, a_trit); - norm_b += @as(i64, b_trit) * @as(i64, b_trit); - } - - if (norm_a == 0 or norm_b == 0) - return 0; - - const norm_sum = norm_a + norm_b; - const abs_dot = @abs(dot_product); - const scaled = @as(u64, @intCast(abs_dot)) * 65535 / @as(u64, @intCast(norm_sum)); - - return @intCast(scaled); - } - - /// Permutation (cyclic shift) - pub fn permute(self: *const Self, shift: u16) !Self { - var result = Self.zero(); - const effective_shift = @as(usize, @intCast(shift)) % DIM_10K; - - var i: usize = 0; - while (i < DIM_10K) : (i += 1) { - const src_idx = (i + DIM_10K - effective_shift) % DIM_10K; - const trit = try self.get(src_idx); - try result.set(i, trit); - } - - return result; - } - - /// Count non-zero trits - pub fn countNonZero(self: *const Self) !usize { - var count: usize = 0; - var i: usize = 0; - while (i < DIM_10K) : (i += 1) { - if (try self.get(i) != TRIT_ZERO) - count += 1; - } - return count; - } - - /// Convert to slice of 32-bit words (for FPGA transfer) - pub fn toWords(self: *const Self) [WORDS_32BIT]u32 { - var result: [WORDS_32BIT]u32 = undefined; - var i: usize = 0; - while (i < WORDS_32BIT) : (i += 1) { - const byte_idx = i * 4; - if (byte_idx + 4 <= BYTES_PER_10K) { - result[i] = std.mem.readInt(u32, self.data[byte_idx..][0..4], .little); - } else { - result[i] = 0; - } - } - return result; - } - - /// Create from slice of 32-bit words (from FPGA) - pub fn fromWords(words: []const u32) Self { - var result = Self.zero(); - var i: usize = 0; - while (i < @min(WORDS_32BIT, words.len)) : (i += 1) { - const byte_idx = i * 4; - if (byte_idx + 4 <= BYTES_PER_10K) { - std.mem.writeInt(u32, result.data[byte_idx..][0..4], words[i], .little); - } - } - return result; - } - - /// Format as hex string - pub fn formatHex(self: *const Self, allocator: std.mem.Allocator) ![]u8 { - return std.fmt.allocPrint(allocator, "{s}", .{std.fmt.fmtSliceHexLower(&self.data)}); - } -}; - -/// Trit multiplication lookup table (combinational logic) -inline fn tritMul(a: u2, b: u2) u2 { - return if (a == 0 or b == 0) 0 else if (a == b) 1 else 2; -} - -/// Trit bundle (majority vote of 2) -inline fn tritBundle(a: Trit, b: Trit) Trit { - if (a == TRIT_NEG) { - return if (b == TRIT_NEG) TRIT_NEG else if (b == TRIT_POS) TRIT_ZERO else TRIT_NEG; - } else if (a == TRIT_POS) { - return if (b == TRIT_NEG) TRIT_ZERO else if (b == TRIT_POS) TRIT_POS else TRIT_POS; - } else { // a == ZERO - return b; - } -} - -//========================================================================== -// BENCHMARK FUNCTIONS -//========================================================================== - -pub const BenchmarkResult = struct { - bind_ns: f64, - bundle_ns: f64, - similarity_ns: f64, - bind_throughput: f64, // ops/sec - dimensions: usize = DIM_10K, -}; - -/// Run 10K VSA benchmark -pub fn benchmark(_: std.mem.Allocator, iterations: usize) !BenchmarkResult { - var rng = std.Random.DefaultPrng.init(@intCast(std.time.timestamp())); - - // Create test vectors - const vec_a = try HyperVector10K.random(&rng); - const vec_b = try HyperVector10K.random(&rng); - - // Warmup - _ = HyperVector10K.bind(&vec_a, &vec_b); - _ = try HyperVector10K.bundle(&vec_a, &vec_b); - _ = try HyperVector10K.cosineSimilarity(&vec_a, &vec_b); - - // Benchmark bind - const bind_start = std.time.nanoTimestamp(); - var i: usize = 0; - while (i < iterations) : (i += 1) { - _ = HyperVector10K.bind(&vec_a, &vec_b); - } - const bind_end = std.time.nanoTimestamp(); - const bind_ns = @as(f64, @floatFromInt(bind_end - bind_start)) / @as(f64, @floatFromInt(iterations)); - - // Benchmark bundle - const bundle_start = std.time.nanoTimestamp(); - i = 0; - while (i < iterations) : (i += 1) { - _ = try HyperVector10K.bundle(&vec_a, &vec_b); - } - const bundle_end = std.time.nanoTimestamp(); - const bundle_ns = @as(f64, @floatFromInt(bundle_end - bundle_start)) / @as(f64, @floatFromInt(iterations)); - - // Benchmark similarity - const sim_start = std.time.nanoTimestamp(); - i = 0; - while (i < iterations) : (i += 1) { - _ = try HyperVector10K.cosineSimilarity(&vec_a, &vec_b); - } - const sim_end = std.time.nanoTimestamp(); - const sim_ns = @as(f64, @floatFromInt(sim_end - sim_start)) / @as(f64, @floatFromInt(iterations)); - - return BenchmarkResult{ - .bind_ns = bind_ns, - .bundle_ns = bundle_ns, - .similarity_ns = sim_ns, - .bind_throughput = 1_000_000_000.0 / bind_ns, - }; -} - -/// Print benchmark results -pub fn printBenchmark(result: BenchmarkResult) void { - const stdout = std.io.getStdOut().writer(); - - stdout.print( - \\╔════════════════════════════════════════════════════════════════════════════╗ - \\║ TRINITY VSA 10K BENCHMARK RESULTS ║ - \\╚════════════════════════════════════════════════════════════════════════════╝ - \\ - \\Dimensions: {d} - \\Vector size: {d} bytes - \\ - \\═══════════════════════════════════════════════════════════════════════════ - \\OPERATION TIME (ns) THROUGHPUT vs FPGA (est) - \\═══════════════════════════════════════════════════════════════════════════ - \\BIND {d:.2} ns {d:.0} op/s ~1000x slower - \\BUNDLE {d:.2} ns {d:.0} op/s ~500x slower - \\SIMILARITY {d:.2} ns {d:.0} op/s ~100x slower - \\═══════════════════════════════════════════════════════════════════════════ - \\ - \\φ² + 1/φ² = 3 = TRINITY - \\ - , .{ - DIM_10K, - BYTES_PER_10K, - result.bind_ns, - result.bind_throughput, - result.bundle_ns, - 1_000_000_000.0 / result.bundle_ns, - result.similarity_ns, - 1_000_000_000.0 / result.similarity_ns, - }) catch return; -} - -//========================================================================== -// TESTS -//========================================================================== - -test "HyperVector10K: zero vector" { - const vec = HyperVector10K.zero(); - try std.testing.expectEqual(@as(usize, 0), try vec.countNonZero()); -} - -test "HyperVector10K: bind identity" { - var rng = std.Random.DefaultPrng.init(42); - const vec = try HyperVector10K.random(&rng); - - // Identity vector (all +1) - var identity = HyperVector10K.zero(); - var i: usize = 0; - while (i < DIM_10K) : (i += 1) { - try identity.set(i, TRIT_POS); - } - - const result = HyperVector10K.bind(&vec, &identity); - - // Verify result equals original (sample check) - var match_count: usize = 0; - i = 0; - while (i < 100) : (i += 1) { - if ((try result.get(i)) == (try vec.get(i))) - match_count += 1; - } - - try std.testing.expect(match_count >= 95); // Allow some tolerance -} - -test "HyperVector10K: bind inverse" { - var rng = std.Random.DefaultPrng.init(42); - const vec = try HyperVector10K.random(&rng); - - // Inverse vector (all -1) - var inverse = HyperVector10K.zero(); - var i: usize = 0; - while (i < DIM_10K) : (i += 1) { - try inverse.set(i, TRIT_NEG); - } - - const result = HyperVector10K.bind(&vec, &inverse); - - // Verify result is negation of original - var match_count: usize = 0; - i = 0; - while (i < 100) : (i += 1) { - const vi = try vec.get(i); - const expected: i8 = if (vi == TRIT_NEG) TRIT_POS else if (vi == TRIT_POS) TRIT_NEG else TRIT_ZERO; - if ((try result.get(i)) == expected) - match_count += 1; - } - - try std.testing.expectEqual(@as(usize, 100), match_count); -} - -test "HyperVector10K: cosine similarity bounds" { - var rng = std.Random.DefaultPrng.init(42); - const vec_a = try HyperVector10K.random(&rng); - const vec_b = try HyperVector10K.random(&rng); - - const sim = try HyperVector10K.cosineSimilarity(&vec_a, &vec_b); - - // Similarity should be in range [0, 65535] - try std.testing.expect(sim >= 0 and sim <= 65535); -} - -test "HyperVector10K: permutation roundtrip" { - var rng = std.Random.DefaultPrng.init(42); - const original = try HyperVector10K.random(&rng); - - const shifted = try original.permute(100); - const unshifted = try shifted.permute(@as(u16, @intCast(DIM_10K - 100))); - - // Sample check (not all 10K to save time) - var match_count: usize = 0; - var i: usize = 0; - while (i < 100) : (i += 1) { - if ((try unshifted.get(i)) == (try original.get(i))) - match_count += 1; - } - - try std.testing.expectEqual(@as(usize, 100), match_count); -} - -test "HyperVector10K: benchmark quick" { - const allocator = std.testing.allocator; - const result = try benchmark(allocator, 10); - _ = result; - - // Just verify it completes without error - try std.testing.expect(true); -} - -// φ² + 1/φ² = 3 = TRINITY +//! Re-export. The implementation lives in gHashTag/zig-golden-float. +//! +//! This file used to be a second copy of that one. Both repositories carried +//! src/vsa/10k_vsa.zig, they were edited independently, and they diverged -- which is +//! why repairing sixteen defects in golden-float (#97) left every one of them +//! standing here. Two maintained copies of the same code is how that happens, +//! and it happens quietly, because nothing reports it. +//! +//! The names are listed one by one because `usingnamespace` was removed in Zig +//! 0.15, which is the version this package targets. That is a cost: a name added +//! there does not appear here until it is added here too. It is still cheaper +//! than a second implementation, and unlike a second implementation it fails +//! loudly -- the name is simply missing rather than quietly different. +const upstream = @import("zig_golden_float").vsa_10k; + +pub const Trit = upstream.Trit; +pub const HybridBigInt = upstream.HybridBigInt; +pub const DIM_10K = upstream.DIM_10K; +pub const BYTES_PER_10K = upstream.BYTES_PER_10K; +pub const WORDS_32BIT = upstream.WORDS_32BIT; +pub const BRAM_SIZE = upstream.BRAM_SIZE; +pub const VECTORS_PER_BRAM = upstream.VECTORS_PER_BRAM; +pub const TRIT_NEG = upstream.TRIT_NEG; +pub const TRIT_ZERO = upstream.TRIT_ZERO; +pub const TRIT_POS = upstream.TRIT_POS; +pub const HyperVector10K = upstream.HyperVector10K; +pub const BenchmarkResult = upstream.BenchmarkResult; +pub const benchmark = upstream.benchmark; +pub const printBenchmark = upstream.printBenchmark; diff --git a/src/vsa/common.zig b/src/vsa/common.zig index 7663154..9366d76 100644 --- a/src/vsa/common.zig +++ b/src/vsa/common.zig @@ -1,19 +1,29 @@ -// 🤖 TRINITY v0.11.0: Suborbital Order -// Common types and imports for VSA module +//! Re-export. The implementation lives in gHashTag/zig-golden-float. +//! +//! This file used to be a second copy of that one. Both repositories carried +//! src/vsa/common.zig, they were edited independently, and they diverged -- which is +//! why repairing sixteen defects in golden-float (#97) left every one of them +//! standing here. Two maintained copies of the same code is how that happens, +//! and it happens quietly, because nothing reports it. +//! +//! The names are listed one by one because `usingnamespace` was removed in Zig +//! 0.15, which is the version this package targets. That is a cost: a name added +//! there does not appear here until it is added here too. It is still cheaper +//! than a second implementation, and unlike a second implementation it fails +//! loudly -- the name is simply missing rather than quietly different. +const upstream = @import("zig_golden_float").vsa_common; -const std = @import("std"); -const tvc_hybrid = @import("../hybrid.zig"); +pub const HybridBigInt = upstream.HybridBigInt; +pub const Trit = upstream.Trit; +pub const Vec32i8 = upstream.Vec32i8; +pub const SIMD_WIDTH = upstream.SIMD_WIDTH; +pub const MAX_TRITS = upstream.MAX_TRITS; +pub const SearchResult = upstream.SearchResult; -pub const HybridBigInt = tvc_hybrid.HybridBigInt; -pub const Trit = tvc_hybrid.Trit; -pub const Vec32i8 = tvc_hybrid.Vec32i8; -pub const SIMD_WIDTH = tvc_hybrid.SIMD_WIDTH; -pub const MAX_TRITS = tvc_hybrid.MAX_TRITS; -pub const MAX_PACKED_BYTES = tvc_hybrid.MAX_PACKED_BYTES; - -pub const SearchResult = struct { - index: usize, - similarity: f64, -}; - -// φ² + 1/φ² = 3 | TRINITY +// golden-float's vsa/common.zig has no counterpart for this one, so it cannot +// come from the re-export above. It is taken from where the value actually +// lives -- packed_trit -- rather than written out as a literal, so there is +// still exactly one definition of it. Nothing inside this repository uses it, +// but it was part of this module's public surface before the deduplication and +// dropping it silently would break somebody outside who does. +pub const MAX_PACKED_BYTES = @import("zig_golden_float").packed_trit.MAX_PACKED_BYTES; diff --git a/src/vsa/concurrency.zig b/src/vsa/concurrency.zig index c21cfe8..c4dbbd2 100644 --- a/src/vsa/concurrency.zig +++ b/src/vsa/concurrency.zig @@ -1,289 +1,38 @@ -// 🤖 TRINITY v0.11.0: Suborbital Order -// Concurrency and Parallel Processing layer for VSA -const std = @import("std"); -const common = @import("common.zig"); -const HybridBigInt = common.HybridBigInt; - -// CONSTANTS -pub const POOL_SIZE = 4; -pub const DEQUE_CAPACITY = 128; -pub const MAX_WORKERS = 8; -pub const PRIORITY_LEVELS = 5; -pub const PRIORITY_QUEUE_CAPACITY = 256; -pub const MAX_JOB_AGE = 100; -pub const MAX_DAG_NODES = 256; -pub const MAX_DEPENDENCIES = 16; -pub const PHI_INVERSE: f64 = 0.618033988749895; - -// TYPES -pub const JobFn = *const fn (context: *anyopaque) void; -pub const PoolJob = struct { func: JobFn, context: *anyopaque }; -pub const PriorityLevel = enum(u8) { critical = 0, high = 1, normal = 2, low = 3, background = 4 }; -pub const JobPriority = PriorityLevel; -pub const TaskState = enum(u8) { pending = 0, ready = 1, running = 2, completed = 3, failed = 4 }; - -pub const TaskNode = struct { - id: u32, - func: JobFn, - context: *anyopaque, - dependencies: [MAX_DEPENDENCIES]u32, - dep_count: usize, - dependents: [MAX_DEPENDENCIES]u32, - dependent_count: usize, - state: TaskState, - priority: JobPriority, - deadline: ?i64, - wait_count: std.atomic.Value(usize), - - pub fn init(id: u32, func: JobFn, context: *anyopaque) TaskNode { - return TaskNode{ - .id = id, - .func = func, - .context = context, - .dependencies = undefined, - .dep_count = 0, - .dependents = undefined, - .dependent_count = 0, - .state = .pending, - .priority = .normal, - .deadline = null, - .wait_count = std.atomic.Value(usize).init(0), - }; - } - pub fn addDependency(self: *TaskNode, dep_id: u32) bool { - if (self.dep_count >= MAX_DEPENDENCIES) return false; - self.dependencies[self.dep_count] = dep_id; - self.dep_count += 1; - _ = self.wait_count.fetchAdd(1, .monotonic); - return true; - } - pub fn addDependent(self: *TaskNode, dep_id: u32) bool { - if (self.dependent_count >= MAX_DEPENDENCIES) return false; - self.dependents[self.dependent_count] = dep_id; - self.dependent_count += 1; - return true; - } - pub fn satisfyDependency(self: *TaskNode) bool { - const remaining = self.wait_count.fetchSub(1, .release) - 1; - if (remaining == 0) { - std.atomic.fence(.acquire); - self.state = .ready; - return true; - } - return false; - } - pub fn getEffectivePriority(self: *const TaskNode) f64 { - const base = switch (self.priority) { - .critical => 1.0, - .high => 0.8, - .normal => 0.6, - .low => 0.4, - .background => 0.2, - }; - if (self.deadline) |dl| { - const now = std.time.nanoTimestamp(); - const remaining = dl - now; - if (remaining < 0) return 2.0; - const boost = 1.0 / (@as(f64, @floatFromInt(remaining)) / 1e9 + 1.0); - return base + boost; - } - return base; - } -}; - -pub const DAGStats = struct { - total: usize, - completed: usize, - failed: usize, - pending: usize, - ready: usize, - completion_rate: f64, -}; - -// CHASE-LEV DEQUE & WORK-STEALING POOL -pub const ChaseLevDeque = struct { - jobs: [DEQUE_CAPACITY]PoolJob, - bottom: usize, - top: usize, - - pub fn init() ChaseLevDeque { - return ChaseLevDeque{ .jobs = undefined, .bottom = 0, .top = 0 }; - } - pub fn push(self: *ChaseLevDeque, job: PoolJob) bool { - const b = @atomicLoad(usize, &self.bottom, .seq_cst); - const t = @atomicLoad(usize, &self.top, .seq_cst); - if (b - t >= DEQUE_CAPACITY) return false; - self.jobs[b % DEQUE_CAPACITY] = job; - @atomicStore(usize, &self.bottom, b + 1, .seq_cst); - return true; - } - pub fn pop(self: *ChaseLevDeque) ?PoolJob { - var b = @atomicLoad(usize, &self.bottom, .seq_cst); - if (b == 0) return null; - b -= 1; - @atomicStore(usize, &self.bottom, b, .seq_cst); - const t = @atomicLoad(usize, &self.top, .seq_cst); - if (t <= b) { - const job = self.jobs[b % DEQUE_CAPACITY]; - if (t == b) { - const result = @cmpxchgWeak(usize, &self.top, t, t + 1, .seq_cst, .seq_cst); - if (result == null) { - @atomicStore(usize, &self.bottom, t + 1, .seq_cst); - return job; - } else { - @atomicStore(usize, &self.bottom, t + 1, .seq_cst); - return null; - } - } - return job; - } else { - @atomicStore(usize, &self.bottom, t, .seq_cst); - return null; - } - } - pub fn steal(self: *ChaseLevDeque) ?PoolJob { - const t = @atomicLoad(usize, &self.top, .seq_cst); - const b = @atomicLoad(usize, &self.bottom, .seq_cst); - if (t >= b) return null; - const job = self.jobs[t % DEQUE_CAPACITY]; - const result = @cmpxchgWeak(usize, &self.top, t, t + 1, .seq_cst, .seq_cst); - if (result == null) return job; - return null; - } - pub fn size(self: *ChaseLevDeque) usize { - const b = @atomicLoad(usize, &self.bottom, .seq_cst); - const t = @atomicLoad(usize, &self.top, .seq_cst); - return if (b > t) b - t else 0; - } - pub fn reset(self: *ChaseLevDeque) void { - @atomicStore(usize, &self.bottom, 0, .seq_cst); - @atomicStore(usize, &self.top, 0, .seq_cst); - } -}; - -pub const ThreadPool = struct { - workers: [MAX_WORKERS]ChaseLevDeque, - count: usize, - pub fn init() ThreadPool { - return ThreadPool{ .workers = undefined, .count = 0 }; - } -}; - -// DEPENDENCY GRAPH (DAG) -pub const DependencyGraph = struct { - nodes: [MAX_DAG_NODES]?TaskNode, - node_count: usize, - ready_queue: [MAX_DAG_NODES]u32, - ready_count: std.atomic.Value(usize), - completed_count: usize, - failed_count: usize, - execution_order: [MAX_DAG_NODES]u32, - order_computed: bool, - - const Self = @This(); - pub fn init() Self { - return Self{ - .nodes = .{null} ** MAX_DAG_NODES, - .node_count = 0, - .ready_queue = .{0} ** MAX_DAG_NODES, - .ready_count = std.atomic.Value(usize).init(0), - .completed_count = 0, - .failed_count = 0, - .execution_order = .{0} ** MAX_DAG_NODES, - .order_computed = false, - }; - } - pub fn addTask(self: *Self, func: JobFn, context: *anyopaque) ?u32 { - if (self.node_count >= MAX_DAG_NODES) return null; - const id: u32 = @intCast(self.node_count); - self.nodes[id] = TaskNode.init(id, func, context); - self.node_count += 1; - self.order_computed = false; - return id; - } - pub fn addTaskWithPriority(self: *Self, func: JobFn, context: *anyopaque, priority: JobPriority) ?u32 { - const id = self.addTask(func, context) orelse return null; - if (self.nodes[id]) |*node| node.priority = priority; - return id; - } - pub fn addDependency(self: *Self, from_id: u32, to_id: u32) bool { - if (from_id >= self.node_count or to_id >= self.node_count) return false; - if (from_id == to_id) return false; - if (self.nodes[from_id]) |*from_node| { - if (!from_node.addDependent(to_id)) return false; - } else return false; - if (self.nodes[to_id]) |*to_node| { - if (!to_node.addDependency(from_id)) return false; - } else return false; - self.order_computed = false; - return true; - } - pub fn computeTopologicalOrder(self: *Self) bool { - if (self.order_computed) return true; - var in_degree: [MAX_DAG_NODES]usize = .{0} ** MAX_DAG_NODES; - var queue: [MAX_DAG_NODES]u32 = .{0} ** MAX_DAG_NODES; - var queue_start: usize = 0; - var queue_end: usize = 0; - var order_idx: usize = 0; - for (0..self.node_count) |i| { - if (self.nodes[i]) |node| { - in_degree[i] = node.dep_count; - if (in_degree[i] == 0) { - queue[queue_end] = @intCast(i); - queue_end += 1; - } - } - } - while (queue_start < queue_end) { - const current = queue[queue_start]; - queue_start += 1; - self.execution_order[order_idx] = current; - order_idx += 1; - if (self.nodes[current]) |node| { - for (0..node.dependent_count) |i| { - const dep_id = node.dependents[i]; - in_degree[dep_id] -= 1; - if (in_degree[dep_id] == 0) { - queue[queue_end] = dep_id; - queue_end += 1; - } - } - } - } - if (order_idx != self.node_count) return false; - self.order_computed = true; - return true; - } - pub fn executeAll(self: *Self) struct { completed: usize, failed: usize } { - if (!self.computeTopologicalOrder()) return .{ .completed = 0, .failed = self.node_count }; - var completed: usize = 0; - for (0..self.node_count) |order_idx| { - const id = self.execution_order[order_idx]; - if (self.nodes[id]) |node| { - node.func(node.context); - completed += 1; - } - } - self.completed_count = completed; - return .{ .completed = completed, .failed = 0 }; - } -}; - -var global_pool: ?ThreadPool = null; -pub fn getGlobalPool() *ThreadPool { - if (global_pool == null) global_pool = ThreadPool.init(); - return &global_pool.?; -} -var global_dag: ?DependencyGraph = null; -pub fn getDAG() *DependencyGraph { - if (global_dag == null) global_dag = DependencyGraph.init(); - return &global_dag.?; -} -pub fn shutdownDAG() void { - global_dag = null; -} -pub fn hasDAG() bool { - return global_dag != null; -} - -// φ² + 1/φ² = 3 | TRINITY +//! Re-export. The implementation lives in gHashTag/zig-golden-float. +//! +//! This file used to be a second copy of that one. Both repositories carried +//! src/vsa/concurrency.zig, they were edited independently, and they diverged -- which is +//! why repairing sixteen defects in golden-float (#97) left every one of them +//! standing here. Two maintained copies of the same code is how that happens, +//! and it happens quietly, because nothing reports it. +//! +//! The names are listed one by one because `usingnamespace` was removed in Zig +//! 0.15, which is the version this package targets. That is a cost: a name added +//! there does not appear here until it is added here too. It is still cheaper +//! than a second implementation, and unlike a second implementation it fails +//! loudly -- the name is simply missing rather than quietly different. +const upstream = @import("zig_golden_float").vsa_concurrency; + +pub const POOL_SIZE = upstream.POOL_SIZE; +pub const DEQUE_CAPACITY = upstream.DEQUE_CAPACITY; +pub const MAX_WORKERS = upstream.MAX_WORKERS; +pub const PRIORITY_LEVELS = upstream.PRIORITY_LEVELS; +pub const PRIORITY_QUEUE_CAPACITY = upstream.PRIORITY_QUEUE_CAPACITY; +pub const MAX_JOB_AGE = upstream.MAX_JOB_AGE; +pub const MAX_DAG_NODES = upstream.MAX_DAG_NODES; +pub const MAX_DEPENDENCIES = upstream.MAX_DEPENDENCIES; +pub const PHI_INVERSE = upstream.PHI_INVERSE; +pub const JobFn = upstream.JobFn; +pub const PoolJob = upstream.PoolJob; +pub const PriorityLevel = upstream.PriorityLevel; +pub const JobPriority = upstream.JobPriority; +pub const TaskState = upstream.TaskState; +pub const TaskNode = upstream.TaskNode; +pub const DAGStats = upstream.DAGStats; +pub const ChaseLevDeque = upstream.ChaseLevDeque; +pub const ThreadPool = upstream.ThreadPool; +pub const DependencyGraph = upstream.DependencyGraph; +pub const getGlobalPool = upstream.getGlobalPool; +pub const getDAG = upstream.getDAG; +pub const shutdownDAG = upstream.shutdownDAG; +pub const hasDAG = upstream.hasDAG; diff --git a/src/vsa/core.zig b/src/vsa/core.zig index e5b5fe1..3ff13ba 100644 --- a/src/vsa/core.zig +++ b/src/vsa/core.zig @@ -1,911 +1,39 @@ -// 🤖 TRINITY v0.11.0: Suborbital Order -// Core VSA operations for Balanced Ternary -// bind, bundle, similarity, permute - -const std = @import("std"); -const common = @import("common.zig"); -const HybridBigInt = common.HybridBigInt; -const Trit = common.Trit; -const Vec32i8 = common.Vec32i8; -const SIMD_WIDTH = common.SIMD_WIDTH; -const MAX_TRITS = common.MAX_TRITS; - -/// Helper: Safe trit access after ensureUnpacked() -inline fn getTritSafe(vec: *const HybridBigInt, pos: usize) Trit { - const cache = vec.unpacked_cache orelse return 0; - return cache[pos]; -} - -/// Helper: Safe trit write after ensureUnpacked() -inline fn setTritSafe(vec: *HybridBigInt, pos: usize, value: Trit) void { - vec.setTritChecked(pos, value); -} - -/// Bind operation (XOR-like for balanced ternary) -pub fn bind(a: *HybridBigInt, b: *HybridBigInt) HybridBigInt { - a.ensureUnpacked(); - b.ensureUnpacked(); - - // Inherit allocator from first argument - const inherited_allocator = a.allocator orelse std.heap.page_allocator; - var result = HybridBigInt{ - .packed_data = [_]u8{0} ** common.MAX_PACKED_BYTES, - .unpacked_cache = null, - .allocator = inherited_allocator, - .mode = .unpacked_mode, - .trit_len = 1, - .dirty = true, - }; - result.ensureUnpacked(); - - const len = @max(a.trit_len, b.trit_len); - result.trit_len = len; - - const min_len = @min(a.trit_len, b.trit_len); - const num_full_chunks = min_len / SIMD_WIDTH; - - var i: usize = 0; - while (i < num_full_chunks * SIMD_WIDTH) : (i += SIMD_WIDTH) { - var a_vec: Vec32i8 = undefined; - var b_vec: Vec32i8 = undefined; - - inline for (0..SIMD_WIDTH) |j| { - const idx = i + j; - a_vec[j] = getTritSafe(a, idx); - b_vec[j] = getTritSafe(b, idx); - } - - const prod = a_vec * b_vec; - var result_vec: Vec32i8 = undefined; - - inline for (0..SIMD_WIDTH) |j| { - result_vec[j] = @truncate(prod[j]); - } - - // Write back using safe access - inline for (0..SIMD_WIDTH) |j| { - setTritSafe(&result, i + j, result_vec[j]); - } - } - - while (i < len) : (i += 1) { - const a_trit: Trit = if (i < a.trit_len) getTritSafe(a, i) else 0; - const b_trit: Trit = if (i < b.trit_len) getTritSafe(b, i) else 0; - setTritSafe(&result, i, a_trit * b_trit); - } - - return result; -} - -pub fn unbind(bound: *HybridBigInt, key: *HybridBigInt) HybridBigInt { - return bind(bound, key); -} - -pub fn bundle2(a: *HybridBigInt, b: *HybridBigInt, allocator: std.mem.Allocator) HybridBigInt { - _ = allocator; // Use inherited allocator instead - a.ensureUnpacked(); - b.ensureUnpacked(); - - // Inherit allocator from first argument - const inherited_allocator = a.allocator orelse std.heap.page_allocator; - var result = HybridBigInt.zero(); - result.allocator = inherited_allocator; - result.ensureUnpacked(); - - const len = @max(a.trit_len, b.trit_len); - result.trit_len = len; - - const min_len = @min(a.trit_len, b.trit_len); - const num_full_chunks = min_len / SIMD_WIDTH; - - var i: usize = 0; - while (i < num_full_chunks * SIMD_WIDTH) : (i += SIMD_WIDTH) { - var a_vec: Vec32i8 = undefined; - var b_vec: Vec32i8 = undefined; - - inline for (0..SIMD_WIDTH) |j| { - const idx = i + j; - a_vec[j] = getTritSafe(a, idx); - b_vec[j] = getTritSafe(b, idx); - } - - const a_wide: @Vector(32, i16) = a_vec; - const b_wide: @Vector(32, i16) = b_vec; - const sum = a_wide + b_wide; - - const zeros: @Vector(32, i16) = @splat(0); - const ones: @Vector(32, i16) = @splat(1); - const neg_ones: @Vector(32, i16) = @splat(-1); - - const pos_mask = sum > zeros; - const neg_mask = sum < zeros; - - var out = zeros; - out = @select(i16, pos_mask, ones, out); - out = @select(i16, neg_mask, neg_ones, out); - - inline for (0..SIMD_WIDTH) |j| { - setTritSafe(&result, i + j, @truncate(out[j])); - } - } - - while (i < len) : (i += 1) { - const a_trit: i16 = if (i < a.trit_len) getTritSafe(a, i) else 0; - const b_trit: i16 = if (i < b.trit_len) getTritSafe(b, i) else 0; - const sum = a_trit + b_trit; - - if (sum > 0) { - setTritSafe(&result, i, 1); - } else if (sum < 0) { - setTritSafe(&result, i, -1); - } else { - setTritSafe(&result, i, 0); - } - } - - return result; -} - -pub fn bundle3(a: *HybridBigInt, b: *HybridBigInt, c: *HybridBigInt, allocator: std.mem.Allocator) HybridBigInt { - _ = allocator; // Use inherited allocator instead - a.ensureUnpacked(); - b.ensureUnpacked(); - c.ensureUnpacked(); - - // Inherit allocator from first argument - const inherited_allocator = a.allocator orelse std.heap.page_allocator; - var result = HybridBigInt.zero(); - result.allocator = inherited_allocator; - result.ensureUnpacked(); - - const len = @max(@max(a.trit_len, b.trit_len), c.trit_len); - const min_len = @min(@min(a.trit_len, b.trit_len), c.trit_len); - const num_full_chunks = min_len / SIMD_WIDTH; - - // SIMD path: 32 trits at a time via i16 widening + sign extraction - var i: usize = 0; - while (i < num_full_chunks * SIMD_WIDTH) : (i += SIMD_WIDTH) { - var a_vec: Vec32i8 = undefined; - var b_vec: Vec32i8 = undefined; - var c_vec: Vec32i8 = undefined; - - inline for (0..SIMD_WIDTH) |j| { - const idx = i + j; - a_vec[j] = getTritSafe(a, idx); - b_vec[j] = getTritSafe(b, idx); - c_vec[j] = getTritSafe(c, idx); - } - - const a_wide: @Vector(32, i16) = a_vec; - const b_wide: @Vector(32, i16) = b_vec; - const c_wide: @Vector(32, i16) = c_vec; - const sum = a_wide + b_wide + c_wide; - - const zeros: @Vector(32, i16) = @splat(0); - const ones: @Vector(32, i16) = @splat(1); - const neg_ones: @Vector(32, i16) = @splat(-1); - - const pos_mask = sum > zeros; - const neg_mask = sum < zeros; - - var out = zeros; - out = @select(i16, pos_mask, ones, out); - out = @select(i16, neg_mask, neg_ones, out); - - inline for (0..SIMD_WIDTH) |j| { - setTritSafe(&result, i + j, @truncate(out[j])); - } - } - - // Scalar remainder - while (i < len) : (i += 1) { - const a_trit: i16 = if (i < a.trit_len) getTritSafe(a, i) else 0; - const b_trit: i16 = if (i < b.trit_len) getTritSafe(b, i) else 0; - const c_trit: i16 = if (i < c.trit_len) getTritSafe(c, i) else 0; - const sum = a_trit + b_trit + c_trit; - - if (sum > 0) { - setTritSafe(&result, i, 1); - } else if (sum < 0) { - setTritSafe(&result, i, -1); - } else { - setTritSafe(&result, i, 0); - } - } - - result.trit_len = len; - return result; -} - -pub fn cosineSimilarity(a: *const HybridBigInt, b: *const HybridBigInt) f64 { - const dot = @constCast(a).dotProduct(@constCast(b), std.heap.page_allocator); - const norm_a = vectorNorm(@constCast(a)); - const norm_b = vectorNorm(@constCast(b)); - - if (norm_a == 0 or norm_b == 0) return 0; - - return @as(f64, @floatFromInt(dot)) / (norm_a * norm_b); -} - -/// Cosine similarity using 16-wide f16 SIMD (2× throughput vs f32). -/// Converts ternary vectors to f16, computes similarity with 16-wide operations. -/// Returns f64 in range [-1, 1]. -pub fn cosineSimilarityF16(a: *const HybridBigInt, b: *const HybridBigInt, allocator: std.mem.Allocator) f64 { - _ = allocator; // Read-only, no allocations needed - @constCast(a).ensureUnpacked(); - @constCast(b).ensureUnpacked(); - - const len = @max(a.trit_len, b.trit_len); - if (len == 0) return 0; - - const F16_VEC_SIZE = 16; - const num_f16_chunks = len / F16_VEC_SIZE; - - // f32 accumulators for precision - var acc_dot: f64 = 0; - var acc_norm_a: f64 = 0; - var acc_norm_b: f64 = 0; - - // Process 16 elements at a time using f16 SIMD - var i: usize = 0; - while (i < num_f16_chunks * F16_VEC_SIZE) : (i += F16_VEC_SIZE) { - // Load trits into i8 vectors using safe access - var a_trits: @Vector(F16_VEC_SIZE, i8) = undefined; - var b_trits: @Vector(F16_VEC_SIZE, i8) = undefined; - - inline for (0..F16_VEC_SIZE) |j| { - a_trits[j] = if (i + j < a.trit_len) getTritSafe(@constCast(a), i + j) else 0; - b_trits[j] = if (i + j < b.trit_len) getTritSafe(@constCast(b), i + j) else 0; - } - - // Convert to f16 - const a_f16: @Vector(F16_VEC_SIZE, f16) = @floatCast(@as(@Vector(F16_VEC_SIZE, f32), @floatFromInt(a_trits))); - const b_f16: @Vector(F16_VEC_SIZE, f16) = @floatCast(@as(@Vector(F16_VEC_SIZE, f32), @floatFromInt(b_trits))); - - // Convert to f32 for compute - const a_f32: @Vector(F16_VEC_SIZE, f32) = @floatCast(a_f16); - const b_f32: @Vector(F16_VEC_SIZE, f32) = @floatCast(b_f16); - - // Compute dot product contribution - const prod = a_f32 * b_f32; - var sum_prod: f32 = 0; - inline for (0..F16_VEC_SIZE) |j| { - sum_prod += prod[j]; - } - acc_dot += @as(f64, sum_prod); - - // Compute norm contributions - const a_sq = a_f32 * a_f32; - const b_sq = b_f32 * b_f32; - var sum_a_sq: f32 = 0; - var sum_b_sq: f32 = 0; - inline for (0..F16_VEC_SIZE) |j| { - sum_a_sq += a_sq[j]; - sum_b_sq += b_sq[j]; - } - acc_norm_a += @as(f64, sum_a_sq); - acc_norm_b += @as(f64, sum_b_sq); - } - - // Handle scalar tail using safe access - while (i < len) : (i += 1) { - const a_trit: i8 = if (i < a.trit_len) getTritSafe(@constCast(a), i) else 0; - const b_trit: i8 = if (i < b.trit_len) getTritSafe(@constCast(b), i) else 0; - - const a_f32: f32 = @floatFromInt(a_trit); - const b_f32: f32 = @floatFromInt(b_trit); - - acc_dot += @as(f64, a_f32 * b_f32); - acc_norm_a += @as(f64, a_f32 * a_f32); - acc_norm_b += @as(f64, b_f32 * b_f32); - } - - const norm_a = @sqrt(acc_norm_a); - const norm_b = @sqrt(acc_norm_b); - - if (norm_a == 0 or norm_b == 0) return 0; - - return acc_dot / (norm_a * norm_b); -} - -pub fn hammingDistance(a: *HybridBigInt, b: *HybridBigInt) usize { - a.ensureUnpacked(); - b.ensureUnpacked(); - - var distance: usize = 0; - const len = @max(a.trit_len, b.trit_len); - const min_len = @min(a.trit_len, b.trit_len); - const num_full_chunks = min_len / SIMD_WIDTH; - - var i: usize = 0; - while (i < num_full_chunks * SIMD_WIDTH) : (i += SIMD_WIDTH) { - // Use safe slice access - loads 32 trits at a time - const base = i; - var a_vec: Vec32i8 = undefined; - var b_vec: Vec32i8 = undefined; - inline for (0..SIMD_WIDTH) |j| { - a_vec[j] = getTritSafe(a, base + j); - b_vec[j] = getTritSafe(b, base + j); - } - const diff = a_vec != b_vec; - distance += @popCount(@as(u32, @bitCast(diff))); - } - - while (i < len) : (i += 1) { - const a_trit: Trit = if (i < a.trit_len) getTritSafe(a, i) else 0; - const b_trit: Trit = if (i < b.trit_len) getTritSafe(b, i) else 0; - if (a_trit != b_trit) distance += 1; - } - - return distance; -} - -pub fn hammingSimilarity(a: *HybridBigInt, b: *HybridBigInt) f64 { - const len = @max(a.trit_len, b.trit_len); - if (len == 0) return 1.0; - const distance = hammingDistance(a, b); - return 1.0 - @as(f64, @floatFromInt(distance)) / @as(f64, @floatFromInt(len)); -} - -pub fn dotSimilarity(a: *HybridBigInt, b: *HybridBigInt) f64 { - const dot = a.dotProduct(b); - const len = @max(a.trit_len, b.trit_len); - if (len == 0) return 0; - return @as(f64, @floatFromInt(dot)) / @as(f64, @floatFromInt(len)); -} - -/// Vector norm — SIMD accelerated via dotProduct(v, v) (OPT-001) -pub fn vectorNorm(v: *HybridBigInt) f64 { - const dot = v.dotProduct(v, std.heap.page_allocator); - return @sqrt(@as(f64, @floatFromInt(dot))); -} - -/// Count non-zero trits — SIMD accelerated (OPT-001) -pub fn countNonZero(v: *HybridBigInt) usize { - v.ensureUnpacked(); - var count: usize = 0; - const num_full_chunks = v.trit_len / SIMD_WIDTH; - - var i: usize = 0; - while (i < num_full_chunks * SIMD_WIDTH) : (i += SIMD_WIDTH) { - // Use safe slice access - loads 32 trits at a time - const base = i; - var vec: Vec32i8 = undefined; - inline for (0..SIMD_WIDTH) |j| { - vec[j] = getTritSafe(v, base + j); - } - const zeros: Vec32i8 = @splat(0); - const nonzero = vec != zeros; - count += @popCount(@as(u32, @bitCast(nonzero))); - } - - while (i < v.trit_len) : (i += 1) { - if (getTritSafe(v, i) != 0) count += 1; - } - - return count; -} - -/// Bundle N vectors — SIMD accelerated majority vote (OPT-001) -pub fn bundleN(vectors: []*HybridBigInt, allocator: std.mem.Allocator) !HybridBigInt { - if (vectors.len == 0) return HybridBigInt.zero(); - if (vectors.len == 1) { - vectors[0].ensureUnpacked(); - // Inherit allocator from first vector - const inherited_allocator = vectors[0].allocator orelse std.heap.page_allocator; - var result = HybridBigInt.zero(); - result.allocator = inherited_allocator; - result.mode = .unpacked_mode; - result.dirty = true; - result.trit_len = vectors[0].trit_len; - result.ensureUnpacked(); - // Copy using safe access - for (0..vectors[0].trit_len) |i| { - setTritSafe(&result, i, getTritSafe(vectors[0], i)); - } - return result; - } - if (vectors.len == 2) return bundle2(vectors[0], vectors[1], allocator); - if (vectors.len == 3) return bundle3(vectors[0], vectors[1], vectors[2], allocator); - - var max_len: usize = 0; - for (vectors) |v| { - v.ensureUnpacked(); - max_len = @max(max_len, v.trit_len); - } - - // Allocate accumulator on heap (115 KB) - macOS stack fix - const accum = allocator.alloc(i16, MAX_TRITS) catch |err| { - std.debug.panic("OOM in bundleN: {}", .{err}); - }; - defer allocator.free(accum); - @memset(accum, @as(i16, 0)); - - for (vectors) |v| { - const num_chunks = v.trit_len / SIMD_WIDTH; - var i: usize = 0; - while (i < num_chunks * SIMD_WIDTH) : (i += SIMD_WIDTH) { - // Use safe vector slice access - const base = i; - var vec: Vec32i8 = undefined; - inline for (0..SIMD_WIDTH) |j| { - vec[j] = getTritSafe(v, base + j); - } - const wide: @Vector(32, i16) = vec; - const acc_array: [SIMD_WIDTH]i16 = accum[i..][0..SIMD_WIDTH].*; - const acc_vec: @Vector(32, i16) = acc_array; - const sum_val = acc_vec + wide; - accum[i..][0..SIMD_WIDTH].* = sum_val; - } - while (i < v.trit_len) : (i += 1) { - accum[i] += @as(i16, getTritSafe(v, i)); - } - } - - // Inherit allocator from first vector - const inherited_allocator = if (vectors.len > 0) vectors[0].allocator orelse std.heap.page_allocator else std.heap.page_allocator; - var result = HybridBigInt.zero(); - result.allocator = inherited_allocator; - result.mode = .unpacked_mode; - result.dirty = true; - result.trit_len = max_len; - result.ensureUnpacked(); - - const num_result_chunks = max_len / SIMD_WIDTH; - var i: usize = 0; - while (i < num_result_chunks * SIMD_WIDTH) : (i += SIMD_WIDTH) { - const acc_vec: @Vector(32, i16) = @as(@Vector(32, i16), accum[i..][0..SIMD_WIDTH].*); - const zeros: @Vector(32, i16) = @splat(0); - const ones: @Vector(32, i16) = @splat(1); - const neg_ones: @Vector(32, i16) = @splat(-1); - - const pos_mask = acc_vec > zeros; - const neg_mask = acc_vec < zeros; - - var out = zeros; - out = @select(i16, pos_mask, ones, out); - out = @select(i16, neg_mask, neg_ones, out); - - inline for (0..SIMD_WIDTH) |j| { - setTritSafe(&result, i + j, @truncate(out[j])); - } - } - - while (i < max_len) : (i += 1) { - const val = accum[i]; - if (val > 0) { - setTritSafe(&result, i, 1); - } else if (val < 0) { - setTritSafe(&result, i, -1); - } else { - setTritSafe(&result, i, 0); - } - } - - return result; -} - -pub fn randomVector(len: usize, seed: u64) HybridBigInt { - _ = len; // TODO: actually use this parameter - var result = HybridBigInt.zero(); - result.allocator = std.heap.page_allocator; // Explicit: random vectors use page allocator - result.mode = .unpacked_mode; - result.dirty = true; - result.ensureUnpacked(); - - var rng = std.Random.DefaultPrng.init(seed); - const random = rng.random(); - for (0..result.trit_len) |i| { - setTritSafe(&result, i, random.intRangeAtMost(i8, -1, 1)); - } - return result; -} - -pub fn permute(v: *HybridBigInt, k: usize) HybridBigInt { - v.ensureUnpacked(); - // Inherit allocator from v - const inherited_allocator = v.allocator orelse std.heap.page_allocator; - var result = HybridBigInt.zero(); - result.allocator = inherited_allocator; - result.ensureUnpacked(); - - result.mode = .unpacked_mode; - result.dirty = true; - result.trit_len = v.trit_len; - if (v.trit_len == 0) return result; - const shift = k % v.trit_len; - for (0..v.trit_len) |i| { - const new_pos = (i + shift) % v.trit_len; - setTritSafe(&result, new_pos, getTritSafe(v, i)); - } - return result; -} - -pub fn inversePermute(v: *HybridBigInt, k: usize) HybridBigInt { - v.ensureUnpacked(); - // Inherit allocator from v - const inherited_allocator = v.allocator orelse std.heap.page_allocator; - var result = HybridBigInt.zero(); - result.allocator = inherited_allocator; - result.ensureUnpacked(); - - result.mode = .unpacked_mode; - result.dirty = true; - result.trit_len = v.trit_len; - if (v.trit_len == 0) return result; - const shift = k % v.trit_len; - for (0..v.trit_len) |i| { - const new_pos = (i + v.trit_len - shift) % v.trit_len; - setTritSafe(&result, new_pos, getTritSafe(v, i)); - } - return result; -} - -pub fn encodeSequence(items: []HybridBigInt) HybridBigInt { - if (items.len == 0) return HybridBigInt.zero(); - var result = items[0]; - for (1..items.len) |i| { - var permuted = permute(&items[i], i); - result = result.add(&permuted, std.heap.page_allocator); - } - return result; -} - -pub fn probeSequence(sequence: *HybridBigInt, candidate: *HybridBigInt, position: usize) f64 { - var permuted = permute(candidate, position); - return cosineSimilarity(sequence, &permuted); -} - -// ═══════════════════════════════════════════════════════════════════════════════ -// TESTS -// ═══════════════════════════════════════════════════════════════════════════════ - -test "cosineSimilarityF16 matches cosineSimilarity" { - var a = randomVector(100, 111); - var b = randomVector(100, 222); - - const sim_f64 = cosineSimilarity(&a, &b); - const sim_f16 = cosineSimilarityF16(&a, &b, std.heap.page_allocator); - - // Should be very close (within f16 precision) - try std.testing.expectApproxEqAbs(sim_f64, sim_f16, 0.01); -} - -test "cosineSimilarityF16 identical vectors" { - var a = randomVector(100, 333); - - const sim = cosineSimilarityF16(&a, &a, std.heap.page_allocator); - - // Identical vectors should have similarity 1.0 - try std.testing.expectApproxEqAbs(@as(f64, 1.0), sim, 0.01); -} - -test "cosineSimilarityF16 zero vectors" { - var a = HybridBigInt.zero(); - var b = HybridBigInt.zero(); - - const sim = cosineSimilarityF16(&a, &b, std.heap.page_allocator); - - // Zero vectors should return 0 - try std.testing.expectEqual(@as(f64, 0), sim); -} - -// ═══════════════════════════════════════════════════════════════════════════════ -// QUANTUM-ENHANCED VSA OPERATIONS -// Reference: [arXiv 2106.05268 VSA], [LinkedIn Kantian Vectors] -// ═══════════════════════════════════════════════════════════════════════════════ - -/// Quantum bind: extends classical bind with phase coherence tracking -/// Maintains superposition state across operation -/// -/// In VSA framework: bind = XOR-like operation on hypervectors -/// Quantum extension: track phase information for interference -pub fn qbind(a: *const HybridBigInt, b: *const HybridBigInt) HybridBigInt { - // Classical bind (XOR-like for balanced ternary) - const result = bind(a, b); - - // Phase tracking: maintain coherence if both inputs coherent - // Result phase = (phase_a + phase_b) mod 2π - // Coherence = coherence_a AND coherence_b - // Note: In current implementation, HybridBigInt doesn't track phase - // This is a placeholder for future phase-aware VSA - - // In full quantum extension, would track: - // - Phase information via separate struct - // - Coherence preservation logic - // - Interference effects - - return result; -} - -/// Quantum bundle: probabilistic mixture with superposition amplitudes -/// Result = Σ αᵢ|vᵢ⟩ where αᵢ = amplitudes -/// -/// In VSA framework: bundle = majority vote on hypervectors -/// Quantum extension: weighted majority using amplitudes as weights -/// This is equivalent to "quantum-inspired mixture" in hybrid architectures -pub fn qbundle(vectors: []const HybridBigInt, amplitudes: []const f32, allocator: std.mem.Allocator) !HybridBigInt { - _ = allocator; // Reserved for future allocation needs - if (vectors.len == 0) return HybridBigInt.zero(); - if (vectors.len == 1) { - const result = vectors[0]; - return result; - } - - // Validate amplitudes length - if (amplitudes.len != vectors.len) { - return error.AmplitudeLengthMismatch; - } - - // Normalize amplitudes (if not already) - var total_amp: f32 = 0.0; - for (amplitudes) |amp| total_amp += amp; - const normalized = if (total_amp > 0.0) total_amp else 1.0; - - // Use weighted bundle: each trit gets weighted votes - var result = HybridBigInt.zero(); - result.mode = .unpacked_mode; - result.dirty = true; - result.trit_len = @max(MAX_TRITS, @as(usize, @intFromFloat(normalized))); - - const num_full_chunks = result.trit_len / SIMD_WIDTH; - - var i: usize = 0; - while (i < num_full_chunks * SIMD_WIDTH) : (i += SIMD_WIDTH) { - var weighted_sum: [SIMD_WIDTH]f32 = undefined; - - for (0..SIMD_WIDTH) |j| { - var sum: f32 = 0.0; - for (vectors, 0..) |*vec, k| { - @constCast(vec).ensureUnpacked(); - if (i + j < vec.trit_len) { - const weight = amplitudes[k] / normalized; - if (vec.unpacked_cache) |cache| { - sum += @as(f32, @floatFromInt(cache[i + j])) * weight; - } - } - } - weighted_sum[j] = sum; - } - - // Quantize to ternary lattice (measurement collapse) - const zeros: @Vector(32, f32) = @splat(0.0); - const ones: @Vector(32, f32) = @splat(1.0); - const neg_ones: @Vector(32, f32) = @splat(-1.0); - - const weighted_vec: @Vector(32, f32) = weighted_sum; - const pos_mask = weighted_vec > zeros; - const neg_mask = weighted_vec < zeros; - - var out = zeros; - out = @select(f32, pos_mask, ones, out); - out = @select(f32, neg_mask, neg_ones, out); - - inline for (0..SIMD_WIDTH) |j| { - const float_val: f32 = out[j]; - const int_val: i32 = @intFromFloat(float_val); - if (result.unpacked_cache) |cache| { - cache[i + j] = @intCast(int_val); - } - } - } - - // Handle scalar tail using safe access - while (i < result.trit_len) : (i += 1) { - var sum: f32 = 0.0; - for (vectors, 0..) |*vec, k| { - @constCast(vec).ensureUnpacked(); - if (i < vec.trit_len) { - const weight = amplitudes[k] / normalized; - if (vec.unpacked_cache) |cache| { - sum += @as(f32, @floatFromInt(cache[i])) * weight; - } - } - } - - // Threshold-based quantization (collapse) - setTritSafe(&result, i, if (sum > 0.5) 1 else if (sum < -0.5) -1 else 0); - } - - return result; -} - -/// Measure: collapse superposition to classical ternary state -/// Samples from |ψ|² distribution (Born rule) -/// -/// In VSA framework: measurement = reading out hypervector -/// Quantum extension: probabilistic sampling based on amplitudes -/// For simplicity: deterministic collapse to quantized state -pub fn measure(qvec: *const HybridBigInt, rng: *std.Random) HybridBigInt { - _ = rng; // For future probabilistic measurement (const) - - // Born rule: P(state) = |α|² - // Sample from ternary distribution based on amplitudes - // For Trinity: collapse to quantized ternary value - - // In current implementation, HybridBigInt is already quantized - // This function returns a copy (collapsing any superposition metadata) - - const result = qvec.*; - - // In full implementation: - // 1. Extract "amplitude" from packed representation - // 2. Use threshold to decide {-1, 0, +1} - // 3. For probabilistic measurement, sample from distribution - - // For now: deterministic quantization (already collapsed) - return result; -} - -/// Quantum similarity with interference term -/// Includes phase-dependent interference: cos(phase_diff) -/// -/// In VSA framework: similarity = cosine similarity of hypervectors -/// Quantum extension: includes phase interference -/// sim_q = sim_classical × (1 + η·cos(Δφ)) -pub fn similarity_quantum(a: *const HybridBigInt, b: *const HybridBigInt, phase_diff: f32) f64 { - const classical_sim = cosineSimilarity(a, b); - - // Interference term: constructive (cos>0) or destructive (cos<0) - // η = 0.5 is interference strength - const interference = 0.5 * @cos(phase_diff); - - return classical_sim * (1.0 + interference); -} - -/// Apply phase shift to hypervector (for quantum interference) -/// Rotates the vector in the complex phase plane -pub fn applyPhase(vec: *const HybridBigInt, phase_shift: f32, allocator: std.mem.Allocator) !HybridBigInt { - _ = allocator; // Reserved for future allocation needs - // In a full quantum VSA, this would rotate complex amplitudes - // For ternary VSA, we simulate via permute-like operation - - // Number of trit positions to shift - const shift_amount = @abs(@as(i32, @intFromFloat(phase_shift * 10.0))) % @as(i32, @intCast(vec.trit_len)); - - const abs_shift = @abs(shift_amount); - - if (abs_shift > 0) { - return permute(vec, @as(usize, @intCast(abs_shift))); - } - - // No shift - return copy - const result = vec.*; - return result; -} - -/// Compute quantum coherence between multiple vectors -/// Returns value [0, 1] where 1 = fully coherent -pub fn computeCoherence(vectors: []const HybridBigInt) f32 { - if (vectors.len < 2) return 1.0; - - var total_sim: f64 = 0.0; - var count: usize = 0; - - for (0..vectors.len) |i| { - for (i + 1..vectors.len) |j| { - const sim = cosineSimilarityF16(&vectors[i], &vectors[j], std.heap.page_allocator); - total_sim += sim; - count += 1; - } - } - - return if (count > 0) @as(f32, @floatCast(total_sim / @as(f64, @floatFromInt(count)))) else 0.0; -} - -/// Entangle two hypervectors (correlated superposition) -/// Creates a combined state that maintains correlation -pub fn entangle(a: *const HybridBigInt, b: *const HybridBigInt, correlation: f32) struct { - left: HybridBigInt, - right: HybridBigInt, -} { - // Create correlated copies based on correlation strength - // correlation ∈ [0, 1]: 0 = independent, 1 = fully entangled - - var left = a.*; - var right = b.*; - - // Apply correlation: blend some trits using safe access - if (correlation > 0 and a.trit_len == b.trit_len) { - const num_entangled = @as(usize, @intFromFloat(@as(f32, @floatFromInt(a.trit_len)) * correlation)); - - for (0..num_entangled) |i| { - const idx = i; // Simple linear mapping - if (idx < a.trit_len and idx < b.trit_len) { - // Swap some trits to create correlation - setTritSafe(&left, idx, getTritSafe(@constCast(b), idx)); - setTritSafe(&right, idx, getTritSafe(@constCast(a), idx)); - } - } - } - - return .{ .left = left, .right = right }; -} - -// φ² + 1/φ² = 3 | TRINITY - -// ═══════════════════════════════════════════════════════════════════════════════ -// QUANTUM VSA TESTS -// ═══════════════════════════════════════════════════════════════════════════════ - -test "qbundle with amplitudes" { - const a = randomVector(100, 111); - const b = randomVector(100, 222); - const c = randomVector(100, 333); - - const amplitudes = [_]f32{ 1.0, 1.0, 1.0 }; - - // Create array of vectors (pass by value for qbundle API) - var vec_slice = [_]HybridBigInt{ a, b, c }; - - const result = try qbundle(&vec_slice, &litudes, std.testing.allocator); - - // Result should be valid ternary vector - try std.testing.expect(result.trit_len > 0); - // Check first 100 trits are in valid range using safe access - const check_len = @min(100, result.trit_len); - for (0..check_len) |i| { - const trit = getTritSafe(&result, i); - try std.testing.expect(trit >= -1 and trit <= 1); - } -} - -test "similarity_quantum with interference" { - var a = randomVector(100, 444); - var b = randomVector(100, 555); - - // Classical similarity - const sim_classical = cosineSimilarity(&a, &b); - - // Quantum similarity with constructive interference (phase_diff = 0) - const sim_constructive = similarity_quantum(&a, &b, 0.0); - - // Quantum similarity with destructive interference (phase_diff = π) - const sim_destructive = similarity_quantum(&a, &b, std.math.pi); - - // Constructive should enhance similarity - try std.testing.expect(sim_constructive >= sim_classical); - - // Destructive should reduce similarity - try std.testing.expect(sim_destructive <= sim_classical); -} - -test "computeCoherence" { - const v1 = randomVector(50, 123); - const v2 = randomVector(50, 124); - var v3 = randomVector(50, 125); - - // Set v3 to be similar to v1 using safe access - for (0..@min(v1.trit_len, v3.trit_len)) |i| { - if (i < v3.trit_len) setTritSafe(&v3, i, getTritSafe(&v1, i)); - } - - // Create array of vectors (pass by value for computeCoherence API) - var vec_slice = [_]HybridBigInt{ v1, v2, v3 }; - - const coherence = computeCoherence(&vec_slice); - - // Should have some coherence (> 0) - try std.testing.expect(coherence > 0.0); -} - -test "entangle with correlation" { - var a = randomVector(50, 666); - var b = randomVector(50, 777); - - const fully_entangled = entangle(&a, &b, 1.0); - - // With full correlation, vectors should share trits using safe access - try std.testing.expectEqual( - getTritSafe(&a, 0), - getTritSafe(&fully_entangled.right, 0), - ); - - const independent = entangle(&a, &b, 0.0); - - // With zero correlation, vectors should be copies - try std.testing.expectEqual( - if (a.unpacked_cache) |cache| cache[0] else 0, - if (independent.left.unpacked_cache) |cache| cache[0] else 0, - ); -} +//! Re-export. The implementation lives in gHashTag/zig-golden-float. +//! +//! This file used to be a second copy of that one. Both repositories carried +//! src/vsa/core.zig, they were edited independently, and they diverged -- which is +//! why repairing sixteen defects in golden-float (#97) left every one of them +//! standing here. Two maintained copies of the same code is how that happens, +//! and it happens quietly, because nothing reports it. +//! +//! The names are listed one by one because `usingnamespace` was removed in Zig +//! 0.15, which is the version this package targets. That is a cost: a name added +//! there does not appear here until it is added here too. It is still cheaper +//! than a second implementation, and unlike a second implementation it fails +//! loudly -- the name is simply missing rather than quietly different. +const upstream = @import("zig_golden_float").vsa; + +pub const bind = upstream.bind; +pub const unbind = upstream.unbind; +pub const bundle2 = upstream.bundle2; +pub const bundle3 = upstream.bundle3; +pub const cosineSimilarity = upstream.cosineSimilarity; +pub const cosineSimilarityF16 = upstream.cosineSimilarityF16; +pub const hammingDistance = upstream.hammingDistance; +pub const hammingSimilarity = upstream.hammingSimilarity; +pub const dotSimilarity = upstream.dotSimilarity; +pub const vectorNorm = upstream.vectorNorm; +pub const countNonZero = upstream.countNonZero; +pub const bundleN = upstream.bundleN; +pub const randomVector = upstream.randomVector; +pub const permute = upstream.permute; +pub const inversePermute = upstream.inversePermute; +pub const encodeSequence = upstream.encodeSequence; +pub const probeSequence = upstream.probeSequence; +pub const qbind = upstream.qbind; +pub const qbundle = upstream.qbundle; +pub const measure = upstream.measure; +pub const similarity_quantum = upstream.similarity_quantum; +pub const applyPhase = upstream.applyPhase; +pub const computeCoherence = upstream.computeCoherence; +pub const entangle = upstream.entangle; diff --git a/src/vsa/fpga_bind.zig b/src/vsa/fpga_bind.zig index 5fff7ca..8ba8991 100644 --- a/src/vsa/fpga_bind.zig +++ b/src/vsa/fpga_bind.zig @@ -1,486 +1,20 @@ -// 🤖 TRINITY v0.11.0: Suborbital Order -// FPGA VSA Bind Interface — Week 2 -// -// Provides Zig interface to FPGA-accelerated VSA operations -// via UART communication with QMTECH XC7A100T - -const std = @import("std"); -const builtin = @import("builtin"); -const common = @import("common.zig"); -const HybridBigInt = common.HybridBigInt; -const Trit = common.Trit; - -pub const Config = struct { - /// UART device path - device: []const u8, - /// Baud rate - baud: u32 = 115200, - /// Vector dimension (must match FPGA) - dimension: usize = 256, - /// Timeout in milliseconds - timeout_ms: u32 = 5000, -}; - -pub const FPGAInterface = struct { - port: std.fs.File, - config: Config, - allocator: std.mem.Allocator, - - const Self = @This(); - - /// Initialize FPGA interface - pub fn init(config: Config, allocator: std.mem.Allocator) !Self { - const device_path = if (builtin.os.tag == .linux) - "/dev/ttyUSB0" - else if (builtin.os.tag == .macos) - "/dev/tty.usbserial-.*" - else - return error.UnsupportedOS; - - // Try to open the UART device - const port = std.fs.openFileAbsolute(device_path, .{ - .read = true, - .write = true, - }) catch |err| { - std.log.err("Failed to open FPGA UART device: {}", .{err}); - return err; - }; - - return Self{ - .port = port, - .config = config, - .allocator = allocator, - }; - } - - /// Close FPGA interface - pub fn deinit(self: *Self) void { - self.port.close(); - } - - // ═══════════════════════════════════════════════════════════════════════ - // UART PROTOCOL - // ═══════════════════════════════════════════════════════════════════════ - - const Command = enum(u8) { - BIND = 0x01, - UNBIND = 0x02, - BUNDLE2 = 0x03, - BUNDLE3 = 0x04, - SIMILARITY = 0x05, - PING = 0xFF, - }; - - const Response = enum(u8) { - OK = 0x00, - ERROR = 0x01, - BUSY = 0x02, - PONG = 0xFF, - }; - - /// Send command to FPGA - fn sendCommand(self: *Self, cmd: Command, data: []const u8) !void { - var buffer: [1024]u8 = undefined; - var offset: usize = 0; - - buffer[offset] = @intFromEnum(cmd); - offset += 1; - - buffer[offset] = @intCast(data.len & 0xFF); - offset += 1; - - @memcpy(buffer[offset..][0..data.len], data); - offset += data.len; - - // Simple checksum - var checksum: u8 = 0; - for (buffer[0..offset]) |b| checksum ^= b; - buffer[offset] = checksum; - offset += 1; - - _ = try self.port.writeAll(buffer[0..offset]); - } - - /// Receive response from FPGA - fn recvResponse(self: *Self, expected_len: usize) ![]u8 { - _ = expected_len; - var buffer: [1024]u8 = undefined; - const header_len = 2; // status + len - - const n = try self.port.readAll(buffer[0..header_len]); - if (n < header_len) return error.ShortRead; - - const status = buffer[0]; - const len = buffer[1]; - - if (status == @intFromEnum(Response.ERROR)) { - return error.FPGAError; - } - - if (len > 0) { - const n2 = try self.port.readAll(buffer[header_len .. header_len + len]); - if (n2 < len) return error.ShortRead; - } - - // Verify checksum - // ... - - return buffer[0 .. header_len + len]; - } - - // ═══════════════════════════════════════════════════════════════════════ - // VSA OPERATIONS - // ═══════════════════════════════════════════════════════════════════════ - - /// Bind two hypervectors on FPGA - pub fn bind(self: *Self, a: *HybridBigInt, b: *HybridBigInt) !HybridBigInt { - a.ensureUnpacked(); - b.ensureUnpacked(); - - const dim = @min(self.config.dimension, @max(a.trit_len, b.trit_len)); - - // Pack trits into 2-bit format for FPGA - const bytes_needed = (dim * 2 + 7) / 8; - var buffer = try self.allocator.alloc(u8, bytes_needed * 2); - defer self.allocator.free(buffer); - - // Pack vector A - for (0..dim) |i| { - const trit_val: i2 = if (i < a.trit_len) a.unpacked_cache[i] else 0; - const encoded = encodeTrit(trit_val); - const byte_idx = (i * 2) / 8; - const bit_offset = (i * 2) % 8; - buffer[byte_idx] |= encoded << bit_offset; - if (bit_offset >= 6) { - buffer[byte_idx + 1] |= encoded >> (8 - bit_offset); - } - } - - // Pack vector B (offset by bytes_needed) - const b_offset = bytes_needed; - for (0..dim) |i| { - const trit_val: i2 = if (i < b.trit_len) b.unpacked_cache[i] else 0; - const encoded = encodeTrit(trit_val); - const byte_idx = b_offset + (i * 2) / 8; - const bit_offset = (i * 2) % 8; - buffer[byte_idx] |= encoded << bit_offset; - if (bit_offset >= 6) { - buffer[byte_idx + 1] |= encoded >> (8 - bit_offset); - } - } - - // Send BIND command - try self.sendCommand(Command.BIND, buffer[0 .. bytes_needed * 2]); - - // Receive result - const response = try self.recvResponse(bytes_needed); - - // Unpack result - var result = HybridBigInt.zero(); - result.mode = .unpacked_mode; - result.trit_len = dim; - - for (0..dim) |i| { - const byte_idx = 2 + (i * 2) / 8; - const bit_offset = (i * 2) % 8; - const encoded = (response[byte_idx] >> bit_offset) & 0x03; - result.unpacked_cache[i] = decodeTrit(encoded); - } - - return result; - } - - /// Check if FPGA is responsive - pub fn ping(self: *Self) !bool { - try self.sendCommand(Command.PING, &[_]u8{}); - const response = try self.recvResponse(0); - return response[0] == @intFromEnum(Response.PONG); - } - - /// Bundle two hypervectors on FPGA (majority voting) - pub fn bundle(self: *Self, a: *HybridBigInt, b: *HybridBigInt) !HybridBigInt { - a.ensureUnpacked(); - b.ensureUnpacked(); - - const dim = @min(self.config.dimension, @max(a.trit_len, b.trit_len)); - const bytes_needed = (dim * 2 + 7) / 8; - var buffer = try self.allocator.alloc(u8, bytes_needed * 2); - defer self.allocator.free(buffer); - - // Pack vectors (same as bind) - for (0..dim) |i| { - const trit_val: i2 = if (i < a.trit_len) a.unpacked_cache[i] else 0; - const encoded = encodeTrit(trit_val); - const byte_idx = (i * 2) / 8; - const bit_offset = (i * 2) % 8; - buffer[byte_idx] |= encoded << bit_offset; - if (bit_offset >= 6) { - buffer[byte_idx + 1] |= encoded >> (8 - bit_offset); - } - } - - const b_offset = bytes_needed; - for (0..dim) |i| { - const trit_val: i2 = if (i < b.trit_len) b.unpacked_cache[i] else 0; - const encoded = encodeTrit(trit_val); - const byte_idx = b_offset + (i * 2) / 8; - const bit_offset = (i * 2) % 8; - buffer[byte_idx] |= encoded << bit_offset; - if (bit_offset >= 6) { - buffer[byte_idx + 1] |= encoded >> (8 - bit_offset); - } - } - - // Send BUNDLE command - try self.sendCommand(Command.BUNDLE2, buffer[0 .. bytes_needed * 2]); - - // Receive result (same format as bind) - const response = try self.recvResponse(bytes_needed); - - // Unpack result - var result = HybridBigInt.zero(); - result.mode = .unpacked_mode; - result.trit_len = dim; - - for (0..dim) |i| { - const byte_idx = 2 + (i * 2) / 8; - const bit_offset = (i * 2) % 8; - const encoded = (response[byte_idx] >> bit_offset) & 0x03; - result.unpacked_cache[i] = decodeTrit(encoded); - } - - return result; - } - - /// Compute dot product similarity on FPGA - pub fn similarity(self: *Self, a: *HybridBigInt, b: *HybridBigInt) !f64 { - a.ensureUnpacked(); - b.ensureUnpacked(); - - const dim = @min(self.config.dimension, @max(a.trit_len, b.trit_len)); - const bytes_needed = (dim * 2 + 7) / 8; - var buffer = try self.allocator.alloc(u8, bytes_needed * 2); - defer self.allocator.free(buffer); - - // Pack vectors - for (0..dim) |i| { - const trit_val: i2 = if (i < a.trit_len) a.unpacked_cache[i] else 0; - const encoded = encodeTrit(trit_val); - const byte_idx = (i * 2) / 8; - const bit_offset = (i * 2) % 8; - buffer[byte_idx] |= encoded << bit_offset; - if (bit_offset >= 6) { - buffer[byte_idx + 1] |= encoded >> (8 - bit_offset); - } - } - - const b_offset = bytes_needed; - for (0..dim) |i| { - const trit_val: i2 = if (i < b.trit_len) b.unpacked_cache[i] else 0; - const encoded = encodeTrit(trit_val); - const byte_idx = b_offset + (i * 2) / 8; - const bit_offset = (i * 2) % 8; - buffer[byte_idx] |= encoded << bit_offset; - if (bit_offset >= 6) { - buffer[byte_idx + 1] |= encoded >> (8 - bit_offset); - } - } - - // Send SIMILARITY command - try self.sendCommand(Command.SIMILARITY, buffer[0 .. bytes_needed * 2]); - - // Receive result (3 bytes: status + dot LSB + dot MSB) - const response = try self.recvResponse(3); - - // Parse dot product (signed 11-bit: -256 to +256) - const dot_lsb = response[1]; - const dot_msb = response[2] & 0x07; - var dot: i11 = @as(i11, @bitCast(@as(u11, @intCast(dot_msb)) << 8 | dot_lsb)); - // Sign extend - if (dot_msb & 0x04) dot |= 0xF800; - - // Normalize by dimension (cosine similarity for unit vectors) - // For raw similarity: just return dot / dim - return @as(f64, @floatFromInt(dot)) / @as(f64, @floatFromInt(dim)); - } - - // ═══════════════════════════════════════════════════════════════════════ - // TRIT ENCODING - // ═══════════════════════════════════════════════════════════════════════ - - /// Encode trit to 2-bit format - inline fn encodeTrit(t: i2) u2 { - return switch (t) { - 0 => 0b00, - 1 => 0b01, - -1 => 0b10, - else => unreachable, - }; - } - - /// Decode trit from 2-bit format - inline fn decodeTrit(e: u2) i2 { - return switch (e) { - 0b00 => 0, - 0b01 => 1, - 0b10 => -1, - else => 0, - }; - } -}; - -// ═════════════════════════════════════════════════════════════════════════════ -// CPU FALLBACK (when FPGA unavailable) -// ═════════════════════════════════════════════════════════════════════════════ - -pub const CpuFallback = struct { - allocator: std.mem.Allocator, - - const Self = @This(); - - pub fn init(allocator: std.mem.Allocator) Self { - return Self{ .allocator = allocator }; - } - - /// Bind using CPU (simulates FPGA behavior) - pub fn bind(self: *Self, a: *HybridBigInt, b: *HybridBigInt) !HybridBigInt { - _ = self; - const core = @import("core.zig"); - return core.bind(a, b); - } - - /// Bundle using CPU - pub fn bundle(self: *Self, a: *HybridBigInt, b: *HybridBigInt) !HybridBigInt { - _ = self; - const core = @import("core.zig"); - return core.bundle2(a, b); - } - - /// Similarity using CPU - pub fn similarity(self: *Self, a: *HybridBigInt, b: *HybridBigInt) !f64 { - _ = self; - const core = @import("core.zig"); - return core.cosineSimilarity(a, b); - } -}; - -// ═════════════════════════════════════════════════════════════════════════════ -// UNIFIED INTERFACE (auto-detect FPGA) -// ═════════════════════════════════════════════════════════════════════════════ - -pub const AutoVSA = struct { - fpga: ?FPGAInterface, - cpu: CpuFallback, - use_fpga: bool, - - const Self = @This(); - - /// Initialize with auto-detection - pub fn init(config: Config, allocator: std.mem.Allocator) Self { - const fpga = FPGAInterface.init(config, allocator) catch |err| { - std.log.warn("FPGA unavailable ({}), using CPU fallback", .{err}); - return Self{ - .fpga = null, - .cpu = CpuFallback.init(allocator), - .use_fpga = false, - }; - }; - - // Verify FPGA is responsive - if (fpga.ping() catch false) { - std.log.info("FPGA VSA accelerator detected", .{}); - return Self{ - .fpga = fpga, - .cpu = CpuFallback.init(allocator), - .use_fpga = true, - }; - } else { - fpga.deinit(); - std.log.warn("FPGA not responsive, using CPU fallback", .{}); - return Self{ - .fpga = null, - .cpu = CpuFallback.init(allocator), - .use_fpga = false, - }; - } - } - - pub fn deinit(self: *Self) void { - if (self.fpga) |*f| f.deinit(); - } - - /// Bind with automatic FPGA/CPU selection - pub fn bind(self: *Self, a: *HybridBigInt, b: *HybridBigInt) !HybridBigInt { - if (self.use_fpga and self.fpga != null) { - return self.fpga.?.bind(a, b); - } else { - return self.cpu.bind(a, b); - } - } - - /// Bundle with automatic FPGA/CPU selection - pub fn bundle(self: *Self, a: *HybridBigInt, b: *HybridBigInt) !HybridBigInt { - if (self.use_fpga and self.fpga != null) { - return self.fpga.?.bundle(a, b); - } else { - return self.cpu.bundle(a, b); - } - } - - /// Similarity with automatic FPGA/CPU selection - pub fn similarity(self: *Self, a: *HybridBigInt, b: *HybridBigInt) !f64 { - if (self.use_fpga and self.fpga != null) { - return self.fpga.?.similarity(a, b); - } else { - return self.cpu.similarity(a, b); - } - } -}; - -// Backward compatibility alias -pub const AutoBind = AutoVSA; - -// ═════════════════════════════════════════════════════════════════════════════ -// TESTS -// ═════════════════════════════════════════════════════════════════════════════ - -test "fpga bind: trit encoding" { - const testing = std.testing; - - try testing.expectEqual(@as(u2, 0b00), FPGAInterface.encodeTrit(0)); - try testing.expectEqual(@as(u2, 0b01), FPGAInterface.encodeTrit(1)); - try testing.expectEqual(@as(u2, 0b10), FPGAInterface.encodeTrit(-1)); -} - -test "fpga bind: trit decoding" { - const testing = std.testing; - - try testing.expectEqual(@as(i2, 0), FPGAInterface.decodeTrit(0b00)); - try testing.expectEqual(@as(i2, 1), FPGAInterface.decodeTrit(0b01)); - try testing.expectEqual(@as(i2, -1), FPGAInterface.decodeTrit(0b10)); -} - -test "fpga bind: cpu fallback" { - const testing = std.testing; - - var cpu = CpuFallback.init(testing.allocator); - - var a = HybridBigInt.zero(); - a.mode = .unpacked_mode; - a.trit_len = 16; - - var b = HybridBigInt.zero(); - b.mode = .unpacked_mode; - b.trit_len = 16; - - // Fill with test data - for (0..16) |i| { - a.unpacked_cache[i] = @intCast(@rem(i, 3) - 1); - b.unpacked_cache[i] = @intCast(@rem(i + 1, 3) - 1); - } - - const result = try cpu.bind(&a, &b); - try testing.expectEqual(@as(usize, 16), result.trit_len); -} - -// φ² + 1/φ² = 3 = TRINITY +//! Re-export. The implementation lives in gHashTag/zig-golden-float. +//! +//! This file used to be a second copy of that one. Both repositories carried +//! src/vsa/fpga_bind.zig, they were edited independently, and they diverged -- which is +//! why repairing sixteen defects in golden-float (#97) left every one of them +//! standing here. Two maintained copies of the same code is how that happens, +//! and it happens quietly, because nothing reports it. +//! +//! The names are listed one by one because `usingnamespace` was removed in Zig +//! 0.15, which is the version this package targets. That is a cost: a name added +//! there does not appear here until it is added here too. It is still cheaper +//! than a second implementation, and unlike a second implementation it fails +//! loudly -- the name is simply missing rather than quietly different. +const upstream = @import("zig_golden_float").fpga_bind; + +pub const Config = upstream.Config; +pub const FPGAInterface = upstream.FPGAInterface; +pub const CpuFallback = upstream.CpuFallback; +pub const AutoVSA = upstream.AutoVSA; +pub const AutoBind = upstream.AutoBind; diff --git a/src/vsa/hrr.zig b/src/vsa/hrr.zig index 6457ed1..3873ed8 100644 --- a/src/vsa/hrr.zig +++ b/src/vsa/hrr.zig @@ -1,412 +1,16 @@ -//! ═══════════════════════════════════════════════════════════════════════════════ -//! HRR — Holographic Reduced Representations -//! ═══════════════════════════════════════════════════════════════════════════════ +//! Re-export. The implementation lives in gHashTag/zig-golden-float. //! -//! Vector Symbolic Architecture (VSA) using Holographic Reduced Representations. -//! High-dimensional vectors for symbolic reasoning and cognitive computing. +//! This file used to be a second copy of that one. Both repositories carried +//! src/vsa/hrr.zig, they were edited independently, and they diverged -- which is +//! why repairing sixteen defects in golden-float (#97) left every one of them +//! standing here. Two maintained copies of the same code is how that happens, +//! and it happens quietly, because nothing reports it. //! -//! Features: -//! - Random vector generation with Gaussian distribution -//! - Binding via circular convolution -//! - Unbinding (inverse binding) -//! - Bundling (superposition of vectors) -//! - Similarity (cosine distance) -//! - Vector normalization -//! -//! References: -//! - Plate, R. (1995). "Holographic Reduced Representations" -//! - Kanerva, P. (2009). "Hyperdimensional Computing" -//! -//! φ² + 1/φ² = 3 = TRINITY -//! ═══════════════════════════════════════════════════════════════════════════════ - -const std = @import("std"); -const math = std.math; -const Allocator = std.mem.Allocator; -const random = std.crypto.random; - -/// ═══════════════════════════════════════════════════════════════════════════════ -/// SACRED CONSTANTS FOR HRR -/// ═══════════════════════════════════════════════════════════════════════════════ -const PHI: f64 = 1.618033988749895; // Golden Ratio -const PHI_INV: f64 = 0.618033988749895; // φ⁻¹ - -/// ═══════════════════════════════════════════════════════════════════════════════ -/// HRR — Holographic Reduced Representations -/// ═══════════════════════════════════════════════════════════════════════════════ -pub const HRR = struct { - dim: usize, - allocator: Allocator, - - pub const Error = error{ - DimensionMismatch, - EmptyVector, - InvalidVector, - }; - - /// Initialize HRR with given dimensionality - pub fn init(allocator: Allocator, dim: usize) !HRR { - if (dim < 8) return Error.InvalidVector; - return .{ - .dim = dim, - .allocator = allocator, - }; - } - - /// Initialize with φ-based dimension (phi-powered) - pub fn initPhi(allocator: Allocator, power: u32) !HRR { - // Dimensions that are powers of φ (rounded) - const base_dim: f64 = 1000.0; - const phi_factor = std.math.pow(f64, PHI, @as(f64, @floatFromInt(power))); - const dim: usize = @intFromFloat(base_dim * phi_factor); - return init(allocator, dim); - } - - /// ═══════════════════════════════════════════════════════════════════════════════ - /// VECTOR OPERATIONS - /// ═══════════════════════════════════════════════════════════════════════════════ - /// Generate random high-dimensional vector with Gaussian distribution - pub fn randomVector(self: *const HRR) ![]f32 { - var vec = try self.allocator.alloc(f32, self.dim); - - // Generate using Box-Muller transform for Gaussian distribution - var i: usize = 0; - while (i < self.dim) : (i += 2) { - // Generate uniform random floats in (0, 1] - const u1_raw: f32 = random.float(f32); - const u2_raw: f32 = random.float(f32); - - // Avoid log(0) and ensure valid range - const u1_safe = if (u1_raw <= 0.0) 1.0e-6 else if (u1_raw >= 1.0) 0.999999 else u1_raw; - const u2_safe = if (u2_raw <= 0.0) 0.0 else if (u2_raw >= 1.0) 0.999999 else u2_raw; - - const r = @sqrt(-2.0 * @log(u1_safe)); - const theta = 2.0 * math.pi * u2_safe; - - vec[i] = r * @cos(theta); - if (i + 1 < self.dim) { - vec[i + 1] = r * @sin(theta); - } - } - - return self.normalize(vec); - } - - /// Generate deterministic vector from seed string (for encoding) - pub fn seededVector(self: *const HRR, seed: []const u8) ![]f32 { - var vec = try self.allocator.alloc(f32, self.dim); - - // Simple hash-based generation (djb2 algorithm with wrapping) - var hash: u32 = 5381; - for (seed) |c| { - hash = hash *% 33 +% @as(u8, @intCast(c)); - } - - var prng = std.Random.DefaultPrng.init(hash); - var i: usize = 0; - while (i < self.dim) : (i += 2) { - const u1_val = prng.random().float(f32); - const u2_val = prng.random().float(f32); - const u1_safe = if (u1_val > 1.0e-6) u1_val else 1.0e-6; - - const r = @sqrt(-2.0 * @log(u1_safe)); - const theta = 2.0 * math.pi * u2_val; - - vec[i] = r * @cos(theta); - if (i + 1 < self.dim) { - vec[i + 1] = r * @sin(theta); - } - } - - return self.normalize(vec); - } - - /// Bind two vectors using circular convolution - /// This creates an associative binding operation - pub fn bind(self: *const HRR, a: []const f32, b: []const f32) ![]f32 { - if (a.len != self.dim or b.len != self.dim) return Error.DimensionMismatch; - - const result = try self.allocator.alloc(f32, self.dim); - - // Circular convolution - // result[k] = sum(a[i] * b[(k-i) mod dim]) - for (0..self.dim) |k| { - var sum: f32 = 0; - for (0..self.dim) |i| { - const j = if (k >= i) k - i else self.dim + k - i; - sum += a[i] * b[j]; - } - result[k] = sum; - } - - return self.normalize(result); - } - - /// Unbind (inverse binding) — for HRR, inverse is the reversed vector - pub fn unbind(self: *const HRR, bound: []const f32, known: []const f32) ![]f32 { - if (bound.len != self.dim or known.len != self.dim) return Error.DimensionMismatch; - - // For HRR circular convolution, the inverse is the reversed vector - // Unbind(a ⊗ b, b) should recover a (approximately) - const result = try self.allocator.alloc(f32, self.dim); - - // Reverse the known vector (true inverse for circular convolution) - // Then convolve with bound vector - for (0..self.dim) |k| { - var sum: f32 = 0; - for (0..self.dim) |i| { - // For inverse: known_rev[j] = known[(dim - j) % dim] - const j = if (k >= i) k - i else self.dim + k - i; - const inv_idx = (self.dim - j) % self.dim; - sum += bound[i] * known[inv_idx]; - } - result[k] = sum; - } - - return self.normalize(result); - } - - /// Bundle (superposition) multiple vectors - pub fn bundle(self: *const HRR, vectors: []const []const f32) ![]f32 { - if (vectors.len == 0) return Error.EmptyVector; - - const result = try self.allocator.alloc(f32, self.dim); - @memset(result, 0); - - // Sum all vectors - for (vectors) |vec| { - if (vec.len != self.dim) return Error.DimensionMismatch; - for (result, 0..) |*r, i| { - r.* += vec[i]; - } - } - - return self.normalize(result); - } - - /// Compute cosine similarity between two vectors - pub fn similarity(self: *const HRR, a: []const f32, b: []const f32) !f32 { - if (a.len != self.dim or b.len != self.dim) return Error.DimensionMismatch; - - var dot: f32 = 0; - var norm_a: f32 = 0; - var norm_b: f32 = 0; - - for (a, 0..) |av, i| { - dot += av * b[i]; - norm_a += av * av; - norm_b += b[i] * b[i]; - } - - const denom = @sqrt(norm_a * norm_b); - return if (denom > 1.0e-6) dot / denom else 0; - } - - /// Normalize vector to unit length - fn normalize(_: *const HRR, vec: []f32) []f32 { - var norm: f32 = 0; - for (vec) |v| { - norm += v * v; - } - norm = @sqrt(norm); - - if (norm > 1.0e-6) { - const inv_norm = 1.0 / norm; - for (vec) |*v| { - v.* *= inv_norm; - } - } - - return vec; - } - - /// Compute Hamming distance (for binary-like comparison) - pub fn hammingDistance(self: *const HRR, a: []const f32, b: []const f32) !usize { - if (a.len != self.dim or b.len != self.dim) return Error.DimensionMismatch; - - var distance: usize = 0; - for (a, b) |av, bv| { - // Count as different if signs differ - if ((av >= 0) != (bv >= 0)) { - distance += 1; - } - } - - return distance; - } - - /// Cleanup vector - pub fn freeVector(self: *const HRR, vec: []f32) void { - self.allocator.free(vec); - } -}; - -// ═══════════════════════════════════════════════════════════════════════════════ -// TESTS -// ═══════════════════════════════════════════════════════════════════════════════ - -test "HRR — Vector Generation" { - const testing = std.testing; - - var hrr = try HRR.init(testing.allocator, 1000); - - const vec1 = try hrr.randomVector(); - defer hrr.freeVector(vec1); - - // Check dimension - try testing.expectEqual(@as(usize, 1000), vec1.len); - - // Check normalization (should be close to 1) - var norm: f32 = 0; - for (vec1) |v| { - norm += v * v; - } - try testing.expectApproxEqAbs(1.0, norm, 0.01); -} - -test "HRR — Deterministic Seeded Vector" { - const testing = std.testing; - - var hrr = try HRR.init(testing.allocator, 1000); - - const vec1 = try hrr.seededVector("test"); - defer hrr.freeVector(vec1); - - const vec2 = try hrr.seededVector("test"); - defer hrr.freeVector(vec2); - - // Same seed should produce same vector - try testing.expectEqualSlices(f32, vec1, vec2); -} - -test "HRR — Binding Similarity" { - const testing = std.testing; - - var hrr = try HRR.init(testing.allocator, 1000); - - const vec_a = try hrr.seededVector("alice"); - defer hrr.freeVector(vec_a); - const vec_b = try hrr.seededVector("bob"); - defer hrr.freeVector(vec_b); - - const bound = try hrr.bind(vec_a, vec_b); - defer hrr.freeVector(bound); - - // Binding should be order-independent for HRR - const bound2 = try hrr.bind(vec_b, vec_a); - defer hrr.freeVector(bound2); - - const sim = try hrr.similarity(bound, bound2); - - // Should be nearly identical - try testing.expect(sim > 0.9); -} - -test "HRR — Unbinding Recovery" { - const testing = std.testing; - - var hrr = try HRR.init(testing.allocator, 1000); - - const vec_a = try hrr.seededVector("alice"); - defer hrr.freeVector(vec_a); - const vec_b = try hrr.seededVector("bob"); - defer hrr.freeVector(vec_b); - - const bound = try hrr.bind(vec_a, vec_b); - defer hrr.freeVector(bound); - - const recovered = try hrr.unbind(bound, vec_a); - defer hrr.freeVector(recovered); - - // Recovered should be similar to original - const sim = try hrr.similarity(vec_b, recovered); - - // Should have good similarity (unbinding is approximate) - try testing.expect(sim > 0.5); -} - -test "HRR — Bundle Orthogonality" { - const testing = std.testing; - - var hrr = try HRR.init(testing.allocator, 1000); - - const vec1 = try hrr.seededVector("vector1"); - defer hrr.freeVector(vec1); - const vec2 = try hrr.seededVector("vector2"); - defer hrr.freeVector(vec2); - - // Bundled vector - const bundled = try hrr.bundle(&[_][]const f32{ vec1, vec2 }); - defer hrr.freeVector(bundled); - - // Similarity to individual vectors should be moderate - // (not too high, not too low) - const sim1 = try hrr.similarity(bundled, vec1); - const sim2 = try hrr.similarity(bundled, vec2); - - // Similarity should be positive but less than 1 - try testing.expect(sim1 > 0 and sim1 < 1.0); - try testing.expect(sim2 > 0 and sim2 < 1.0); -} - -test "HRR — Similarity Reflexive" { - const testing = std.testing; - - var hrr = try HRR.init(testing.allocator, 1000); - - const vec = try hrr.randomVector(); - defer hrr.freeVector(vec); - - // Vector should be perfectly similar to itself - const sim = try hrr.similarity(vec, vec); - - try testing.expectApproxEqAbs(1.0, sim, 0.001); -} - -test "HRR — Hamming Distance" { - const testing = std.testing; - - var hrr = try HRR.init(testing.allocator, 1000); - - const vec1 = try hrr.randomVector(); - defer hrr.freeVector(vec1); - const vec2 = try hrr.randomVector(); - defer hrr.freeVector(vec2); - - // Hamming distance should be valid - const dist = try hrr.hammingDistance(vec1, vec2); - - try testing.expect(dist >= 0 and dist <= 1000); -} - -test "HRR — Phi-Dimension Initialization" { - const testing = std.testing; - - // Initialize with φ^1 = 1618 dimensions (approximately) - const hrr = try HRR.initPhi(testing.allocator, 1); - - // Should be close to 1618 - try testing.expect(hrr.dim >= 1500 and hrr.dim <= 1800); -} - -test "HRR — Sacred Constant Integration" { - const testing = std.testing; - - const hrr = try HRR.init(testing.allocator, 1000); - - // Create vectors representing sacred concepts - const phi_vec = try hrr.seededVector("phi"); - defer hrr.freeVector(phi_vec); - const trinity_vec = try hrr.seededVector("trinity"); - defer hrr.freeVector(trinity_vec); - - // Bind phi and trinity - const bound = try hrr.bind(phi_vec, trinity_vec); - defer hrr.freeVector(bound); - - // Verify binding creates a distinct representation - const sim = try hrr.similarity(phi_vec, bound); - - // Should be different but related - try testing.expect(sim < 0.9); -} +//! The names are listed one by one because `usingnamespace` was removed in Zig +//! 0.15, which is the version this package targets. That is a cost: a name added +//! there does not appear here until it is added here too. It is still cheaper +//! than a second implementation, and unlike a second implementation it fails +//! loudly -- the name is simply missing rather than quietly different. +const upstream = @import("zig_golden_float").hrr; + +pub const HRR = upstream.HRR; diff --git a/src/vsa/text_encoding.zig b/src/vsa/text_encoding.zig index f350d1f..71e7412 100644 --- a/src/vsa/text_encoding.zig +++ b/src/vsa/text_encoding.zig @@ -72,7 +72,7 @@ pub fn encodeWord(word: []const u8) HybridBigInt { for (word[1..]) |c| { var char_vec = charToVector(c); - result = core.bundle2(&result, &char_vec, std.heap.page_allocator); + result = core.bundle2(&result, &char_vec); } return result; @@ -87,8 +87,10 @@ pub fn encodeWordWithPosition(word: []const u8) HybridBigInt { for (word, 0..) |c, pos| { var char_vec = charToVector(c); // Permute by position to preserve order information - const permuted = core.permute(&char_vec, pos); - result = result.add(&permuted, std.heap.page_allocator); + // var, not const: add takes *Self because the value caches its own + // unpacked form, and filling that cache is a mutation. + var permuted = core.permute(&char_vec, pos); + result = result.add(&permuted); } return result; @@ -134,7 +136,7 @@ pub fn encodeTextWithNgrams(text: []const u8, allocator: Allocator) !struct { var char_vec = HybridBigInt.zero(); for (text) |c| { var cv = charToVector(c); - char_vec = char_vec.add(&cv, std.heap.page_allocator); + char_vec = char_vec.add(&cv); } // N-gram level encoding @@ -144,7 +146,7 @@ pub fn encodeTextWithNgrams(text: []const u8, allocator: Allocator) !struct { if (text.len >= NGRAM_N) { for (0..text.len - NGRAM_N + 1) |i| { var ngram = encodeNgram(text[i..][0..NGRAM_N]); - ngram_vec = ngram_vec.add(&ngram, std.heap.page_allocator); + ngram_vec = ngram_vec.add(&ngram); ngram_count += 1; } } @@ -155,7 +157,7 @@ pub fn encodeTextWithNgrams(text: []const u8, allocator: Allocator) !struct { var ngram_weighted = ngram_vec; // Scale vectors (simplified: just bundle) - const combined = core.bundle2(&char_weighted, &ngram_weighted, std.heap.page_allocator); + const combined = core.bundle2(&char_weighted, &ngram_weighted); return .{ .char_level = char_vec, @@ -186,7 +188,7 @@ pub fn encodeText(text: []const u8) HybridBigInt { } else if (!is_alpha and in_word) { const word = text[word_start..i]; var word_vec = encodeWord(word); - result = result.add(&word_vec, std.heap.page_allocator); + result = result.add(&word_vec); in_word = false; } } @@ -195,7 +197,7 @@ pub fn encodeText(text: []const u8) HybridBigInt { if (in_word) { const word = text[word_start..]; var word_vec = encodeWord(word); - result = result.add(&word_vec, std.heap.page_allocator); + result = result.add(&word_vec); } return result; @@ -325,7 +327,7 @@ pub fn encodeTextTFIDF(text: []const u8, stats: *const DocumentStats) HybridBigI const scale = @as(usize, @intFromFloat(idf)); var weighted = word_vec; for (0..@max(1, scale)) |_| { - result = result.add(&weighted, std.heap.page_allocator); + result = result.add(&weighted); } in_word = false; @@ -340,7 +342,7 @@ pub fn encodeTextTFIDF(text: []const u8, stats: *const DocumentStats) HybridBigI const scale = @as(usize, @intFromFloat(idf)); var weighted = word_vec; for (0..@max(1, scale)) |_| { - result = result.add(&weighted, std.heap.page_allocator); + result = result.add(&weighted); } }