aprender-core 0.31.2

Next-generation machine learning library in pure Rust
// CONTRACT: rope-kernel-v1.yaml
// HASH: sha256:f6e4c3b2a1d85970
// Generated by: pv probar --binding
// DO NOT EDIT — regenerate with `pv probar --binding`

use aprender::autograd::Tensor;
use aprender::nn::RotaryPositionEmbedding;
use proptest::prelude::*;

proptest! {
    /// Obligation: Norm preservation (invariant)
    /// Formal: |norm(RoPE(x, m)) - norm(x)| < eps
    #[test]
    fn prop_norm_preservation(
        data in proptest::collection::vec(-10.0f32..10.0, 4..5usize),
        pos in 0usize..32
    ) {
        // head_dim=4, batch=1, seq=1, heads=1
        let head_dim = 4;
        let rope = RotaryPositionEmbedding::new(head_dim, 64);
        let x = Tensor::new(&data[..head_dim], &[1, 1, 1, head_dim]);

        let y = rope.apply(&x, &[pos]);

        // Compute norms
        let x_data = x.data();
        let y_data = y.data();
        let x_norm: f32 = x_data.iter().map(|v| v * v).sum::<f32>().sqrt();
        let y_norm: f32 = y_data.iter().map(|v| v * v).sum::<f32>().sqrt();

        let diff = (x_norm - y_norm).abs();
        prop_assert!(
            diff < 1e-4,
            "norm not preserved: |{x_norm} - {y_norm}| = {diff}"
        );
    }

    /// Obligation: Relative position encoding (invariant)
    /// Formal: dot(RoPE(q,m), RoPE(k,n)) = f(q, k, m-n)
    #[test]
    fn prop_relative_position_encoding(
        q_data in proptest::collection::vec(-5.0f32..5.0, 4..5usize),
        k_data in proptest::collection::vec(-5.0f32..5.0, 4..5usize),
        m in 0usize..16,
        n in 0usize..16
    ) {
        let head_dim = 4;
        let max_seq = 64;
        let rope = RotaryPositionEmbedding::new(head_dim, max_seq);

        let q = Tensor::new(&q_data[..head_dim], &[1, 1, 1, head_dim]);
        let k = Tensor::new(&k_data[..head_dim], &[1, 1, 1, head_dim]);

        // dot(RoPE(q, m), RoPE(k, n))
        let q_rot = rope.apply(&q, &[m]);
        let k_rot = rope.apply(&k, &[n]);
        let dot1: f32 = q_rot.data().iter()
            .zip(k_rot.data().iter())
            .map(|(a, b)| a * b)
            .sum();

        // dot(RoPE(q, 0), RoPE(k, n-m)) if n >= m
        // dot(RoPE(q, m-n), RoPE(k, 0)) if m >= n
        let (pos_a, pos_b) = if n >= m { (0, n - m) } else { (m - n, 0) };
        let q_rot2 = rope.apply(&q, &[pos_a]);
        let k_rot2 = rope.apply(&k, &[pos_b]);
        let dot2: f32 = q_rot2.data().iter()
            .zip(k_rot2.data().iter())
            .map(|(a, b)| a * b)
            .sum();

        let diff = (dot1 - dot2).abs();
        prop_assert!(
            diff < 1e-3,
            "relative position: dot1={dot1}, dot2={dot2}, diff={diff}"
        );
    }

    /// Obligation: SIMD matches scalar (equivalence)
    #[test]
    #[ignore = "SIMD equivalence — trueno domain"]
    fn prop_simd_matches_scalar(
        _x in proptest::collection::vec(-5.0f32..5.0, 1..32usize)
    ) {
        // SIMD equivalence testing is trueno's responsibility
    }

    /// Obligation: Output bounded by input norm (bound)
    /// Formal: norm(RoPE(x, m)) <= norm(x) + eps
    #[test]
    fn prop_output_bounded_by_input_norm(
        data in proptest::collection::vec(-10.0f32..10.0, 4..5usize),
        pos in 0usize..32
    ) {
        let head_dim = 4;
        let rope = RotaryPositionEmbedding::new(head_dim, 64);
        let x = Tensor::new(&data[..head_dim], &[1, 1, 1, head_dim]);

        let y = rope.apply(&x, &[pos]);

        let x_norm: f32 = x.data().iter().map(|v| v * v).sum::<f32>().sqrt();
        let y_norm: f32 = y.data().iter().map(|v| v * v).sum::<f32>().sqrt();

        prop_assert!(
            y_norm <= x_norm + 1e-4,
            "output norm {y_norm} exceeds input norm {x_norm}"
        );
    }
}

/// RoPE at position 0 should be identity (cos(0)=1, sin(0)=0)
#[test]
fn rope_position_zero_is_identity() {
    let head_dim = 4;
    let rope = RotaryPositionEmbedding::new(head_dim, 64);
    let data = vec![1.0, 2.0, 3.0, 4.0f32];
    let x = Tensor::new(&data, &[1, 1, 1, head_dim]);

    let y = rope.apply(&x, &[0]);
    let y_data = y.data();

    for i in 0..head_dim {
        let diff = (data[i] - y_data[i]).abs();
        assert!(
            diff < 1e-5,
            "RoPE(x, 0)[{i}]={}, expected {}",
            y_data[i],
            data[i]
        );
    }
}