fib-quant 0.1.0-alpha.1

Experimental Rust implementation of the FibQuant radial-angular vector quantization core
Documentation
use fib_quant::{
    beta_d_k, DirectionMethod, FibQuantError, FibQuantProfileV1, NormFormat, RadiusMethod,
    SourceMode,
};

#[test]
fn profile_digest_is_stable_and_math_sensitive() {
    let base = FibQuantProfileV1::paper_default(8, 2, 8, 11).unwrap();
    let same = FibQuantProfileV1::paper_default(8, 2, 8, 11).unwrap();
    let mut changed = base.clone();
    changed.codebook_seed = changed.codebook_seed.wrapping_add(1);

    assert_eq!(base.digest().unwrap(), same.digest().unwrap());
    assert_ne!(base.digest().unwrap(), changed.digest().unwrap());
    assert!(base.digest().unwrap().starts_with("blake3:"));
}

#[test]
fn invalid_profile_parts_reject() {
    assert!(matches!(
        FibQuantProfileV1::paper_default(0, 2, 8, 0),
        Err(FibQuantError::ZeroDimension)
    ));
    assert!(matches!(
        FibQuantProfileV1::paper_default(8, 0, 8, 0),
        Err(FibQuantError::InvalidBlockDim { .. })
    ));
    assert!(matches!(
        FibQuantProfileV1::paper_default(10, 4, 8, 0),
        Err(FibQuantError::DimensionNotDivisible { .. })
    ));
    assert!(matches!(
        FibQuantProfileV1::paper_default(8, 2, 1, 0),
        Err(FibQuantError::InvalidCodebookSize(1))
    ));
}

#[test]
fn paper_and_wire_rates_are_distinct_when_needed() {
    let profile = FibQuantProfileV1::paper_default(8, 2, 3, 1).unwrap();
    assert_eq!(profile.wire_index_bits, 2);
    assert!((profile.paper_rate_bits_per_coord - (3.0f64).log2() / 2.0).abs() < 1e-12);
    assert_eq!(profile.wire_bits_per_coord, 1.0);
}

#[test]
fn beta_d_k_underflow_edge_stays_finite() {
    let beta = beta_d_k(4, 3).unwrap();
    assert!(beta.is_finite());
    assert!(beta > 0.0);
    assert!((beta - 0.7).abs() < 1.0e-12);
}

#[test]
fn profile_rate_tampering_rejects() {
    let mut profile = FibQuantProfileV1::paper_default(8, 2, 8, 11).unwrap();
    profile.paper_rate_bits_per_coord += 0.01;
    assert!(matches!(
        profile.validate(),
        Err(FibQuantError::CorruptPayload(_))
    ));

    let mut profile = FibQuantProfileV1::paper_default(8, 2, 8, 11).unwrap();
    profile.wire_bits_per_coord = f64::NAN;
    assert!(matches!(
        profile.validate(),
        Err(FibQuantError::CorruptPayload(_))
    ));
}

#[test]
fn profile_method_and_schema_tampering_rejects() {
    let mut profile = FibQuantProfileV1::paper_default(8, 2, 8, 11).unwrap();
    profile.schema_version = "wrong".into();
    assert!(matches!(
        profile.validate(),
        Err(FibQuantError::CorruptPayload(_))
    ));

    let mut profile = FibQuantProfileV1::paper_default(8, 2, 8, 11).unwrap();
    profile.radius_method = RadiusMethod::BetaQuantile;
    assert!(matches!(
        profile.validate(),
        Err(FibQuantError::CorruptPayload(_))
    ));

    let mut profile = FibQuantProfileV1::paper_default(9, 3, 8, 11).unwrap();
    profile.direction_method = DirectionMethod::RobertsKronecker;
    assert!(matches!(
        profile.validate(),
        Err(FibQuantError::CorruptPayload(_))
    ));
}

#[test]
fn profile_source_norm_and_lloyd_tampering_rejects() {
    let mut profile = FibQuantProfileV1::paper_default(8, 2, 8, 11).unwrap();
    profile.norm_format = NormFormat::F32Reference;
    assert!(matches!(
        profile.validate(),
        Err(FibQuantError::CorruptPayload(_))
    ));

    let mut profile = FibQuantProfileV1::paper_default(8, 2, 8, 11).unwrap();
    profile.source_mode = SourceMode::ReferenceGaussianProjection;
    assert!(matches!(
        profile.validate(),
        Err(FibQuantError::CorruptPayload(_))
    ));

    let mut profile = FibQuantProfileV1::paper_default(8, 2, 8, 11).unwrap();
    profile.rotation_algorithm_version = "wrong".into();
    assert!(matches!(
        profile.validate(),
        Err(FibQuantError::CorruptPayload(_))
    ));

    let mut profile = FibQuantProfileV1::paper_default(8, 2, 8, 11).unwrap();
    profile.lloyd_iterations = 0;
    assert!(matches!(
        profile.validate(),
        Err(FibQuantError::CorruptPayload(_))
    ));

    let mut profile = FibQuantProfileV1::paper_default(8, 2, 8, 11).unwrap();
    profile.training_samples = profile.codebook_size - 1;
    assert!(matches!(
        profile.validate(),
        Err(FibQuantError::CorruptPayload(_))
    ));
}