use urna_format::error::UrnaError;
use urna_format::sections::graph::parse_csr_parts;
use crate::error::RuntimeError;
pub struct CsrIndex {
n_nodes: usize,
offsets: Vec<u32>,
neighbors: Vec<u32>,
edge_types: Vec<u8>,
}
impl CsrIndex {
pub fn from_bytes(bytes: &[u8], n_embeddings: usize) -> Result<Self, RuntimeError> {
let parts = parse_csr_parts(bytes).map_err(RuntimeError::Format)?;
if parts.n_nodes != n_embeddings {
return Err(RuntimeError::Format(UrnaError::MalformedSectionPayload {
section_id: urna_format::layout::SECTION_GRAPH_ADJACENCY,
reason: format!(
"graph node count {} != n_embeddings {}",
parts.n_nodes, n_embeddings
),
}));
}
let offsets: Vec<u32> = parts.offsets.iter().map(|&o| o as u32).collect();
Ok(Self {
n_nodes: parts.n_nodes,
offsets,
neighbors: parts.neighbors,
edge_types: parts.edge_types,
})
}
pub fn n_nodes(&self) -> usize {
self.n_nodes
}
#[inline]
pub fn neighbors(&self, node: usize) -> &[u32] {
if node + 1 >= self.offsets.len() {
return &[];
}
let start = self.offsets[node] as usize;
let end = self.offsets[node + 1] as usize;
&self.neighbors[start..end]
}
#[inline]
pub fn edge_type(&self, node: usize, i: usize) -> Option<u8> {
if node + 1 >= self.offsets.len() {
return None;
}
let start = self.offsets[node] as usize;
let end = self.offsets[node + 1] as usize;
let idx = start + i;
if idx < end {
Some(self.edge_types[idx])
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use urna_format::sections::graph::{
EDGE_TYPE_NEXT_CHUNK, EDGE_TYPE_SEMANTIC, Edge, encode_graph_adjacency,
};
fn edge(src: u32, dst: u32, t: u8) -> Edge {
Edge {
src,
dst,
edge_type: t,
}
}
#[test]
fn neighbors_are_contiguous_and_correct() {
let edges = vec![
edge(0, 2, EDGE_TYPE_SEMANTIC),
edge(0, 1, EDGE_TYPE_NEXT_CHUNK),
edge(1, 2, EDGE_TYPE_NEXT_CHUNK),
edge(3, 0, EDGE_TYPE_NEXT_CHUNK),
];
let payload = encode_graph_adjacency(4, &edges).unwrap();
let idx = CsrIndex::from_bytes(&payload, 4).unwrap();
assert_eq!(idx.n_nodes(), 4);
assert_eq!(idx.neighbors(0), &[1, 2]);
assert_eq!(idx.neighbors(1), &[2]);
assert_eq!(idx.neighbors(2), &[] as &[u32]);
assert_eq!(idx.neighbors(3), &[0]);
assert_eq!(idx.edge_type(0, 0), Some(EDGE_TYPE_NEXT_CHUNK));
assert_eq!(idx.edge_type(0, 1), Some(EDGE_TYPE_SEMANTIC));
assert_eq!(idx.edge_type(0, 2), None);
assert_eq!(idx.neighbors(99), &[] as &[u32]);
}
#[test]
fn rejects_node_count_mismatch() {
let edges = vec![edge(0, 1, EDGE_TYPE_NEXT_CHUNK)];
let payload = encode_graph_adjacency(2, &edges).unwrap();
assert!(CsrIndex::from_bytes(&payload, 3).is_err());
}
#[test]
fn truncated_payload_errors_never_panics() {
let edges = vec![
edge(0, 1, EDGE_TYPE_NEXT_CHUNK),
edge(1, 2, EDGE_TYPE_SEMANTIC),
edge(2, 0, EDGE_TYPE_SEMANTIC),
];
let payload = encode_graph_adjacency(3, &edges).unwrap();
for cut in 0..payload.len() {
let _ = CsrIndex::from_bytes(&payload[..cut], 3);
}
}
#[test]
fn empty_graph_parses() {
let payload = encode_graph_adjacency(0, &[]).unwrap();
let idx = CsrIndex::from_bytes(&payload, 0).unwrap();
assert_eq!(idx.n_nodes(), 0);
assert_eq!(idx.neighbors(0), &[] as &[u32]);
}
}