use crate::spatial::octree::Octree;
use crate::storage::data_structures::NodePoint;
use glam::Vec3;
use std::collections::HashMap;
pub struct Token {
pub id: u64,
pub position: Vec3,
pub time_step: u64,
}
pub struct AttentionResult {
pub query_id: u64,
pub attended: Vec<(u64, f32)>,
}
pub struct AttentionStats {
pub n_queries: usize,
pub mean_attended: f32,
pub min_attended: usize,
pub max_attended: usize,
pub total_pairs: usize,
pub attended_pairs: usize,
pub sparsity: f32,
}
fn to_node_point(t: &Token) -> NodePoint {
NodePoint {
id: t.id,
x: t.position.x,
y: t.position.y,
z: t.position.z,
}
}
fn build_key_octree(queries: &[Token], keys: &[Token]) -> Octree {
use crate::spatial::octree::BoundingBox;
let all: Vec<Vec3> = queries
.iter()
.chain(keys.iter())
.map(|t| t.position)
.collect();
let mut min = all[0];
let mut max = all[0];
for &p in &all {
min = min.min(p);
max = max.max(p);
}
let span = (max - min).length().max(1.0);
let pad = Vec3::splat(span * 0.25);
let bounds = BoundingBox::new(min - pad, max + pad);
let mut octree = Octree::new(bounds);
for key in keys {
octree.insert(to_node_point(key));
}
octree
}
fn softmax_weights(neg_dist_sq: &[f32]) -> Vec<f32> {
let max = neg_dist_sq
.iter()
.cloned()
.fold(f32::NEG_INFINITY, f32::max);
let exps: Vec<f32> = neg_dist_sq.iter().map(|&v| (v - max).exp()).collect();
let sum: f32 = exps.iter().sum();
exps.iter().map(|&e| e / sum).collect()
}
pub fn sparse_attention(
queries: &[Token],
keys: &[Token],
k: usize,
temperature: f32,
) -> Vec<AttentionResult> {
if queries.is_empty() {
return Vec::new();
}
if keys.is_empty() {
return queries
.iter()
.map(|q| AttentionResult {
query_id: q.id,
attended: Vec::new(),
})
.collect();
}
let octree = build_key_octree(queries, keys);
let key_time: HashMap<u64, u64> = keys.iter().map(|t| (t.id, t.time_step)).collect();
let temp = temperature.max(1e-6);
queries
.iter()
.map(|query| {
let neighbors = octree.query_knn(query.position, k);
let causal: Vec<(u64, f32)> = neighbors
.into_iter()
.filter(|(np, _)| {
key_time
.get(&np.id)
.is_some_and(|&ts| ts <= query.time_step)
})
.map(|(np, dist_sq)| (np.id, dist_sq))
.collect();
if causal.is_empty() {
return AttentionResult {
query_id: query.id,
attended: Vec::new(),
};
}
let neg_dists: Vec<f32> = causal.iter().map(|(_, d)| -d / temp).collect();
let weights = softmax_weights(&neg_dists);
let attended = causal
.iter()
.zip(weights)
.map(|((id, _), w)| (*id, w))
.collect();
AttentionResult {
query_id: query.id,
attended,
}
})
.collect()
}
pub fn attention_stats(results: &[AttentionResult], n_keys: usize) -> AttentionStats {
let n_queries = results.len();
if n_queries == 0 {
return AttentionStats {
n_queries: 0,
mean_attended: 0.0,
min_attended: 0,
max_attended: 0,
total_pairs: 0,
attended_pairs: 0,
sparsity: 1.0,
};
}
let counts: Vec<usize> = results.iter().map(|r| r.attended.len()).collect();
let attended_pairs: usize = counts.iter().sum();
let min_attended = *counts.iter().min().unwrap();
let max_attended = *counts.iter().max().unwrap();
let mean_attended = attended_pairs as f32 / n_queries as f32;
let total_pairs = n_queries * n_keys;
let sparsity = if total_pairs == 0 {
1.0
} else {
1.0 - attended_pairs as f32 / total_pairs as f32
};
AttentionStats {
n_queries,
mean_attended,
min_attended,
max_attended,
total_pairs,
attended_pairs,
sparsity,
}
}
#[cfg(test)]
mod tests {
use super::*;
use glam::Vec3;
fn tok(id: u64, x: f32, y: f32, z: f32, t: u64) -> Token {
Token {
id,
position: Vec3::new(x, y, z),
time_step: t,
}
}
#[test]
fn test_empty_queries_returns_empty() {
let result = sparse_attention(&[], &[tok(0, 0.0, 0.0, 0.0, 0)], 3, 1.0);
assert!(result.is_empty());
}
#[test]
fn test_empty_keys_returns_empty_attended() {
let queries = vec![tok(0, 0.0, 0.0, 0.0, 0)];
let result = sparse_attention(&queries, &[], 3, 1.0);
assert_eq!(result.len(), 1);
assert!(result[0].attended.is_empty());
}
#[test]
fn test_single_pair_weight_is_one() {
let queries = vec![tok(0, 0.0, 0.0, 0.0, 5)];
let keys = vec![tok(1, 0.1, 0.0, 0.0, 3)];
let result = sparse_attention(&queries, &keys, 1, 1.0);
assert_eq!(result[0].attended.len(), 1);
let (id, w) = result[0].attended[0];
assert_eq!(id, 1);
assert!(
(w - 1.0).abs() < 1e-5,
"single key must get weight 1.0, got {w}"
);
}
#[test]
fn test_causal_mask_excludes_future_keys() {
let queries = vec![tok(0, 0.0, 0.0, 0.0, 2)];
let keys = vec![tok(1, 0.1, 0.0, 0.0, 1), tok(2, 0.2, 0.0, 0.0, 3)];
let result = sparse_attention(&queries, &keys, 5, 1.0);
let ids: Vec<u64> = result[0].attended.iter().map(|(id, _)| *id).collect();
assert!(ids.contains(&1), "past key must be attended");
assert!(!ids.contains(&2), "future key must be masked");
}
#[test]
fn test_same_time_step_is_allowed() {
let queries = vec![tok(0, 0.0, 0.0, 0.0, 3)];
let keys = vec![tok(1, 0.1, 0.0, 0.0, 3)];
let result = sparse_attention(&queries, &keys, 1, 1.0);
assert_eq!(
result[0].attended.len(),
1,
"equal time_step should not be masked"
);
}
#[test]
fn test_k_limits_attended_count() {
let queries = vec![tok(0, 0.5, 0.5, 0.5, 10)];
let keys: Vec<Token> = (1u64..=8)
.map(|i| tok(i, i as f32 * 0.1, 0.0, 0.0, i))
.collect();
let result = sparse_attention(&queries, &keys, 3, 1.0);
assert!(
result[0].attended.len() <= 3,
"attended count must not exceed k"
);
}
#[test]
fn test_weights_sum_to_one() {
let queries = vec![tok(0, 0.0, 0.0, 0.0, 10)];
let keys: Vec<Token> = (1u64..=5)
.map(|i| tok(i, i as f32 * 0.2, 0.0, 0.0, i))
.collect();
let result = sparse_attention(&queries, &keys, 5, 1.0);
let sum: f32 = result[0].attended.iter().map(|(_, w)| w).sum();
assert!(
(sum - 1.0).abs() < 1e-5,
"weights must sum to 1.0, got {sum}"
);
}
#[test]
fn test_closer_token_gets_higher_weight() {
let queries = vec![tok(0, 0.0, 0.0, 0.0, 10)];
let keys = vec![
tok(1, 0.1, 0.0, 0.0, 1), tok(2, 5.0, 0.0, 0.0, 2), ];
let result = sparse_attention(&queries, &keys, 5, 1.0);
let w1 = result[0]
.attended
.iter()
.find(|(id, _)| *id == 1)
.map(|(_, w)| *w)
.expect("close key must be attended");
let w2 = result[0]
.attended
.iter()
.find(|(id, _)| *id == 2)
.map(|(_, w)| *w)
.expect("far key must be attended");
assert!(
w1 > w2,
"closer token must have higher weight ({w1:.4} vs {w2:.4})"
);
}
#[test]
fn test_attention_stats_fields() {
let queries: Vec<Token> = (0..4).map(|i| tok(i, i as f32, 0.0, 0.0, 10)).collect();
let keys: Vec<Token> = (10..14)
.map(|i| tok(i, (i - 10) as f32 * 0.5, 0.0, 0.0, 5))
.collect();
let results = sparse_attention(&queries, &keys, 2, 1.0);
let stats = attention_stats(&results, keys.len());
assert_eq!(stats.n_queries, 4);
assert_eq!(stats.total_pairs, 16);
assert!(stats.sparsity >= 0.0 && stats.sparsity <= 1.0);
assert!(stats.attended_pairs <= stats.total_pairs);
}
}