Files
wifi-densepose/crates/ruQu/src/syndrome.rs
ruv d803bfe2b1 Squashed 'vendor/ruvector/' content from commit b64c2172
git-subtree-dir: vendor/ruvector
git-subtree-split: b64c21726f2bb37286d9ee36a7869fef60cc6900
2026-02-28 14:39:40 -05:00

1657 lines
48 KiB
Rust

//! Syndrome Processing Module
//!
//! High-throughput data pipeline for quantum error syndrome ingestion, buffering,
//! and transformation. This module implements the Supporting Domain for the
//! Coherence Gate core domain.
//!
//! ## Components
//!
//! - [`DetectorBitmap`]: Packed bit representation for up to 1024 detectors
//! - [`SyndromeRound`]: Complete syndrome measurement cycle
//! - [`SyndromeBuffer`]: Ring buffer for syndrome history
//! - [`SyndromeDelta`]: Change between consecutive rounds
//!
//! ## Performance
//!
//! All types are designed for microsecond-scale operations:
//! - SIMD-friendly memory layouts (aligned, packed)
//! - Zero-copy where possible
//! - Preallocated buffers to avoid allocation on hot paths
use serde::{Deserialize, Serialize};
// ============================================================================
// DetectorBitmap - Packed bit representation for detectors
// ============================================================================
/// Number of u64 words in the bitmap (1024 detectors / 64 bits per word)
const BITMAP_WORDS: usize = 16;
/// Packed bit representation of detector values.
///
/// Efficiently stores up to 1024 detector values (one bit each) in a fixed-size
/// array of 16 u64 words. Operations are optimized for SIMD execution.
///
/// # Layout
///
/// ```text
/// bits[0]: detectors 0-63
/// bits[1]: detectors 64-127
/// ...
/// bits[15]: detectors 960-1023
/// ```
///
/// # Example
///
/// ```rust
/// use ruqu::syndrome::DetectorBitmap;
///
/// let mut bitmap = DetectorBitmap::new(128);
///
/// // Set some detectors as fired
/// bitmap.set(0, true);
/// bitmap.set(64, true);
/// bitmap.set(127, true);
///
/// assert_eq!(bitmap.fired_count(), 3);
/// assert!(bitmap.get(0));
/// assert!(!bitmap.get(1));
/// ```
#[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[repr(C, align(64))] // Cache-line aligned for SIMD
pub struct DetectorBitmap {
/// Packed detector bits (16 * 64 = 1024 detectors max)
bits: [u64; BITMAP_WORDS],
/// Number of detectors in use (may be less than 1024)
count: usize,
}
impl Default for DetectorBitmap {
fn default() -> Self {
Self::new(0)
}
}
impl std::fmt::Debug for DetectorBitmap {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DetectorBitmap")
.field("count", &self.count)
.field("fired", &self.fired_count())
.finish()
}
}
impl DetectorBitmap {
/// Creates a new bitmap with the specified number of detectors.
///
/// All detectors are initially set to 0 (not fired).
///
/// # Arguments
///
/// * `count` - Number of detectors (0 to 1024)
///
/// # Panics
///
/// Panics if `count` exceeds 1024.
///
/// # Example
///
/// ```rust
/// use ruqu::syndrome::DetectorBitmap;
///
/// let bitmap = DetectorBitmap::new(256);
/// assert_eq!(bitmap.detector_count(), 256);
/// assert_eq!(bitmap.fired_count(), 0);
/// ```
#[inline]
#[must_use]
pub fn new(count: usize) -> Self {
assert!(count <= BITMAP_WORDS * 64, "count exceeds maximum of 1024");
Self {
bits: [0u64; BITMAP_WORDS],
count,
}
}
/// Creates a bitmap from raw bits.
///
/// # Arguments
///
/// * `bits` - Array of 16 u64 words containing packed detector values
/// * `count` - Number of detectors in use (must be <= 1024)
///
/// # Panics
///
/// Panics if `count` exceeds 1024.
///
/// # Note
///
/// For consistent behavior, bits beyond `count` should be zero.
#[inline]
#[must_use]
pub const fn from_raw(bits: [u64; BITMAP_WORDS], count: usize) -> Self {
// SECURITY: Validate count to prevent out-of-bounds access in other methods
assert!(count <= BITMAP_WORDS * 64, "count exceeds maximum of 1024");
Self { bits, count }
}
/// Returns the raw bits array.
#[inline]
#[must_use]
pub const fn raw_bits(&self) -> &[u64; BITMAP_WORDS] {
&self.bits
}
/// Returns the number of detectors configured.
#[inline]
#[must_use]
pub const fn detector_count(&self) -> usize {
self.count
}
/// Sets the value of a detector.
///
/// # Arguments
///
/// * `idx` - Detector index (0 to count-1)
/// * `value` - true if detector fired, false otherwise
///
/// # Panics
///
/// Panics if `idx >= count` (in both debug and release builds).
///
/// # Example
///
/// ```rust
/// use ruqu::syndrome::DetectorBitmap;
///
/// let mut bitmap = DetectorBitmap::new(64);
/// bitmap.set(5, true);
/// assert!(bitmap.get(5));
///
/// bitmap.set(5, false);
/// assert!(!bitmap.get(5));
/// ```
#[inline]
pub fn set(&mut self, idx: usize, value: bool) {
// SECURITY: Use assert! not debug_assert! to ensure bounds check in release builds
assert!(
idx < self.count,
"detector index {} out of bounds (count: {})",
idx,
self.count
);
let word = idx / 64;
let bit = idx % 64;
if value {
self.bits[word] |= 1u64 << bit;
} else {
self.bits[word] &= !(1u64 << bit);
}
}
/// Gets the value of a detector.
///
/// # Arguments
///
/// * `idx` - Detector index (0 to count-1)
///
/// # Returns
///
/// `true` if the detector fired, `false` otherwise.
///
/// # Panics
///
/// Panics if `idx >= count` (in both debug and release builds).
///
/// # Example
///
/// ```rust
/// use ruqu::syndrome::DetectorBitmap;
///
/// let mut bitmap = DetectorBitmap::new(64);
/// bitmap.set(10, true);
///
/// assert!(bitmap.get(10));
/// assert!(!bitmap.get(0));
/// ```
#[inline]
#[must_use]
pub fn get(&self, idx: usize) -> bool {
// SECURITY: Use assert! not debug_assert! to ensure bounds check in release builds
assert!(
idx < self.count,
"detector index {} out of bounds (count: {})",
idx,
self.count
);
let word = idx / 64;
let bit = idx % 64;
(self.bits[word] >> bit) & 1 == 1
}
/// Returns the number of fired detectors (popcount).
///
/// Uses hardware popcount instructions when available.
///
/// # Example
///
/// ```rust
/// use ruqu::syndrome::DetectorBitmap;
///
/// let mut bitmap = DetectorBitmap::new(64);
/// bitmap.set(0, true);
/// bitmap.set(10, true);
/// bitmap.set(63, true);
///
/// assert_eq!(bitmap.fired_count(), 3);
/// ```
#[inline]
#[must_use]
pub fn fired_count(&self) -> usize {
self.popcount()
}
/// Returns the total popcount (number of set bits).
///
/// This is the same as `fired_count()` but with a more algorithmic name.
///
/// # Performance
///
/// - Uses hardware `popcnt` instruction on x86_64
/// - With `simd` feature, uses AVX2 parallel popcount for additional speedup
/// - Falls back to portable implementation on other architectures
#[inline]
#[must_use]
pub fn popcount(&self) -> usize {
// Calculate how many full words to count based on detector count
let full_words = self.count / 64;
let remaining_bits = self.count % 64;
#[cfg(all(feature = "simd", target_arch = "x86_64"))]
{
// AVX2 SIMD popcount using lookup table method
if is_x86_feature_detected!("avx2") && full_words >= 4 {
unsafe {
return self.popcount_avx2(full_words, remaining_bits);
}
}
}
// Scalar path with hardware popcnt
let mut total = 0usize;
// Count full words
for word in &self.bits[..full_words] {
total += word.count_ones() as usize;
}
// Count partial word if any
if remaining_bits > 0 && full_words < BITMAP_WORDS {
let mask = (1u64 << remaining_bits) - 1;
total += (self.bits[full_words] & mask).count_ones() as usize;
}
total
}
/// AVX2 SIMD popcount implementation
///
/// Uses the lookup table method: count bits in each nibble using vpshufb
#[cfg(all(feature = "simd", target_arch = "x86_64"))]
#[inline]
#[target_feature(enable = "avx2")]
unsafe fn popcount_avx2(&self, full_words: usize, remaining_bits: usize) -> usize {
use std::arch::x86_64::*;
// Lookup table for 4-bit popcount
let lookup = _mm256_setr_epi8(
0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4, 0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2,
3, 3, 4,
);
let low_mask = _mm256_set1_epi8(0x0f);
let mut total_vec = _mm256_setzero_si256();
let mut i = 0;
// Process 4 u64s (256 bits) at a time
while i + 4 <= full_words {
let data = _mm256_loadu_si256(self.bits.as_ptr().add(i) as *const __m256i);
// Split into low and high nibbles
let lo = _mm256_and_si256(data, low_mask);
let hi = _mm256_and_si256(_mm256_srli_epi16(data, 4), low_mask);
// Lookup popcount for each nibble
let popcnt_lo = _mm256_shuffle_epi8(lookup, lo);
let popcnt_hi = _mm256_shuffle_epi8(lookup, hi);
// Sum nibble popcounts (sad accumulates byte sums into u64)
let popcnt = _mm256_add_epi8(popcnt_lo, popcnt_hi);
total_vec =
_mm256_add_epi64(total_vec, _mm256_sad_epu8(popcnt, _mm256_setzero_si256()));
i += 4;
}
// Horizontal sum of the 4 u64 accumulators
let mut total = 0usize;
let mut buf = [0u64; 4];
_mm256_storeu_si256(buf.as_mut_ptr() as *mut __m256i, total_vec);
total += buf[0] as usize + buf[1] as usize + buf[2] as usize + buf[3] as usize;
// Handle remaining full words with scalar popcnt
while i < full_words {
total += self.bits[i].count_ones() as usize;
i += 1;
}
// Count partial word if any
if remaining_bits > 0 && full_words < BITMAP_WORDS {
let mask = (1u64 << remaining_bits) - 1;
total += (self.bits[full_words] & mask).count_ones() as usize;
}
total
}
/// Returns an iterator over fired detector indices.
///
/// # Example
///
/// ```rust
/// use ruqu::syndrome::DetectorBitmap;
///
/// let mut bitmap = DetectorBitmap::new(64);
/// bitmap.set(5, true);
/// bitmap.set(10, true);
/// bitmap.set(20, true);
///
/// let fired: Vec<usize> = bitmap.iter_fired().collect();
/// assert_eq!(fired, vec![5, 10, 20]);
/// ```
#[inline]
pub fn iter_fired(&self) -> FiredIterator<'_> {
FiredIterator {
bitmap: self,
word_idx: 0,
current_word: self.bits[0],
base_idx: 0,
}
}
/// Computes the XOR of two bitmaps.
///
/// The result shows which detectors changed state between the two bitmaps.
/// The count is set to the maximum of the two input counts.
///
/// # Performance
///
/// When the `simd` feature is enabled on x86_64, uses AVX2 instructions
/// for 4x speedup on the XOR operation.
///
/// # Example
///
/// ```rust
/// use ruqu::syndrome::DetectorBitmap;
///
/// let mut a = DetectorBitmap::new(64);
/// a.set(0, true);
/// a.set(5, true);
///
/// let mut b = DetectorBitmap::new(64);
/// b.set(0, true);
/// b.set(10, true);
///
/// let delta = a.xor(&b);
/// assert!(delta.get(5)); // Changed: was true, now false
/// assert!(delta.get(10)); // Changed: was false, now true
/// assert!(!delta.get(0)); // Unchanged: both true
/// ```
#[inline]
#[must_use]
pub fn xor(&self, other: &DetectorBitmap) -> DetectorBitmap {
let mut result = DetectorBitmap::new(self.count.max(other.count));
#[cfg(all(feature = "simd", target_arch = "x86_64"))]
{
// AVX2 SIMD: process 256 bits (4 u64s) at a time
if is_x86_feature_detected!("avx2") {
unsafe {
use std::arch::x86_64::*;
// Process first 8 words (512 bits) with two AVX2 operations
let a0 = _mm256_loadu_si256(self.bits.as_ptr() as *const __m256i);
let b0 = _mm256_loadu_si256(other.bits.as_ptr() as *const __m256i);
let r0 = _mm256_xor_si256(a0, b0);
_mm256_storeu_si256(result.bits.as_mut_ptr() as *mut __m256i, r0);
let a1 = _mm256_loadu_si256(self.bits.as_ptr().add(4) as *const __m256i);
let b1 = _mm256_loadu_si256(other.bits.as_ptr().add(4) as *const __m256i);
let r1 = _mm256_xor_si256(a1, b1);
_mm256_storeu_si256(result.bits.as_mut_ptr().add(4) as *mut __m256i, r1);
// Process remaining 8 words
let a2 = _mm256_loadu_si256(self.bits.as_ptr().add(8) as *const __m256i);
let b2 = _mm256_loadu_si256(other.bits.as_ptr().add(8) as *const __m256i);
let r2 = _mm256_xor_si256(a2, b2);
_mm256_storeu_si256(result.bits.as_mut_ptr().add(8) as *mut __m256i, r2);
let a3 = _mm256_loadu_si256(self.bits.as_ptr().add(12) as *const __m256i);
let b3 = _mm256_loadu_si256(other.bits.as_ptr().add(12) as *const __m256i);
let r3 = _mm256_xor_si256(a3, b3);
_mm256_storeu_si256(result.bits.as_mut_ptr().add(12) as *mut __m256i, r3);
return result;
}
}
}
// Scalar fallback: SIMD-friendly unrolled XOR
for i in 0..BITMAP_WORDS {
result.bits[i] = self.bits[i] ^ other.bits[i];
}
result
}
/// Computes the AND of two bitmaps.
///
/// Returns detectors that are fired in both bitmaps.
///
/// # Performance
///
/// With `simd` feature on x86_64, uses AVX2 for vectorized AND.
#[inline]
#[must_use]
pub fn and(&self, other: &DetectorBitmap) -> DetectorBitmap {
let mut result = DetectorBitmap::new(self.count.min(other.count));
#[cfg(all(feature = "simd", target_arch = "x86_64"))]
{
if is_x86_feature_detected!("avx2") {
unsafe {
use std::arch::x86_64::*;
for i in (0..BITMAP_WORDS).step_by(4) {
let a = _mm256_loadu_si256(self.bits.as_ptr().add(i) as *const __m256i);
let b = _mm256_loadu_si256(other.bits.as_ptr().add(i) as *const __m256i);
let r = _mm256_and_si256(a, b);
_mm256_storeu_si256(result.bits.as_mut_ptr().add(i) as *mut __m256i, r);
}
return result;
}
}
}
for i in 0..BITMAP_WORDS {
result.bits[i] = self.bits[i] & other.bits[i];
}
result
}
/// Computes the OR of two bitmaps.
///
/// Returns detectors that are fired in either bitmap.
///
/// # Performance
///
/// With `simd` feature on x86_64, uses AVX2 for vectorized OR.
#[inline]
#[must_use]
pub fn or(&self, other: &DetectorBitmap) -> DetectorBitmap {
let mut result = DetectorBitmap::new(self.count.max(other.count));
#[cfg(all(feature = "simd", target_arch = "x86_64"))]
{
if is_x86_feature_detected!("avx2") {
unsafe {
use std::arch::x86_64::*;
for i in (0..BITMAP_WORDS).step_by(4) {
let a = _mm256_loadu_si256(self.bits.as_ptr().add(i) as *const __m256i);
let b = _mm256_loadu_si256(other.bits.as_ptr().add(i) as *const __m256i);
let r = _mm256_or_si256(a, b);
_mm256_storeu_si256(result.bits.as_mut_ptr().add(i) as *mut __m256i, r);
}
return result;
}
}
}
for i in 0..BITMAP_WORDS {
result.bits[i] = self.bits[i] | other.bits[i];
}
result
}
/// Computes the NOT of this bitmap (inverts all bits).
///
/// # Performance
///
/// With `simd` feature on x86_64, uses AVX2 for vectorized NOT.
#[inline]
#[must_use]
pub fn not(&self) -> DetectorBitmap {
let mut result = DetectorBitmap::new(self.count);
#[cfg(all(feature = "simd", target_arch = "x86_64"))]
{
if is_x86_feature_detected!("avx2") {
unsafe {
use std::arch::x86_64::*;
let ones = _mm256_set1_epi64x(-1i64);
for i in (0..BITMAP_WORDS).step_by(4) {
let a = _mm256_loadu_si256(self.bits.as_ptr().add(i) as *const __m256i);
let r = _mm256_xor_si256(a, ones);
_mm256_storeu_si256(result.bits.as_mut_ptr().add(i) as *mut __m256i, r);
}
// Mask off bits beyond count
let full_words = self.count / 64;
let remaining_bits = self.count % 64;
if remaining_bits > 0 && full_words < BITMAP_WORDS {
let mask = (1u64 << remaining_bits) - 1;
result.bits[full_words] &= mask;
}
// Zero out words beyond count
for i in (full_words + 1)..BITMAP_WORDS {
result.bits[i] = 0;
}
return result;
}
}
}
let full_words = self.count / 64;
let remaining_bits = self.count % 64;
for i in 0..full_words {
result.bits[i] = !self.bits[i];
}
if remaining_bits > 0 && full_words < BITMAP_WORDS {
let mask = (1u64 << remaining_bits) - 1;
result.bits[full_words] = (!self.bits[full_words]) & mask;
}
result
}
/// Returns true if no detectors are fired.
#[inline]
#[must_use]
pub fn is_empty(&self) -> bool {
self.bits.iter().all(|&w| w == 0)
}
/// Clears all detector values to zero.
#[inline]
pub fn clear(&mut self) {
self.bits = [0u64; BITMAP_WORDS];
}
}
/// Iterator over fired detector indices.
pub struct FiredIterator<'a> {
bitmap: &'a DetectorBitmap,
word_idx: usize,
current_word: u64,
base_idx: usize,
}
impl<'a> Iterator for FiredIterator<'a> {
type Item = usize;
#[inline]
fn next(&mut self) -> Option<Self::Item> {
loop {
if self.current_word != 0 {
// Find lowest set bit
let trailing = self.current_word.trailing_zeros() as usize;
let idx = self.base_idx + trailing;
// Check if within detector count
if idx >= self.bitmap.count {
return None;
}
// Clear the bit we just found
self.current_word &= self.current_word - 1;
return Some(idx);
}
// Move to next word
self.word_idx += 1;
if self.word_idx >= BITMAP_WORDS {
return None;
}
self.base_idx = self.word_idx * 64;
// Check if we've passed the detector count
if self.base_idx >= self.bitmap.count {
return None;
}
self.current_word = self.bitmap.bits[self.word_idx];
}
}
fn size_hint(&self) -> (usize, Option<usize>) {
// Upper bound is remaining popcount
let remaining_popcount = self.bitmap.popcount();
(0, Some(remaining_popcount))
}
}
// ============================================================================
// SyndromeRound - Complete syndrome measurement
// ============================================================================
/// A complete syndrome measurement cycle.
///
/// Represents all syndrome data collected in one measurement round (typically 1μs).
/// This is the aggregate root for syndrome data, containing the detector bitmap
/// and associated metadata.
///
/// # Memory Layout
///
/// Total size: 152 bytes (with 64-byte aligned DetectorBitmap)
///
/// # Example
///
/// ```rust
/// use ruqu::syndrome::{DetectorBitmap, SyndromeRound};
///
/// let mut detectors = DetectorBitmap::new(64);
/// detectors.set(5, true);
/// detectors.set(10, true);
///
/// let round = SyndromeRound {
/// round_id: 12345,
/// cycle: 1000,
/// timestamp: 1705500000000,
/// detectors,
/// source_tile: 0,
/// };
///
/// assert_eq!(round.fired_count(), 2);
/// ```
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct SyndromeRound {
/// Unique identifier for this round (monotonically increasing per tile)
pub round_id: u64,
/// Quantum cycle number (global clock)
pub cycle: u64,
/// Hardware timestamp in nanoseconds
pub timestamp: u64,
/// Detector measurement outcomes
pub detectors: DetectorBitmap,
/// Source tile identifier (0-255)
pub source_tile: u8,
}
impl SyndromeRound {
/// Creates a new syndrome round with the given parameters.
#[inline]
#[must_use]
pub fn new(
round_id: u64,
cycle: u64,
timestamp: u64,
detectors: DetectorBitmap,
source_tile: u8,
) -> Self {
Self {
round_id,
cycle,
timestamp,
detectors,
source_tile,
}
}
/// Returns the number of fired detectors in this round.
#[inline]
#[must_use]
pub fn fired_count(&self) -> usize {
self.detectors.fired_count()
}
/// Returns an iterator over fired detector indices.
#[inline]
pub fn iter_fired(&self) -> FiredIterator<'_> {
self.detectors.iter_fired()
}
/// Computes the delta to another round.
///
/// # Example
///
/// ```rust
/// use ruqu::syndrome::{DetectorBitmap, SyndromeRound};
///
/// let mut d1 = DetectorBitmap::new(64);
/// d1.set(0, true);
/// d1.set(5, true);
///
/// let round1 = SyndromeRound::new(1, 100, 1000, d1, 0);
///
/// let mut d2 = DetectorBitmap::new(64);
/// d2.set(5, true);
/// d2.set(10, true);
///
/// let round2 = SyndromeRound::new(2, 101, 1001, d2, 0);
///
/// let delta = round1.delta_to(&round2);
/// assert_eq!(delta.flip_count(), 2); // 0 cleared, 10 fired
/// ```
#[inline]
#[must_use]
pub fn delta_to(&self, other: &SyndromeRound) -> SyndromeDelta {
SyndromeDelta::compute(self, other)
}
}
// ============================================================================
// SyndromeBuffer - Ring buffer for syndrome history
// ============================================================================
/// Statistics about buffer state.
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct BufferStatistics {
/// Total rounds pushed to buffer
pub total_rounds: u64,
/// Number of rounds currently in buffer
pub current_size: usize,
/// Buffer capacity
pub capacity: usize,
/// Number of rounds evicted (overwritten)
pub evicted_rounds: u64,
/// Average firing rate across recent rounds
pub avg_firing_rate: f64,
/// Maximum firing count seen
pub max_firing_count: usize,
/// Oldest round ID in buffer
pub oldest_round_id: Option<u64>,
/// Newest round ID in buffer
pub newest_round_id: Option<u64>,
}
/// Ring buffer holding recent syndrome history.
///
/// Provides efficient O(1) push and windowed access to recent syndrome rounds.
/// When the buffer is full, oldest entries are overwritten.
///
/// # Capacity
///
/// The buffer has a fixed capacity set at creation. Typical values:
/// - 1024 rounds for 1ms history at 1MHz syndrome rate
/// - 4096 rounds for longer-term analysis
///
/// # Thread Safety
///
/// This buffer is not thread-safe. Use external synchronization or
/// one buffer per tile (recommended).
///
/// # Example
///
/// ```rust
/// use ruqu::syndrome::{DetectorBitmap, SyndromeRound, SyndromeBuffer};
///
/// let mut buffer = SyndromeBuffer::new(1024);
///
/// // Push rounds
/// for i in 0..100 {
/// let mut detectors = DetectorBitmap::new(64);
/// if i % 10 == 0 {
/// detectors.set(i % 64, true);
/// }
/// let round = SyndromeRound::new(i as u64, i as u64, i as u64 * 1000, detectors, 0);
/// buffer.push(round);
/// }
///
/// // Get recent window
/// let window = buffer.window(10);
/// assert_eq!(window.len(), 10);
///
/// // Access by round ID
/// if let Some(round) = buffer.get(95) {
/// assert_eq!(round.round_id, 95);
/// }
/// ```
#[derive(Clone, Debug)]
pub struct SyndromeBuffer {
/// Buffer capacity (fixed at creation)
capacity: usize,
/// Preallocated round storage
rounds: Vec<Option<SyndromeRound>>,
/// Current write index (wraps at capacity)
write_index: usize,
/// Number of valid entries
valid_count: usize,
/// Watermark: oldest round ID guaranteed to be in buffer
watermark: u64,
/// Total rounds pushed (for statistics)
total_pushed: u64,
/// Running sum of firing counts (for average)
firing_sum: u64,
/// Maximum firing count seen
max_firing: usize,
}
impl SyndromeBuffer {
/// Creates a new buffer with the specified capacity.
///
/// # Arguments
///
/// * `capacity` - Maximum number of rounds to store
///
/// # Panics
///
/// Panics if capacity is 0.
#[must_use]
pub fn new(capacity: usize) -> Self {
assert!(capacity > 0, "buffer capacity must be positive");
Self {
capacity,
rounds: vec![None; capacity],
write_index: 0,
valid_count: 0,
watermark: 0,
total_pushed: 0,
firing_sum: 0,
max_firing: 0,
}
}
/// Returns the buffer capacity.
#[inline]
#[must_use]
pub const fn capacity(&self) -> usize {
self.capacity
}
/// Returns the number of valid entries in the buffer.
#[inline]
#[must_use]
pub const fn len(&self) -> usize {
self.valid_count
}
/// Returns true if the buffer is empty.
#[inline]
#[must_use]
pub const fn is_empty(&self) -> bool {
self.valid_count == 0
}
/// Returns true if the buffer is full.
#[inline]
#[must_use]
pub const fn is_full(&self) -> bool {
self.valid_count >= self.capacity
}
/// Returns the current watermark (oldest retained round ID).
#[inline]
#[must_use]
pub const fn watermark(&self) -> u64 {
self.watermark
}
/// Pushes a new round into the buffer.
///
/// If the buffer is full, the oldest entry is evicted.
///
/// # Arguments
///
/// * `round` - The syndrome round to add
///
/// # Example
///
/// ```rust
/// use ruqu::syndrome::{DetectorBitmap, SyndromeRound, SyndromeBuffer};
///
/// let mut buffer = SyndromeBuffer::new(100);
/// let round = SyndromeRound::new(1, 1, 1000, DetectorBitmap::new(64), 0);
/// buffer.push(round);
///
/// assert_eq!(buffer.len(), 1);
/// ```
#[inline]
pub fn push(&mut self, round: SyndromeRound) {
// Update statistics
let fired = round.fired_count();
self.firing_sum += fired as u64;
self.max_firing = self.max_firing.max(fired);
self.total_pushed += 1;
// Update watermark if we're overwriting
if self.valid_count >= self.capacity {
if let Some(ref old) = self.rounds[self.write_index] {
// Advance watermark past the evicted round
self.watermark = old.round_id + 1;
}
}
// Store the round
self.rounds[self.write_index] = Some(round);
// Advance write pointer
self.write_index = (self.write_index + 1) % self.capacity;
// Update valid count
if self.valid_count < self.capacity {
self.valid_count += 1;
}
}
/// Returns a window of the most recent rounds.
///
/// # Arguments
///
/// * `size` - Number of rounds to retrieve (clamped to available)
///
/// # Returns
///
/// A vector of the most recent `size` rounds, oldest first.
///
/// # Example
///
/// ```rust
/// use ruqu::syndrome::{DetectorBitmap, SyndromeRound, SyndromeBuffer};
///
/// let mut buffer = SyndromeBuffer::new(100);
/// for i in 0..50 {
/// let round = SyndromeRound::new(i, i, i * 1000, DetectorBitmap::new(64), 0);
/// buffer.push(round);
/// }
///
/// let window = buffer.window(10);
/// assert_eq!(window.len(), 10);
/// assert_eq!(window[0].round_id, 40); // Oldest in window
/// assert_eq!(window[9].round_id, 49); // Newest in window
/// ```
#[must_use]
pub fn window(&self, size: usize) -> Vec<&SyndromeRound> {
let actual_size = size.min(self.valid_count);
if actual_size == 0 {
return Vec::new();
}
let mut result = Vec::with_capacity(actual_size);
// Calculate start index (oldest in window)
let start = if self.write_index >= actual_size {
self.write_index - actual_size
} else {
self.capacity - (actual_size - self.write_index)
};
for i in 0..actual_size {
let idx = (start + i) % self.capacity;
if let Some(ref round) = self.rounds[idx] {
result.push(round);
}
}
result
}
/// Retrieves a round by its round ID.
///
/// # Arguments
///
/// * `round_id` - The round ID to look up
///
/// # Returns
///
/// `Some(&SyndromeRound)` if found, `None` if not in buffer.
///
/// # Performance
///
/// O(1) if the buffer maintains sequential round IDs, otherwise O(n).
#[must_use]
pub fn get(&self, round_id: u64) -> Option<&SyndromeRound> {
if self.valid_count == 0 || round_id < self.watermark {
return None;
}
// Try direct index first (assumes sequential round IDs)
if let Some(ref newest) =
self.rounds[(self.write_index + self.capacity - 1) % self.capacity]
{
if round_id <= newest.round_id {
let offset = (newest.round_id - round_id) as usize;
if offset < self.valid_count {
let idx = if self.write_index > offset {
self.write_index - 1 - offset
} else {
self.capacity - 1 - (offset - self.write_index)
};
if let Some(ref round) = self.rounds[idx] {
if round.round_id == round_id {
return Some(round);
}
}
}
}
}
// Fall back to linear search
for i in 0..self.valid_count {
let idx = if self.write_index > i {
self.write_index - 1 - i
} else {
self.capacity - 1 - (i - self.write_index)
};
if let Some(ref round) = self.rounds[idx] {
if round.round_id == round_id {
return Some(round);
}
}
}
None
}
/// Returns buffer statistics.
#[must_use]
pub fn statistics(&self) -> BufferStatistics {
let (oldest_id, newest_id) = if self.valid_count > 0 {
let oldest_idx = if self.valid_count < self.capacity {
0
} else {
self.write_index
};
let newest_idx = (self.write_index + self.capacity - 1) % self.capacity;
let oldest = self.rounds[oldest_idx].as_ref().map(|r| r.round_id);
let newest = self.rounds[newest_idx].as_ref().map(|r| r.round_id);
(oldest, newest)
} else {
(None, None)
};
let avg_firing = if self.total_pushed > 0 {
self.firing_sum as f64 / self.total_pushed as f64
} else {
0.0
};
let evicted = if self.total_pushed > self.capacity as u64 {
self.total_pushed - self.capacity as u64
} else {
0
};
BufferStatistics {
total_rounds: self.total_pushed,
current_size: self.valid_count,
capacity: self.capacity,
evicted_rounds: evicted,
avg_firing_rate: avg_firing,
max_firing_count: self.max_firing,
oldest_round_id: oldest_id,
newest_round_id: newest_id,
}
}
/// Clears the buffer, removing all entries.
pub fn clear(&mut self) {
for round in &mut self.rounds {
*round = None;
}
self.write_index = 0;
self.valid_count = 0;
self.total_pushed = 0;
self.firing_sum = 0;
self.max_firing = 0;
self.watermark = 0;
}
/// Returns an iterator over all valid rounds, oldest first.
pub fn iter(&self) -> impl Iterator<Item = &SyndromeRound> {
let start = if self.valid_count < self.capacity {
0
} else {
self.write_index
};
(0..self.valid_count)
.map(move |i| (start + i) % self.capacity)
.filter_map(move |idx| self.rounds[idx].as_ref())
}
}
// ============================================================================
// SyndromeDelta - Change between rounds
// ============================================================================
/// Represents the change in syndrome state between two rounds.
///
/// Used to track which detectors flipped between consecutive measurements,
/// enabling efficient change detection and activity monitoring.
///
/// # Example
///
/// ```rust
/// use ruqu::syndrome::{DetectorBitmap, SyndromeRound, SyndromeDelta};
///
/// let mut d1 = DetectorBitmap::new(64);
/// d1.set(0, true);
/// d1.set(5, true);
///
/// let mut d2 = DetectorBitmap::new(64);
/// d2.set(5, true);
/// d2.set(10, true);
///
/// let round1 = SyndromeRound::new(1, 100, 1000, d1, 0);
/// let round2 = SyndromeRound::new(2, 101, 2000, d2, 0);
///
/// let delta = SyndromeDelta::compute(&round1, &round2);
///
/// assert_eq!(delta.from_round, 1);
/// assert_eq!(delta.to_round, 2);
/// assert_eq!(delta.flip_count(), 2); // Detectors 0 and 10 flipped
/// assert!(!delta.is_quiet());
/// ```
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct SyndromeDelta {
/// Source round ID
pub from_round: u64,
/// Target round ID
pub to_round: u64,
/// Bitmap of flipped detectors (XOR of the two rounds)
pub flipped: DetectorBitmap,
}
impl SyndromeDelta {
/// Computes the delta between two syndrome rounds.
///
/// # Arguments
///
/// * `from` - The earlier round
/// * `to` - The later round
#[inline]
#[must_use]
pub fn compute(from: &SyndromeRound, to: &SyndromeRound) -> Self {
Self {
from_round: from.round_id,
to_round: to.round_id,
flipped: from.detectors.xor(&to.detectors),
}
}
/// Creates a delta from raw components.
#[inline]
#[must_use]
pub const fn new(from_round: u64, to_round: u64, flipped: DetectorBitmap) -> Self {
Self {
from_round,
to_round,
flipped,
}
}
/// Returns true if no detectors changed state.
///
/// A "quiet" delta indicates the syndrome is stable.
#[inline]
#[must_use]
pub fn is_quiet(&self) -> bool {
self.flipped.is_empty()
}
/// Returns the number of detectors that flipped.
#[inline]
#[must_use]
pub fn flip_count(&self) -> usize {
self.flipped.popcount()
}
/// Returns the activity level as a ratio of flipped detectors.
///
/// Activity level = flipped_count / total_detectors
///
/// # Returns
///
/// Value between 0.0 (no activity) and 1.0 (all detectors flipped).
#[inline]
#[must_use]
pub fn activity_level(&self) -> f64 {
let count = self.flipped.detector_count();
if count == 0 {
return 0.0;
}
self.flipped.popcount() as f64 / count as f64
}
/// Returns an iterator over flipped detector indices.
#[inline]
pub fn iter_flipped(&self) -> FiredIterator<'_> {
self.flipped.iter_fired()
}
/// Returns the temporal span of this delta.
///
/// # Returns
///
/// Number of rounds between from and to (to_round - from_round).
#[inline]
#[must_use]
pub const fn span(&self) -> u64 {
self.to_round.saturating_sub(self.from_round)
}
}
// ============================================================================
// Tests
// ============================================================================
#[cfg(test)]
mod tests {
use super::*;
// ---------- DetectorBitmap tests ----------
#[test]
fn test_bitmap_new() {
let bitmap = DetectorBitmap::new(64);
assert_eq!(bitmap.detector_count(), 64);
assert_eq!(bitmap.fired_count(), 0);
assert!(bitmap.is_empty());
}
#[test]
fn test_bitmap_set_get() {
let mut bitmap = DetectorBitmap::new(128);
bitmap.set(0, true);
bitmap.set(63, true);
bitmap.set(64, true);
bitmap.set(127, true);
assert!(bitmap.get(0));
assert!(bitmap.get(63));
assert!(bitmap.get(64));
assert!(bitmap.get(127));
assert!(!bitmap.get(1));
assert!(!bitmap.get(100));
}
#[test]
fn test_bitmap_fired_count() {
let mut bitmap = DetectorBitmap::new(256);
bitmap.set(0, true);
bitmap.set(10, true);
bitmap.set(100, true);
bitmap.set(200, true);
assert_eq!(bitmap.fired_count(), 4);
assert!(!bitmap.is_empty());
}
#[test]
fn test_bitmap_iter_fired() {
let mut bitmap = DetectorBitmap::new(128);
bitmap.set(5, true);
bitmap.set(64, true);
bitmap.set(100, true);
let fired: Vec<usize> = bitmap.iter_fired().collect();
assert_eq!(fired, vec![5, 64, 100]);
}
#[test]
fn test_bitmap_xor() {
let mut a = DetectorBitmap::new(64);
a.set(0, true);
a.set(5, true);
a.set(10, true);
let mut b = DetectorBitmap::new(64);
b.set(5, true);
b.set(10, true);
b.set(20, true);
let result = a.xor(&b);
// 0: a=1, b=0 -> 1
// 5: a=1, b=1 -> 0
// 10: a=1, b=1 -> 0
// 20: a=0, b=1 -> 1
assert!(result.get(0));
assert!(!result.get(5));
assert!(!result.get(10));
assert!(result.get(20));
assert_eq!(result.fired_count(), 2);
}
#[test]
fn test_bitmap_and_or() {
let mut a = DetectorBitmap::new(64);
a.set(0, true);
a.set(5, true);
let mut b = DetectorBitmap::new(64);
b.set(5, true);
b.set(10, true);
let and_result = a.and(&b);
assert!(!and_result.get(0));
assert!(and_result.get(5));
assert!(!and_result.get(10));
assert_eq!(and_result.fired_count(), 1);
let or_result = a.or(&b);
assert!(or_result.get(0));
assert!(or_result.get(5));
assert!(or_result.get(10));
assert_eq!(or_result.fired_count(), 3);
}
#[test]
fn test_bitmap_clear() {
let mut bitmap = DetectorBitmap::new(64);
bitmap.set(0, true);
bitmap.set(10, true);
assert_eq!(bitmap.fired_count(), 2);
bitmap.clear();
assert_eq!(bitmap.fired_count(), 0);
assert!(bitmap.is_empty());
}
#[test]
fn test_bitmap_large() {
let mut bitmap = DetectorBitmap::new(1024);
// Set every 100th detector
for i in (0..1024).step_by(100) {
bitmap.set(i, true);
}
let fired: Vec<usize> = bitmap.iter_fired().collect();
assert_eq!(fired.len(), 11); // 0, 100, 200, ..., 1000
}
#[test]
#[should_panic(expected = "count exceeds maximum")]
fn test_bitmap_overflow() {
DetectorBitmap::new(2000);
}
// ---------- SyndromeRound tests ----------
#[test]
fn test_round_new() {
let detectors = DetectorBitmap::new(64);
let round = SyndromeRound::new(1, 100, 1000000, detectors, 5);
assert_eq!(round.round_id, 1);
assert_eq!(round.cycle, 100);
assert_eq!(round.timestamp, 1000000);
assert_eq!(round.source_tile, 5);
assert_eq!(round.fired_count(), 0);
}
#[test]
fn test_round_delta_to() {
let mut d1 = DetectorBitmap::new(64);
d1.set(0, true);
d1.set(5, true);
let mut d2 = DetectorBitmap::new(64);
d2.set(5, true);
d2.set(10, true);
let round1 = SyndromeRound::new(1, 100, 1000, d1, 0);
let round2 = SyndromeRound::new(2, 101, 2000, d2, 0);
let delta = round1.delta_to(&round2);
assert_eq!(delta.from_round, 1);
assert_eq!(delta.to_round, 2);
assert_eq!(delta.flip_count(), 2); // 0 and 10 flipped
}
// ---------- SyndromeBuffer tests ----------
#[test]
fn test_buffer_new() {
let buffer = SyndromeBuffer::new(100);
assert_eq!(buffer.capacity(), 100);
assert_eq!(buffer.len(), 0);
assert!(buffer.is_empty());
assert!(!buffer.is_full());
}
#[test]
fn test_buffer_push() {
let mut buffer = SyndromeBuffer::new(10);
for i in 0..5 {
let round = SyndromeRound::new(i, i, i * 1000, DetectorBitmap::new(64), 0);
buffer.push(round);
}
assert_eq!(buffer.len(), 5);
assert!(!buffer.is_full());
}
#[test]
fn test_buffer_overflow() {
let mut buffer = SyndromeBuffer::new(5);
for i in 0..10 {
let round = SyndromeRound::new(i, i, i * 1000, DetectorBitmap::new(64), 0);
buffer.push(round);
}
assert_eq!(buffer.len(), 5);
assert!(buffer.is_full());
// Oldest should be round 5 (rounds 0-4 evicted)
assert!(buffer.get(4).is_none());
assert!(buffer.get(5).is_some());
assert!(buffer.get(9).is_some());
}
#[test]
fn test_buffer_window() {
let mut buffer = SyndromeBuffer::new(100);
for i in 0..50 {
let round = SyndromeRound::new(i, i, i * 1000, DetectorBitmap::new(64), 0);
buffer.push(round);
}
let window = buffer.window(10);
assert_eq!(window.len(), 10);
assert_eq!(window[0].round_id, 40);
assert_eq!(window[9].round_id, 49);
}
#[test]
fn test_buffer_window_larger_than_buffer() {
let mut buffer = SyndromeBuffer::new(100);
for i in 0..5 {
let round = SyndromeRound::new(i, i, i * 1000, DetectorBitmap::new(64), 0);
buffer.push(round);
}
let window = buffer.window(100);
assert_eq!(window.len(), 5);
}
#[test]
fn test_buffer_get() {
let mut buffer = SyndromeBuffer::new(100);
for i in 0..50 {
let round = SyndromeRound::new(i, i, i * 1000, DetectorBitmap::new(64), 0);
buffer.push(round);
}
assert!(buffer.get(0).is_some());
assert!(buffer.get(49).is_some());
assert!(buffer.get(50).is_none());
assert!(buffer.get(1000).is_none());
}
#[test]
fn test_buffer_statistics() {
let mut buffer = SyndromeBuffer::new(10);
for i in 0..20u64 {
let mut detectors = DetectorBitmap::new(64);
for j in 0..(i % 5) as usize {
detectors.set(j, true);
}
let round = SyndromeRound::new(i, i, i * 1000, detectors, 0);
buffer.push(round);
}
let stats = buffer.statistics();
assert_eq!(stats.total_rounds, 20);
assert_eq!(stats.current_size, 10);
assert_eq!(stats.capacity, 10);
assert_eq!(stats.evicted_rounds, 10);
assert!(stats.avg_firing_rate > 0.0);
}
#[test]
fn test_buffer_clear() {
let mut buffer = SyndromeBuffer::new(10);
for i in 0..5 {
let round = SyndromeRound::new(i, i, i * 1000, DetectorBitmap::new(64), 0);
buffer.push(round);
}
buffer.clear();
assert_eq!(buffer.len(), 0);
assert!(buffer.is_empty());
}
#[test]
fn test_buffer_iter() {
let mut buffer = SyndromeBuffer::new(100);
for i in 0..10 {
let round = SyndromeRound::new(i, i, i * 1000, DetectorBitmap::new(64), 0);
buffer.push(round);
}
let ids: Vec<u64> = buffer.iter().map(|r| r.round_id).collect();
assert_eq!(ids, vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
}
#[test]
#[should_panic(expected = "capacity must be positive")]
fn test_buffer_zero_capacity() {
SyndromeBuffer::new(0);
}
// ---------- SyndromeDelta tests ----------
#[test]
fn test_delta_compute() {
let mut d1 = DetectorBitmap::new(64);
d1.set(0, true);
d1.set(5, true);
let mut d2 = DetectorBitmap::new(64);
d2.set(5, true);
d2.set(10, true);
let round1 = SyndromeRound::new(1, 100, 1000, d1, 0);
let round2 = SyndromeRound::new(2, 101, 2000, d2, 0);
let delta = SyndromeDelta::compute(&round1, &round2);
assert_eq!(delta.from_round, 1);
assert_eq!(delta.to_round, 2);
assert_eq!(delta.flip_count(), 2);
assert!(!delta.is_quiet());
}
#[test]
fn test_delta_quiet() {
let mut d1 = DetectorBitmap::new(64);
d1.set(5, true);
let mut d2 = DetectorBitmap::new(64);
d2.set(5, true);
let round1 = SyndromeRound::new(1, 100, 1000, d1, 0);
let round2 = SyndromeRound::new(2, 101, 2000, d2, 0);
let delta = SyndromeDelta::compute(&round1, &round2);
assert!(delta.is_quiet());
assert_eq!(delta.flip_count(), 0);
assert_eq!(delta.activity_level(), 0.0);
}
#[test]
fn test_delta_activity_level() {
let mut d1 = DetectorBitmap::new(100);
// All zeros
let mut d2 = DetectorBitmap::new(100);
for i in 0..10 {
d2.set(i, true);
}
let round1 = SyndromeRound::new(1, 100, 1000, d1, 0);
let round2 = SyndromeRound::new(2, 101, 2000, d2, 0);
let delta = SyndromeDelta::compute(&round1, &round2);
assert_eq!(delta.flip_count(), 10);
assert!((delta.activity_level() - 0.1).abs() < 0.001);
}
#[test]
fn test_delta_span() {
let d1 = DetectorBitmap::new(64);
let d2 = DetectorBitmap::new(64);
let round1 = SyndromeRound::new(100, 100, 1000, d1, 0);
let round2 = SyndromeRound::new(110, 110, 2000, d2, 0);
let delta = SyndromeDelta::compute(&round1, &round2);
assert_eq!(delta.span(), 10);
}
#[test]
fn test_delta_iter_flipped() {
let mut d1 = DetectorBitmap::new(64);
d1.set(0, true);
let mut d2 = DetectorBitmap::new(64);
d2.set(10, true);
d2.set(20, true);
let round1 = SyndromeRound::new(1, 100, 1000, d1, 0);
let round2 = SyndromeRound::new(2, 101, 2000, d2, 0);
let delta = SyndromeDelta::compute(&round1, &round2);
let flipped: Vec<usize> = delta.iter_flipped().collect();
assert_eq!(flipped, vec![0, 10, 20]);
}
}