From 13bd0f61c0894c08506b17f8a2ad95f9b1c8ba6a Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 30 Apr 2026 05:45:01 +0000 Subject: [PATCH 1/4] feat(hpc): export wht_f32 + i8/i2 dequant + kmeans/squared_l2 for bgz-tensor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drops the `ndarray_compat.rs` shim from lance-graph's bgz-tensor crate by making the missing functions first-class ndarray exports. - hpc/fft.rs: add `wht_f32` (Walsh-Hadamard Transform, butterfly algorithm) + 4 tests (zeros, DC impulse, round-trip, known pair). - hpc/quantized.rs: add `dequantize_i8_to_f32` (inverse of existing `quantize_f32_to_i8`) + `quantize_f32_to_i2` / `dequantize_i2_to_f32` (2-bit symmetric, 4 values per byte, LSB-first packing) + 3 tests (i8 round-trip, i2 round-trip, i2 packing layout). - hpc/cam_pq.rs: promote `squared_l2` and `kmeans` from `fn` to `pub fn` with expanded doc comments. Both were already production-quality; just needed visibility. No new feature flag — these are general-purpose HPC functions usable by any consumer. Existing behaviour unchanged. --- src/hpc/cam_pq.rs | 16 ++++-- src/hpc/fft.rs | 86 ++++++++++++++++++++++++++++++++ src/hpc/quantized.rs | 113 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 211 insertions(+), 4 deletions(-) diff --git a/src/hpc/cam_pq.rs b/src/hpc/cam_pq.rs index 0c103192..a856ce85 100644 --- a/src/hpc/cam_pq.rs +++ b/src/hpc/cam_pq.rs @@ -459,8 +459,11 @@ pub fn train_hybrid( /// /// For 16D subvectors (CAM-PQ subspace dimension), this is one F32x16 /// load-subtract-multiply-reduce. Consumer never sees hardware details. +/// +/// Exposed for downstream HPC consumers (e.g. tensor codecs) that need +/// the same SIMD-accelerated metric used by the CAM-PQ codec internally. #[inline(always)] -fn squared_l2(a: &[f32], b: &[f32]) -> f32 { +pub fn squared_l2(a: &[f32], b: &[f32]) -> f32 { debug_assert_eq!(a.len(), b.len()); let n = a.len(); @@ -515,10 +518,15 @@ fn jaccard_similarity(a: &[String], b: &[String]) -> f32 { if union == 0 { 1.0 } else { intersection as f32 / union as f32 } } -/// Simple k-means clustering. +/// Simple k-means clustering (Lloyd's algorithm with farthest-first seeding). +/// +/// Returns `k` centroid vectors of length `dim`. Empty `data` or `k == 0` +/// returns `k` zero centroids; if `data.len() < k`, the effective number of +/// centroids is clamped to `data.len()`. /// -/// Returns `k` centroid vectors of length `dim`. -fn kmeans(data: &[Vec], k: usize, dim: usize, iterations: usize) -> Vec> { +/// Exposed for downstream HPC consumers (e.g. tensor codebooks). Uses the +/// same SIMD-accelerated [`squared_l2`] as the CAM-PQ codec. +pub fn kmeans(data: &[Vec], k: usize, dim: usize, iterations: usize) -> Vec> { let n = data.len(); if n == 0 || k == 0 { return vec![vec![0.0; dim]; k]; diff --git a/src/hpc/fft.rs b/src/hpc/fft.rs index d3ed8063..09a8726f 100644 --- a/src/hpc/fft.rs +++ b/src/hpc/fft.rs @@ -116,6 +116,45 @@ pub fn ifft_f64(data: &mut [f64], n: usize) { } } +/// Walsh-Hadamard Transform (in-place, unnormalized). +/// +/// Standard butterfly algorithm; companion to [`fft_f32`]. The transform is +/// its own inverse up to scaling — running `wht_f32` twice on the same data +/// multiplies every element by `n`. Divide by `n` after a round-trip if a +/// normalized inverse is desired. +/// +/// `data.len()` must be a power of two. +/// +/// # Example +/// +/// ``` +/// use ndarray::hpc::fft::wht_f32; +/// +/// let mut data = vec![1.0f32, 0.0, 0.0, 0.0]; +/// wht_f32(&mut data); +/// // DC impulse → all bins equal 1.0 +/// for &v in &data { assert!((v - 1.0).abs() < 1e-6); } +/// ``` +pub fn wht_f32(data: &mut [f32]) { + let n = data.len(); + assert!( + n.is_power_of_two(), + "WHT requires power-of-two length, got {n}" + ); + let mut h = 1; + while h < n { + for i in (0..n).step_by(h * 2) { + for j in i..i + h { + let x = data[j]; + let y = data[j + h]; + data[j] = x + y; + data[j + h] = x - y; + } + } + h *= 2; + } +} + /// Real-to-complex FFT (f32): input is n real values, output is n/2+1 complex pairs. /// /// Returns interleaved complex output: [re0, im0, re1, im1, ..., re_{n/2}, im_{n/2}] @@ -206,4 +245,51 @@ mod tests { // DC component: sum = 10 assert!((output[0] - 10.0).abs() < 1e-4); } + + #[test] + fn test_wht_zeros() { + let mut data = vec![0.0f32; 8]; + wht_f32(&mut data); + for &v in &data { + assert_eq!(v, 0.0); + } + } + + #[test] + fn test_wht_dc_impulse() { + // [1, 0, 0, 0, ...] → all-ones + let mut data = vec![0.0f32; 8]; + data[0] = 1.0; + wht_f32(&mut data); + for &v in &data { + assert!((v - 1.0).abs() < 1e-6); + } + } + + #[test] + fn test_wht_round_trip() { + // Running WHT twice scales by n; dividing by n recovers the input. + let original = vec![1.0f32, -2.0, 3.0, -4.0, 5.0, -6.0, 7.0, -8.0]; + let mut data = original.clone(); + wht_f32(&mut data); + wht_f32(&mut data); + let n = original.len() as f32; + for (a, b) in data.iter().zip(original.iter()) { + assert!((a / n - b).abs() < 1e-5, "round-trip mismatch: {a} vs {b}"); + } + } + + #[test] + fn test_wht_known_pair() { + // WHT of [1, 1] = [2, 0]; WHT of [1, -1] = [0, 2] + let mut a = vec![1.0f32, 1.0]; + wht_f32(&mut a); + assert!((a[0] - 2.0).abs() < 1e-6); + assert!(a[1].abs() < 1e-6); + + let mut b = vec![1.0f32, -1.0]; + wht_f32(&mut b); + assert!(b[0].abs() < 1e-6); + assert!((b[1] - 2.0).abs() < 1e-6); + } } diff --git a/src/hpc/quantized.rs b/src/hpc/quantized.rs index 5202d8e3..3376a237 100644 --- a/src/hpc/quantized.rs +++ b/src/hpc/quantized.rs @@ -229,6 +229,33 @@ pub fn quantize_f32_to_i8(data: &[f32]) -> (Vec, QuantParams) { (quantized, QuantParams { scale, zero_point: 0, min_val, max_val }) } +/// Dequantize i8 codes back to f32 using the [`QuantParams`] from +/// [`quantize_f32_to_i8`]. +/// +/// ndarray's symmetric i8 quantization stores `code = round(val / scale)`, +/// so dequantization is `val ≈ code * scale`. Returns up to `n` values +/// (extra trailing codes are ignored). +/// +/// # Example +/// +/// ``` +/// use ndarray::hpc::quantized::{quantize_f32_to_i8, dequantize_i8_to_f32}; +/// +/// let data = vec![1.0f32, -1.0, 0.5, -0.5]; +/// let (codes, params) = quantize_f32_to_i8(&data); +/// let recovered = dequantize_i8_to_f32(&codes, ¶ms, data.len()); +/// for (a, b) in data.iter().zip(recovered.iter()) { +/// assert!((a - b).abs() < 0.05); +/// } +/// ``` +pub fn dequantize_i8_to_f32(codes: &[i8], params: &QuantParams, n: usize) -> Vec { + codes + .iter() + .take(n) + .map(|&c| c as f32 * params.scale) + .collect() +} + /// Per-channel i8 quantization (per row). pub fn quantize_per_channel_i8( data: &[f32], @@ -374,6 +401,57 @@ pub fn dequantize_i4_to_f32(packed: &[u8], params: &QuantParams, len: usize) -> result } +/// 2-bit symmetric quantization: maps values to `{-1, 0, +1}` packed 4 per byte. +/// +/// Encoding: 2-bit signed value per element, stored in LSB-first order within +/// each byte. Bit pattern: `0b11 = -1`, `0b00 = 0`, `0b01 = +1`. Output +/// length is `ceil(data.len() / 4)` bytes. Use [`dequantize_i2_to_f32`] for +/// the inverse, supplying the original element count. +/// +/// `params.scale` is the original `abs_max` (not `abs_max / Q_RANGE`), +/// because the quantized magnitudes are already `{-1, 0, +1}`. +pub fn quantize_f32_to_i2(data: &[f32]) -> (Vec, QuantParams) { + let min_val = data.iter().fold(f32::INFINITY, |a, &b| a.min(b)); + let max_val = data.iter().fold(f32::NEG_INFINITY, |a, &b| a.max(b)); + let abs_max = min_val.abs().max(max_val.abs()); + let scale = if abs_max > 0.0 { 1.0 / abs_max } else { 1.0 }; + + let mut packed = vec![0u8; (data.len() + 3) / 4]; + for (i, &v) in data.iter().enumerate() { + let q = (v * scale).round().clamp(-1.0, 1.0) as i8; + let bits = (q & 0x03) as u8; + packed[i / 4] |= bits << ((i % 4) * 2); + } + ( + packed, + QuantParams { + scale: abs_max, + zero_point: 0, + min_val, + max_val, + }, + ) +} + +/// Dequantize 2-bit packed codes back to f32. +/// +/// Inverse of [`quantize_f32_to_i2`]. `n` is the original element count +/// (the packed buffer is implicitly padded to a byte boundary). +pub fn dequantize_i2_to_f32(packed: &[u8], params: &QuantParams, n: usize) -> Vec { + let mut out = Vec::with_capacity(n); + for i in 0..n { + let byte = packed[i / 4]; + let bits = (byte >> ((i % 4) * 2)) & 0x03; + let val = match bits { + 0b11 => -1.0f32, + 0b01 => 1.0, + _ => 0.0, + }; + out.push(val * params.scale); + } + out +} + #[cfg(test)] mod tests { use super::*; @@ -443,4 +521,39 @@ mod tests { assert!((orig - rec).abs() < 0.5, "i4 roundtrip: {} vs {}", orig, rec); } } + + #[test] + fn test_i8_roundtrip() { + let data = vec![1.0f32, -1.0, 0.5, -0.5, 0.25, -0.25]; + let (codes, params) = quantize_f32_to_i8(&data); + let recovered = dequantize_i8_to_f32(&codes, ¶ms, data.len()); + assert_eq!(recovered.len(), data.len()); + for (orig, rec) in data.iter().zip(recovered.iter()) { + assert!((orig - rec).abs() < 0.02, "i8 roundtrip: {} vs {}", orig, rec); + } + } + + #[test] + fn test_i2_roundtrip() { + // Values cluster near {-1, 0, +1} after scaling. + let data = vec![1.0f32, -1.0, 0.0, 1.0, -1.0, 0.0, 1.0, -1.0]; + let (packed, params) = quantize_f32_to_i2(&data); + assert_eq!(packed.len(), 2); // ceil(8/4) = 2 bytes + let recovered = dequantize_i2_to_f32(&packed, ¶ms, data.len()); + assert_eq!(recovered.len(), data.len()); + for (orig, rec) in data.iter().zip(recovered.iter()) { + assert!((orig - rec).abs() < 1e-5, "i2 roundtrip: {} vs {}", orig, rec); + } + } + + #[test] + fn test_i2_packing_layout() { + // 4 values per byte, LSB first. + let data = vec![1.0f32, -1.0, 0.0, 1.0]; // bits: 01, 11, 00, 01 + let (packed, _) = quantize_f32_to_i2(&data); + assert_eq!(packed.len(), 1); + // byte = (01) | (11 << 2) | (00 << 4) | (01 << 6) + // = 0b01_00_11_01 = 0x4D + assert_eq!(packed[0], 0b01_00_11_01); + } } From 04419d97b817b0d2c974da771107eeb6b62b8854 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 30 Apr 2026 05:57:18 +0000 Subject: [PATCH 2/4] fix: enforce input validation in dequantize_i2_to_f32 + squared_l2 Two P2 review fixes from PR #115: - `dequantize_i2_to_f32`: assert `packed.len() * 4 >= n` before the decode loop. Truncated payloads or inconsistent metadata from untrusted sources can no longer cause an out-of-bounds panic mid- loop; instead we fail fast with a clear message. - `squared_l2`: replace `debug_assert_eq!` with `assert_eq!` so release-build callers can no longer silently drop trailing elements via the scalar `zip` fallback. Now-public API; runtime safety > perf. https://claude.ai/code/session_01NYGrxVopyszZYgLBxe4hgj --- src/hpc/cam_pq.rs | 13 ++++++++++++- src/hpc/quantized.rs | 11 +++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/hpc/cam_pq.rs b/src/hpc/cam_pq.rs index a856ce85..bf989b49 100644 --- a/src/hpc/cam_pq.rs +++ b/src/hpc/cam_pq.rs @@ -462,9 +462,20 @@ pub fn train_hybrid( /// /// Exposed for downstream HPC consumers (e.g. tensor codecs) that need /// the same SIMD-accelerated metric used by the CAM-PQ codec internally. +/// +/// # Panics +/// Panics if `a.len() != b.len()`. This is enforced unconditionally +/// (not `debug_assert`) so callers in release builds can't silently +/// drop trailing elements via the scalar `zip` fallback. #[inline(always)] pub fn squared_l2(a: &[f32], b: &[f32]) -> f32 { - debug_assert_eq!(a.len(), b.len()); + assert_eq!( + a.len(), + b.len(), + "squared_l2: input length mismatch ({} vs {})", + a.len(), + b.len(), + ); let n = a.len(); // Fast path: exactly 16 elements = one F32x16 lane (most common in CAM-PQ). diff --git a/src/hpc/quantized.rs b/src/hpc/quantized.rs index 3376a237..b215588a 100644 --- a/src/hpc/quantized.rs +++ b/src/hpc/quantized.rs @@ -437,7 +437,18 @@ pub fn quantize_f32_to_i2(data: &[f32]) -> (Vec, QuantParams) { /// /// Inverse of [`quantize_f32_to_i2`]. `n` is the original element count /// (the packed buffer is implicitly padded to a byte boundary). +/// +/// # Panics +/// Panics if `packed.len() * 4 < n` — the buffer is too short to decode `n` +/// elements. This guards against truncated payloads or inconsistent metadata +/// from untrusted sources. pub fn dequantize_i2_to_f32(packed: &[u8], params: &QuantParams, n: usize) -> Vec { + assert!( + packed.len() * 4 >= n, + "dequantize_i2_to_f32: packed buffer holds {} elements, requested {}", + packed.len() * 4, + n, + ); let mut out = Vec::with_capacity(n); for i in 0..n { let byte = packed[i / 4]; From 8899961a6598a002e0af983a6cc98c215d1db088 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 30 Apr 2026 06:37:53 +0000 Subject: [PATCH 3/4] fix: drop duplicate wht_f32 + my unnormalized tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Master's #114 added its own SIMD-accelerated wht_f32 with normalization (1/sqrt(n) factor, self-inverse). My branch had an unnormalized version plus 4 tests asserting unnormalized output. The duplicate caused E0428. - Removed my unnormalized wht_f32 (line 138) and orphaned doc block - Removed my 4 redundant tests (test_wht_zeros, test_wht_dc_impulse, test_wht_round_trip, test_wht_known_pair) — master already has test_wht_self_inverse, test_wht_energy_preservation, test_wht_large_simd which cover the normalized version's properties. Verified: cargo build clean, cargo test --lib 1705 passed, cargo fmt clean. Note: bgz-tensor consumers in lance-graph use wht_f32 as a one-way rotation (not round-trip), so the normalization factor doesn't change the relative ordering of values they consume. https://claude.ai/code/session_01NYGrxVopyszZYgLBxe4hgj --- src/hpc/fft.rs | 85 -------------------------------------------------- 1 file changed, 85 deletions(-) diff --git a/src/hpc/fft.rs b/src/hpc/fft.rs index 9c20c9f0..30b9df99 100644 --- a/src/hpc/fft.rs +++ b/src/hpc/fft.rs @@ -116,45 +116,6 @@ pub fn ifft_f64(data: &mut [f64], n: usize) { } } -/// Walsh-Hadamard Transform (in-place, unnormalized). -/// -/// Standard butterfly algorithm; companion to [`fft_f32`]. The transform is -/// its own inverse up to scaling — running `wht_f32` twice on the same data -/// multiplies every element by `n`. Divide by `n` after a round-trip if a -/// normalized inverse is desired. -/// -/// `data.len()` must be a power of two. -/// -/// # Example -/// -/// ``` -/// use ndarray::hpc::fft::wht_f32; -/// -/// let mut data = vec![1.0f32, 0.0, 0.0, 0.0]; -/// wht_f32(&mut data); -/// // DC impulse → all bins equal 1.0 -/// for &v in &data { assert!((v - 1.0).abs() < 1e-6); } -/// ``` -pub fn wht_f32(data: &mut [f32]) { - let n = data.len(); - assert!( - n.is_power_of_two(), - "WHT requires power-of-two length, got {n}" - ); - let mut h = 1; - while h < n { - for i in (0..n).step_by(h * 2) { - for j in i..i + h { - let x = data[j]; - let y = data[j + h]; - data[j] = x + y; - data[j + h] = x - y; - } - } - h *= 2; - } -} - /// Real-to-complex FFT (f32): input is n real values, output is n/2+1 complex pairs. /// /// Returns interleaved complex output: [re0, im0, re1, im1, ..., re_{n/2}, im_{n/2}] @@ -381,50 +342,4 @@ mod tests { assert!((output[0] - 10.0).abs() < 1e-4); } - #[test] - fn test_wht_zeros() { - let mut data = vec![0.0f32; 8]; - wht_f32(&mut data); - for &v in &data { - assert_eq!(v, 0.0); - } - } - - #[test] - fn test_wht_dc_impulse() { - // [1, 0, 0, 0, ...] → all-ones - let mut data = vec![0.0f32; 8]; - data[0] = 1.0; - wht_f32(&mut data); - for &v in &data { - assert!((v - 1.0).abs() < 1e-6); - } - } - - #[test] - fn test_wht_round_trip() { - // Running WHT twice scales by n; dividing by n recovers the input. - let original = vec![1.0f32, -2.0, 3.0, -4.0, 5.0, -6.0, 7.0, -8.0]; - let mut data = original.clone(); - wht_f32(&mut data); - wht_f32(&mut data); - let n = original.len() as f32; - for (a, b) in data.iter().zip(original.iter()) { - assert!((a / n - b).abs() < 1e-5, "round-trip mismatch: {a} vs {b}"); - } - } - - #[test] - fn test_wht_known_pair() { - // WHT of [1, 1] = [2, 0]; WHT of [1, -1] = [0, 2] - let mut a = vec![1.0f32, 1.0]; - wht_f32(&mut a); - assert!((a[0] - 2.0).abs() < 1e-6); - assert!(a[1].abs() < 1e-6); - - let mut b = vec![1.0f32, -1.0]; - wht_f32(&mut b); - assert!(b[0].abs() < 1e-6); - assert!((b[1] - 2.0).abs() < 1e-6); - } } From bb67fd673a9fc26d028ee8c12edeff77423636cf Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 30 Apr 2026 06:53:13 +0000 Subject: [PATCH 4/4] refactor: rename phyllotactic-manifold -> fractal + fix pre-existing clippy/nostd User-requested rename ("phyllotactic-manifold sounds way too weird"). Filesystem move + 6 reference site updates (root Cargo.toml dep, p64 dep, internal manifest name, bench import, p64_bridge.rs uses, COMPARISON.md). Workspace `members = ["crates/*"]` picks up the new path automatically. Also fixed pre-existing CI failures in this crate that were blocking PR #115: - clippy: replaced approx-constant literal `1.414_213_562_373_095_1` with `core::f64::consts::SQRT_2`; converted needless `for i in 0..N` index loops to `iter().enumerate()` + `iter_mut().enumerate()`; replaced redundant slice copy loop with `copy_from_slice`; removed unnecessary `as i8` self-cast; switched manual range checks to `RangeInclusive::contains`; promoted const-only assertion into a `const { assert!(..) }` block; collapsed redundant closures in bench helpers. - nostd: declared a `std` feature (default-on), added `#![cfg_attr(not(feature = "std"), no_std)]` plus `extern crate alloc` and `extern crate core as std` (mirroring ndarray's pattern); imported `alloc::vec::Vec` in `dead_zone`; added `libm` as a hard dep with thin `fsqrt` / `fpowi` polyfills gated on the `std` feature so the manifold geometry compiles on thumbv6m. Propagated the feature: ndarray's `std` feature now enables `fractal/std`, and both `ndarray -> fractal` and `p64 -> fractal` declare `default-features = false` so workspace nostd builds reach a no_std fractal. Note: the workspace nostd CI was already red on master due to unrelated issues in `p64` and the `constant_time_eq` transitive dep of blake3; those are out of scope for this PR. fractal itself now builds clean on thumbv6m-none-eabi standalone. No semantic changes to the crate's public API; consumer-facing re-exports remain identical (`p64_bridge::manifold_consts`, `fractal::seven_plus_one::nars_truth`). https://claude.ai/code/session_01NYGrxVopyszZYgLBxe4hgj --- COMPARISON.md | 2 +- Cargo.lock | 19 +- Cargo.toml | 4 +- .../Cargo.toml | 7 +- .../benches/manifold_bench.rs | 35 +-- .../src/lib.rs | 258 +++++++++--------- crates/p64/Cargo.toml | 2 +- src/hpc/p64_bridge.rs | 4 +- 8 files changed, 167 insertions(+), 164 deletions(-) rename crates/{phyllotactic-manifold => fractal}/Cargo.toml (75%) rename crates/{phyllotactic-manifold => fractal}/benches/manifold_bench.rs (78%) rename crates/{phyllotactic-manifold => fractal}/src/lib.rs (82%) diff --git a/COMPARISON.md b/COMPARISON.md index 4cce0a30..45e86348 100644 --- a/COMPARISON.md +++ b/COMPARISON.md @@ -186,7 +186,7 @@ | Crate | Upstream | **Fork** | Detail | |-------|----------|----------|--------| | `crates/p64` | Not present | **P64** | Palette64 data structure — convergence highway between ndarray and lance-graph | -| `crates/phyllotactic-manifold` | Not present | **Phyllotactic Manifold** | Golden-angle spiral geometry for uniform point distribution | +| `crates/fractal` | Not present | **Phyllotactic Manifold** | Golden-angle spiral geometry for uniform point distribution | ## Burn Backend (20 ops files) diff --git a/Cargo.lock b/Cargo.lock index ac5379bc..fc4c6437 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1461,6 +1461,14 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" +[[package]] +name = "fractal" +version = "0.1.0" +dependencies = [ + "criterion", + "libm", +] + [[package]] name = "futures-core" version = "0.3.32" @@ -2056,6 +2064,7 @@ dependencies = [ "cranelift-jit", "cranelift-module", "defmac", + "fractal", "itertools 0.13.0", "libc", "matrixmultiply", @@ -2064,7 +2073,6 @@ dependencies = [ "num-integer", "num-traits", "p64", - "phyllotactic-manifold", "portable-atomic", "portable-atomic-util", "quickcheck", @@ -2398,7 +2406,7 @@ name = "p64" version = "0.1.0" dependencies = [ "criterion", - "phyllotactic-manifold", + "fractal", ] [[package]] @@ -2463,13 +2471,6 @@ dependencies = [ "serde", ] -[[package]] -name = "phyllotactic-manifold" -version = "0.1.0" -dependencies = [ - "criterion", -] - [[package]] name = "pin-project-lite" version = "0.2.17" diff --git a/Cargo.toml b/Cargo.toml index 870271e5..05a8ce7c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,7 +50,7 @@ libc = { version = "0.2.82", optional = true } matrixmultiply = { version = "0.3.2", default-features = false, features=["cgemm"] } blake3 = "1" p64 = { path = "crates/p64" } -phyllotactic-manifold = { path = "crates/phyllotactic-manifold" } +fractal = { path = "crates/fractal", default-features = false } serde = { version = "1.0", optional = true, default-features = false, features = ["alloc"] } rawpointer = { version = "0.2" } @@ -81,7 +81,7 @@ blas = ["dep:cblas-sys", "dep:libc"] serde = ["dep:serde"] -std = ["num-traits/std", "matrixmultiply/std"] +std = ["num-traits/std", "matrixmultiply/std", "fractal/std"] rayon = ["dep:rayon", "std"] matrixmultiply-threading = ["matrixmultiply/threading"] diff --git a/crates/phyllotactic-manifold/Cargo.toml b/crates/fractal/Cargo.toml similarity index 75% rename from crates/phyllotactic-manifold/Cargo.toml rename to crates/fractal/Cargo.toml index 2367a043..dd9dbb94 100644 --- a/crates/phyllotactic-manifold/Cargo.toml +++ b/crates/fractal/Cargo.toml @@ -1,12 +1,17 @@ [package] -name = "phyllotactic-manifold" +name = "fractal" version = "0.1.0" edition = "2021" rust-version = "1.94" description = "7+1 phyllotactic SIMD manifold with Euler-gamma singularity correction" license = "MIT OR Apache-2.0" +[features] +default = ["std"] +std = [] + [dependencies] +libm = { version = "0.2", default-features = false } [dev-dependencies] criterion = { version = "0.5", features = ["html_reports"] } diff --git a/crates/phyllotactic-manifold/benches/manifold_bench.rs b/crates/fractal/benches/manifold_bench.rs similarity index 78% rename from crates/phyllotactic-manifold/benches/manifold_bench.rs rename to crates/fractal/benches/manifold_bench.rs index 819ec17b..2b23900d 100644 --- a/crates/phyllotactic-manifold/benches/manifold_bench.rs +++ b/crates/fractal/benches/manifold_bench.rs @@ -1,5 +1,5 @@ use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion}; -use phyllotactic_manifold::*; +use fractal::*; fn make_seeds(n: usize) -> Vec<[i8; 34]> { let mut seeds = Vec::with_capacity(n); @@ -7,8 +7,8 @@ fn make_seeds(n: usize) -> Vec<[i8; 34]> { let mut seed = [0i8; 34]; seed[0] = (i % 128) as i8; // HEEL seed[33] = ((i * 7) % 128) as i8; // GAMMA - for j in 1..33 { - seed[j] = ((i * 13 + j * 37) % 256) as i8; + for (j, slot) in seed.iter_mut().enumerate().take(33).skip(1) { + *slot = ((i * 13 + j * 37) % 256) as i8; } seeds.push(seed); } @@ -59,10 +59,10 @@ fn bench_resonance(c: &mut Criterion) { let threshold = 1000.0; // Pre-encode - let flat8_enc: Vec<_> = seeds.iter().map(|s| flat8::encode(s)).collect(); - let spiral8_enc: Vec<_> = seeds.iter().map(|s| spiral8::encode(s)).collect(); - let spiral8g_enc: Vec<_> = seeds.iter().map(|s| spiral8_gamma::encode(s)).collect(); - let s7p1_enc: Vec<_> = seeds.iter().map(|s| seven_plus_one::encode(s)).collect(); + let flat8_enc: Vec<_> = seeds.iter().map(flat8::encode).collect(); + let spiral8_enc: Vec<_> = seeds.iter().map(spiral8::encode).collect(); + let spiral8g_enc: Vec<_> = seeds.iter().map(spiral8_gamma::encode).collect(); + let s7p1_enc: Vec<_> = seeds.iter().map(seven_plus_one::encode).collect(); let mut group = c.benchmark_group("resonance"); @@ -85,11 +85,7 @@ fn bench_resonance(c: &mut Criterion) { group.bench_function("spiral8_gamma", |b| { b.iter(|| { for (x, y) in &spiral8g_enc { - black_box(spiral8_gamma::resonance( - black_box(x), - black_box(y), - threshold, - )); + black_box(spiral8_gamma::resonance(black_box(x), black_box(y), threshold)); } }) }); @@ -107,7 +103,7 @@ fn bench_resonance(c: &mut Criterion) { fn bench_clam48(c: &mut Criterion) { let seeds = make_seeds(1024); - let manifolds: Vec<_> = seeds.iter().map(|s| seven_plus_one::encode(s)).collect(); + let manifolds: Vec<_> = seeds.iter().map(seven_plus_one::encode).collect(); c.bench_function("clam48_extraction", |b| { b.iter(|| { @@ -123,8 +119,8 @@ fn bench_dead_zone(c: &mut Criterion) { let mut s = [0i8; 34]; s[0] = 42; s[33] = 7; - for i in 1..33 { - s[i] = (i as i8).wrapping_mul(13).wrapping_add(37); + for (i, slot) in s.iter_mut().enumerate().take(33).skip(1) { + *slot = (i as i8).wrapping_mul(13).wrapping_add(37); } s }; @@ -151,12 +147,5 @@ fn bench_encode_scaling(c: &mut Criterion) { group.finish(); } -criterion_group!( - benches, - bench_encode, - bench_resonance, - bench_clam48, - bench_dead_zone, - bench_encode_scaling, -); +criterion_group!(benches, bench_encode, bench_resonance, bench_clam48, bench_dead_zone, bench_encode_scaling,); criterion_main!(benches); diff --git a/crates/phyllotactic-manifold/src/lib.rs b/crates/fractal/src/lib.rs similarity index 82% rename from crates/phyllotactic-manifold/src/lib.rs rename to crates/fractal/src/lib.rs index 77cef6da..b1eee690 100644 --- a/crates/phyllotactic-manifold/src/lib.rs +++ b/crates/fractal/src/lib.rs @@ -13,13 +13,49 @@ //! 7 is prime (zero algebraic aliasing) and γ heals the Lane 0 singularity. #![allow(clippy::excessive_precision)] +#![cfg_attr(not(feature = "std"), no_std)] + +extern crate alloc; + +#[cfg(not(feature = "std"))] +extern crate core as std; +#[cfg(feature = "std")] +extern crate std; + +// ============================================================================ +// Float polyfills — under no_std, libm provides sqrt/powi. +// ============================================================================ + +#[cfg(feature = "std")] +#[inline(always)] +fn fsqrt(x: f64) -> f64 { + x.sqrt() +} + +#[cfg(not(feature = "std"))] +#[inline(always)] +fn fsqrt(x: f64) -> f64 { + libm::sqrt(x) +} + +#[cfg(feature = "std")] +#[inline(always)] +fn fpowi(x: f64, n: i32) -> f64 { + x.powi(n) +} + +#[cfg(not(feature = "std"))] +#[inline(always)] +fn fpowi(x: f64, n: i32) -> f64 { + libm::pow(x, n as f64) +} // ============================================================================ // Constants // ============================================================================ pub mod consts { - use std::f64::consts::{GOLDEN_RATIO, TAU}; + use std::f64::consts::{GOLDEN_RATIO, SQRT_2, TAU}; /// Golden angle θ = 2π / φ² ≈ 2.3999632 rad ≈ 137.508° pub const GOLDEN_ANGLE: f64 = TAU / (GOLDEN_RATIO * GOLDEN_RATIO); @@ -36,51 +72,51 @@ pub mod consts { // These are FIXED for 8 lanes — compile-time, zero runtime cost. pub const COS_GOLDEN: [f64; 8] = [ - 1.000_000_000_000_000_0, // n=0 - -0.737_368_878_078_319_7, // n=1 - 0.087_425_724_716_959_9, // n=2 - 0.608_438_860_978_862_6, // n=3 - -0.984_713_485_315_428_7, // n=4 - 0.843_755_294_812_397_2, // n=5 - -0.259_604_304_901_488_6, // n=6 - -0.460_907_024_713_369_2, // n=7 + 1.000_000_000_000_000_0, // n=0 + -0.737_368_878_078_319_7, // n=1 + 0.087_425_724_716_959_9, // n=2 + 0.608_438_860_978_862_6, // n=3 + -0.984_713_485_315_428_7, // n=4 + 0.843_755_294_812_397_2, // n=5 + -0.259_604_304_901_488_6, // n=6 + -0.460_907_024_713_369_2, // n=7 ]; pub const SIN_GOLDEN: [f64; 8] = [ - 0.000_000_000_000_000_0, // n=0 - 0.675_490_294_261_523_8, // n=1 - -0.996_171_040_864_827_8, // n=2 - 0.793_600_751_291_695_9, // n=3 - -0.174_181_950_379_311_6, // n=4 - -0.536_728_052_626_322_7, // n=5 - 0.965_715_074_375_778_3, // n=6 - -0.887_448_429_245_254_6, // n=7 + 0.000_000_000_000_000_0, // n=0 + 0.675_490_294_261_523_8, // n=1 + -0.996_171_040_864_827_8, // n=2 + 0.793_600_751_291_695_9, // n=3 + -0.174_181_950_379_311_6, // n=4 + -0.536_728_052_626_322_7, // n=5 + 0.965_715_074_375_778_3, // n=6 + -0.887_448_429_245_254_6, // n=7 ]; // ── Radii ────────────────────────────────────────────────────────── /// √n — standard Vogel spiral. Lane 0 = 0.0 (SINGULARITY). pub const RADII_SQRT: [f64; 8] = [ - 0.000_000_000_000_000_0, // √0 — dead - 1.000_000_000_000_000_0, // √1 - 1.414_213_562_373_095_1, // √2 - 1.732_050_807_568_877_2, // √3 - 2.000_000_000_000_000_0, // √4 - 2.236_067_977_499_789_8, // √5 - 2.449_489_742_783_177_9, // √6 - 2.645_751_311_064_590_7, // √7 + 0.000_000_000_000_000_0, // √0 — dead + 1.000_000_000_000_000_0, // √1 + SQRT_2, // √2 + 1.732_050_807_568_877_2, // √3 + 2.000_000_000_000_000_0, // √4 + 2.236_067_977_499_789_8, // √5 + 2.449_489_742_783_177_9, // √6 + 2.645_751_311_064_590_7, // √7 ]; /// √(n + γ) — Euler-corrected. Lane 0 = 0.7598 (singularity healed). pub const RADII_GAMMA: [f64; 8] = [ - 0.759_747_105_885_591_9, // √(0 + γ) - 1.255_872_471_591_575_7, // √(1 + γ) - 1.605_370_880_794_071_4, // √(2 + γ) - 1.891_352_866_310_655_6, // √(3 + γ) - 2.139_442_839_830_392_2, // √(4 + γ) - 2.361_612_937_147_307_4, // √(5 + γ) - 2.564_608_286_834_762_0, // √(6 + γ) - 2.752_674_275_118_931_0, // √(7 + γ) + 0.759_747_105_885_591_9, // √(0 + γ) + 1.255_872_471_591_575_7, // √(1 + γ) + 1.605_370_880_794_071_4, // √(2 + γ) + 1.891_352_866_310_655_6, // √(3 + γ) + 2.139_442_839_830_392_2, // √(4 + γ) + 2.361_612_937_147_307_4, // √(5 + γ) + 2.564_608_286_834_762_0, // √(6 + γ) + 2.752_674_275_118_931_0, // √(7 + γ) ]; // ── Composite spiral coordinates: cos(nθ) × radius ──────────────── @@ -153,15 +189,10 @@ pub fn extract_slices(data: &[i8; 34]) -> ([f64; 8], f64, f64) { let gamma = data[33] as f64; let mut slices = [0.0f64; 8]; - for i in 0..8 { + for (i, slot) in slices.iter_mut().enumerate() { let start = 1 + (i * 4); - let bytes = [ - data[start] as u8, - data[start + 1] as u8, - data[start + 2] as u8, - data[start + 3] as u8, - ]; - slices[i] = i32::from_le_bytes(bytes) as f64; + let bytes = [data[start] as u8, data[start + 1] as u8, data[start + 2] as u8, data[start + 3] as u8]; + *slot = i32::from_le_bytes(bytes) as f64; } (slices, heel, gamma) @@ -174,24 +205,14 @@ pub fn extract_7plus1(data: &[i8; 34]) -> ([f64; 8], f64, f64, f64) { let gamma_byte = data[33] as f64; let mut slices = [0.0f64; 8]; - for i in 0..7 { + for (i, slot) in slices.iter_mut().enumerate().take(7) { let start = 1 + (i * 4); - let bytes = [ - data[start] as u8, - data[start + 1] as u8, - data[start + 2] as u8, - data[start + 3] as u8, - ]; - slices[i] = i32::from_le_bytes(bytes) as f64; + let bytes = [data[start] as u8, data[start + 1] as u8, data[start + 2] as u8, data[start + 3] as u8]; + *slot = i32::from_le_bytes(bytes) as f64; } // Lane 7 bytes → contradiction scalar (does NOT enter the spiral) - let contra_bytes = [ - data[29] as u8, - data[30] as u8, - data[31] as u8, - data[32] as u8, - ]; + let contra_bytes = [data[29] as u8, data[30] as u8, data[31] as u8, data[32] as u8]; let contradiction = i32::from_le_bytes(contra_bytes) as f64; slices[7] = 0.0; // Lane 7 stays zero on the manifold @@ -221,8 +242,8 @@ pub mod flat8 { #[inline] pub fn resonance(encoded: &[f64; 8], threshold: f64) -> u8 { let mut mask = 0u8; - for i in 0..8 { - if encoded[i].abs() >= threshold { + for (i, &val) in encoded.iter().enumerate() { + if val.abs() >= threshold { mask |= 1 << i; } } @@ -235,8 +256,8 @@ pub mod flat8 { // ============================================================================ pub mod spiral8 { - use super::*; use super::consts::{SPIRAL8_X, SPIRAL8_Y}; + use super::*; /// 8 lanes projected onto phyllotactic spiral. Lane 0 = dead (√0 = 0). #[inline] @@ -272,8 +293,8 @@ pub mod spiral8 { // ============================================================================ pub mod spiral8_gamma { - use super::*; use super::consts::{SPIRAL8G_X, SPIRAL8G_Y}; + use super::*; /// 8 lanes on Euler-corrected spiral. Lane 0 lives (√γ ≈ 0.76). #[inline] @@ -308,8 +329,8 @@ pub mod spiral8_gamma { // ============================================================================ pub mod seven_plus_one { - use super::*; use super::consts::{SPIRAL7_X, SPIRAL7_Y}; + use super::*; /// Result of 7+1 encoding. #[derive(Debug, Clone, Copy)] @@ -342,7 +363,13 @@ pub mod seven_plus_one { x[7] = 0.0; y[7] = 0.0; - Manifold { x, y, contradiction, heel, gamma } + Manifold { + x, + y, + contradiction, + heel, + gamma, + } } /// Resonance check on the 7 spiral lanes. Returns 7-bit mask. @@ -386,7 +413,11 @@ pub mod seven_plus_one { #[inline] pub fn to_clam48(m: &Manifold, threshold: f64, max_contra: f64) -> [u8; 6] { let res = resonance(m, threshold); - let contra_fires = if m.contradiction.abs() > max_contra * 0.5 { 1u8 } else { 0u8 }; + let contra_fires = if m.contradiction.abs() > max_contra * 0.5 { + 1u8 + } else { + 0u8 + }; // Find peak-magnitude lane (among the 7) let mut peak_lane = 0usize; @@ -398,12 +429,11 @@ pub mod seven_plus_one { peak_lane = i; } } - let peak_mag = peak_mag2.sqrt(); + let peak_mag = super::fsqrt(peak_mag2); // B3: peak lane (3 bits) + X sign (1 bit) + 4 bits X fractional let x_sign = if m.x[peak_lane] >= 0.0 { 0u8 } else { 1u8 }; - let b3 = ((peak_lane as u8) << 5) | (x_sign << 4) - | ((m.x[peak_lane].abs().min(15.0) as u8) & 0x0F); + let b3 = ((peak_lane as u8) << 5) | (x_sign << 4) | ((m.x[peak_lane].abs().min(15.0) as u8) & 0x0F); // B4: Y sign (1 bit) + 7 bits Y fractional let y_sign = if m.y[peak_lane] >= 0.0 { 0u8 } else { 1u8 }; @@ -420,12 +450,12 @@ pub mod seven_plus_one { }; [ - m.heel as i8 as u8, // B1: HEEL + m.heel as i8 as u8, // B1: HEEL (res & 0x7F) | (contra_fires << 7), // B2: 7-bit resonance + 1-bit contradiction - b3, // B3: TWIG A - b4, // B4: TWIG B - b5, // B5: LEAF - b6, // B6: GAMMA + b3, // B3: TWIG A + b4, // B4: TWIG B + b5, // B5: LEAF + b6, // B6: GAMMA ] } } @@ -436,6 +466,7 @@ pub mod seven_plus_one { pub mod dead_zone { use super::seven_plus_one; + use alloc::vec::Vec; /// Flip a single bit in the 34-byte seed at position `bit_pos` (0..272). pub fn flip_bit(data: &[i8; 34], bit_pos: usize) -> [i8; 34] { @@ -443,7 +474,7 @@ pub mod dead_zone { let mut corrupted = *data; let byte_idx = bit_pos / 8; let bit_idx = bit_pos % 8; - corrupted[byte_idx] ^= (1i8 << bit_idx) as i8; + corrupted[byte_idx] ^= 1i8 << bit_idx; corrupted } @@ -452,12 +483,10 @@ pub mod dead_zone { // 14 values: 7 X + 7 Y let mut vals_a = [0.0f64; 14]; let mut vals_b = [0.0f64; 14]; - for i in 0..7 { - vals_a[i] = a.x[i]; - vals_a[7 + i] = a.y[i]; - vals_b[i] = b.x[i]; - vals_b[7 + i] = b.y[i]; - } + vals_a[..7].copy_from_slice(&a.x[..7]); + vals_a[7..14].copy_from_slice(&a.y[..7]); + vals_b[..7].copy_from_slice(&b.x[..7]); + vals_b[7..14].copy_from_slice(&b.y[..7]); let n = 14.0f64; let mean_a = vals_a.iter().sum::() / n; @@ -474,7 +503,7 @@ pub mod dead_zone { var_b += db * db; } - let denom = (var_a * var_b).sqrt(); + let denom = super::fsqrt(var_a * var_b); if denom < 1e-15 { 0.0 } else { @@ -487,10 +516,7 @@ pub mod dead_zone { /// Returns `(bit_pos, rho, region)` for each bit. /// Regions: "heel" (byte 0), "payload_0..6" (bytes 1-28, spiral), /// "contradiction" (bytes 29-32), "gamma" (byte 33). - pub fn run_benchmark( - seed: &[i8; 34], - _threshold: f64, - ) -> Vec<(usize, f64, &'static str)> { + pub fn run_benchmark(seed: &[i8; 34], _threshold: f64) -> Vec<(usize, f64, &'static str)> { let original = seven_plus_one::encode(seed); let mut results = Vec::with_capacity(34 * 8); @@ -554,8 +580,8 @@ pub mod uniformity { if mean.abs() < 1e-15 { return f64::INFINITY; } - let variance = gaps.iter().map(|g| (g - mean).powi(2)).sum::() / n; - variance.sqrt() / mean + let variance = gaps.iter().map(|g| super::fpowi(g - mean, 2)).sum::() / n; + super::fsqrt(variance) / mean } /// Max/min gap ratio. 1.0 = perfectly uniform. Higher = worse. @@ -597,16 +623,8 @@ mod tests { let cos_err = (consts::COS_GOLDEN[n] - cos_expected).abs(); let sin_err = (consts::SIN_GOLDEN[n] - sin_expected).abs(); - assert!( - cos_err < 1e-12, - "cos[{n}]: expected {cos_expected}, got {}, err={cos_err}", - consts::COS_GOLDEN[n] - ); - assert!( - sin_err < 1e-12, - "sin[{n}]: expected {sin_expected}, got {}, err={sin_err}", - consts::SIN_GOLDEN[n] - ); + assert!(cos_err < 1e-12, "cos[{n}]: expected {cos_expected}, got {}, err={cos_err}", consts::COS_GOLDEN[n]); + assert!(sin_err < 1e-12, "sin[{n}]: expected {sin_expected}, got {}, err={sin_err}", consts::SIN_GOLDEN[n]); } } @@ -615,11 +633,7 @@ mod tests { for n in 0..8 { let expected = (n as f64 + EULER_GAMMA).sqrt(); let err = (consts::RADII_GAMMA[n] - expected).abs(); - assert!( - err < 1e-12, - "radius_gamma[{n}]: expected {expected}, got {}, err={err}", - consts::RADII_GAMMA[n] - ); + assert!(err < 1e-12, "radius_gamma[{n}]: expected {expected}, got {}, err={err}", consts::RADII_GAMMA[n]); } } @@ -631,7 +645,7 @@ mod tests { assert_eq!(consts::SPIRAL8_Y[0], 0.0); // √(n+γ): Lane 0 lives - assert!(consts::RADII_GAMMA[0] > 0.75); + const { assert!(consts::RADII_GAMMA[0] > 0.75) }; assert!(consts::SPIRAL8G_X[0].abs() > 0.5); } @@ -659,10 +673,7 @@ mod tests { eprintln!("Gap CV (√n+γ): {cv_gamma:.4}"); eprintln!("Improvement: {:.1}%", (1.0 - cv_gamma / cv_sqrt) * 100.0); - assert!( - cv_gamma < cv_sqrt, - "Euler-gamma should produce more uniform gaps" - ); + assert!(cv_gamma < cv_sqrt, "Euler-gamma should produce more uniform gaps"); let ratio_sqrt = uniformity::gap_ratio(&consts::GAPS_SQRT); let ratio_gamma = uniformity::gap_ratio(&consts::GAPS_GAMMA); @@ -670,10 +681,7 @@ mod tests { eprintln!("Gap ratio (√n): {ratio_sqrt:.4}"); eprintln!("Gap ratio (√n+γ): {ratio_gamma:.4}"); - assert!( - ratio_gamma < ratio_sqrt, - "Euler-gamma should reduce max/min gap ratio" - ); + assert!(ratio_gamma < ratio_sqrt, "Euler-gamma should reduce max/min gap ratio"); } // ── Encoding round-trip ──────────────────────────────────────────── @@ -684,8 +692,8 @@ mod tests { seed[0] = 42; seed[33] = 7; // Fill payload with recognizable pattern - for i in 1..33 { - seed[i] = (i as i8).wrapping_mul(13).wrapping_add(37); + for (i, slot) in seed.iter_mut().enumerate().take(33).skip(1) { + *slot = (i as i8).wrapping_mul(13).wrapping_add(37); } seed } @@ -695,8 +703,8 @@ mod tests { let seed = make_test_seed(); let encoded = flat8::encode(&seed); // All 8 lanes should have values (heel+gamma bias = 49) - for i in 0..8 { - assert!(encoded[i].abs() > 1.0, "Lane {i} too small"); + for (i, val) in encoded.iter().enumerate() { + assert!(val.abs() > 1.0, "Lane {i} too small"); } let mask = flat8::resonance(&encoded, 10.0); assert!(mask.count_ones() > 0); @@ -707,10 +715,7 @@ mod tests { let seed = make_test_seed(); let (x, y) = spiral8::encode(&seed); // Lane 0 should be ~zero due to √0 radius - assert!( - x[0].abs() < 1e-10 && y[0].abs() < 1e-10, - "Lane 0 should be dead in spiral8" - ); + assert!(x[0].abs() < 1e-10 && y[0].abs() < 1e-10, "Lane 0 should be dead in spiral8"); } #[test] @@ -747,8 +752,8 @@ mod tests { let res = seven_plus_one::resonance(&m, 100.0); let (f, c) = seven_plus_one::nars_truth(res, m.contradiction, 1e8); - assert!(f >= 0.0 && f <= 1.0, "frequency out of range: {f}"); - assert!(c >= 0.0 && c <= 1.0, "confidence out of range: {c}"); + assert!((0.0..=1.0).contains(&f), "frequency out of range: {f}"); + assert!((0.0..=1.0).contains(&c), "confidence out of range: {c}"); eprintln!("NARS truth: "); eprintln!("Resonance mask: {res:07b} ({} of 7 fire)", res.count_ones()); @@ -766,8 +771,10 @@ mod tests { // B2 low 7 bits = resonance, bit 7 = contradiction flag let resonance_bits = clam[1] & 0x7F; let contra_flag = (clam[1] >> 7) & 1; - eprintln!("CLAM-48: [{:02X} {:02X} {:02X} {:02X} {:02X} {:02X}]", - clam[0], clam[1], clam[2], clam[3], clam[4], clam[5]); + eprintln!( + "CLAM-48: [{:02X} {:02X} {:02X} {:02X} {:02X} {:02X}]", + clam[0], clam[1], clam[2], clam[3], clam[4], clam[5] + ); eprintln!(" B1 HEEL: {}", clam[0] as i8); eprintln!(" B2 resonance: {:07b} | contra={contra_flag}", resonance_bits); eprintln!(" B3 TWIG A: {:08b}", clam[2]); @@ -836,10 +843,7 @@ mod tests { contra_mean < payload_mean, "Contradiction errors should not move the spiral (contra={contra_mean}, payload={payload_mean})" ); - assert_eq!( - contra_mean, 0.0, - "Contradiction errors must produce zero manifold displacement" - ); + assert_eq!(contra_mean, 0.0, "Contradiction errors must produce zero manifold displacement"); } // ── Primality advantage ──────────────────────────────────────────── @@ -871,6 +875,10 @@ mod tests { } fn gcd(a: usize, b: usize) -> usize { - if b == 0 { a } else { gcd(b, a % b) } + if b == 0 { + a + } else { + gcd(b, a % b) + } } } diff --git a/crates/p64/Cargo.toml b/crates/p64/Cargo.toml index 83c918e5..11de901e 100644 --- a/crates/p64/Cargo.toml +++ b/crates/p64/Cargo.toml @@ -7,7 +7,7 @@ description = "Palette64: 64×64 BNN attention matrix from 8 phyllotactic HEEL p license = "MIT OR Apache-2.0" [dependencies] -phyllotactic-manifold = { path = "../phyllotactic-manifold" } +fractal = { path = "../fractal", default-features = false } [dev-dependencies] criterion = { version = "0.5", features = ["html_reports"] } diff --git a/src/hpc/p64_bridge.rs b/src/hpc/p64_bridge.rs index 75dc362d..36db28d2 100644 --- a/src/hpc/p64_bridge.rs +++ b/src/hpc/p64_bridge.rs @@ -21,7 +21,7 @@ pub use p64::{ AttentionResult, CombineMode, ContraMode, HeelPlanes, Palette3D, Palette64, ThinkingStyle, predicate, }; -pub use phyllotactic_manifold::consts as manifold_consts; +pub use fractal::consts as manifold_consts; // ============================================================================ // Section 1: SIMD manifold expansion @@ -204,7 +204,7 @@ pub fn attend_simd(palette: &Palette64, query: u64, gamma: u8) -> AttentionResul /// ``` pub fn resonance_to_nars(resonance_7bit: u8, contradiction: f64, max_contra: f64) -> NarsTruth { let (f, c) = - phyllotactic_manifold::seven_plus_one::nars_truth(resonance_7bit, contradiction, max_contra); + fractal::seven_plus_one::nars_truth(resonance_7bit, contradiction, max_contra); NarsTruth::new(f as f32, c as f32) }