urna_runtime/graph/
csr.rs1use urna_format::error::UrnaError;
9use urna_format::sections::graph::parse_csr_parts;
10
11use crate::error::RuntimeError;
12
13pub struct CsrIndex {
17 n_nodes: usize,
18 offsets: Vec<u32>,
20 neighbors: Vec<u32>,
22 edge_types: Vec<u8>,
24}
25
26impl CsrIndex {
27 pub fn from_bytes(bytes: &[u8], n_embeddings: usize) -> Result<Self, RuntimeError> {
31 let parts = parse_csr_parts(bytes).map_err(RuntimeError::Format)?;
32 if parts.n_nodes != n_embeddings {
33 return Err(RuntimeError::Format(UrnaError::MalformedSectionPayload {
34 section_id: urna_format::layout::SECTION_GRAPH_ADJACENCY,
35 reason: format!(
36 "graph node count {} != n_embeddings {}",
37 parts.n_nodes, n_embeddings
38 ),
39 }));
40 }
41 let offsets: Vec<u32> = parts.offsets.iter().map(|&o| o as u32).collect();
45 Ok(Self {
46 n_nodes: parts.n_nodes,
47 offsets,
48 neighbors: parts.neighbors,
49 edge_types: parts.edge_types,
50 })
51 }
52
53 pub fn n_nodes(&self) -> usize {
54 self.n_nodes
55 }
56
57 #[inline]
60 pub fn neighbors(&self, node: usize) -> &[u32] {
61 if node + 1 >= self.offsets.len() {
62 return &[];
63 }
64 let start = self.offsets[node] as usize;
65 let end = self.offsets[node + 1] as usize;
66 &self.neighbors[start..end]
67 }
68
69 #[inline]
71 pub fn edge_type(&self, node: usize, i: usize) -> Option<u8> {
72 if node + 1 >= self.offsets.len() {
73 return None;
74 }
75 let start = self.offsets[node] as usize;
76 let end = self.offsets[node + 1] as usize;
77 let idx = start + i;
78 if idx < end {
79 Some(self.edge_types[idx])
80 } else {
81 None
82 }
83 }
84}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89 use urna_format::sections::graph::{
90 EDGE_TYPE_NEXT_CHUNK, EDGE_TYPE_SEMANTIC, Edge, encode_graph_adjacency,
91 };
92
93 fn edge(src: u32, dst: u32, t: u8) -> Edge {
94 Edge {
95 src,
96 dst,
97 edge_type: t,
98 }
99 }
100
101 #[test]
102 fn neighbors_are_contiguous_and_correct() {
103 let edges = vec![
105 edge(0, 2, EDGE_TYPE_SEMANTIC),
106 edge(0, 1, EDGE_TYPE_NEXT_CHUNK),
107 edge(1, 2, EDGE_TYPE_NEXT_CHUNK),
108 edge(3, 0, EDGE_TYPE_NEXT_CHUNK),
109 ];
110 let payload = encode_graph_adjacency(4, &edges).unwrap();
111 let idx = CsrIndex::from_bytes(&payload, 4).unwrap();
112 assert_eq!(idx.n_nodes(), 4);
113 assert_eq!(idx.neighbors(0), &[1, 2]);
116 assert_eq!(idx.neighbors(1), &[2]);
117 assert_eq!(idx.neighbors(2), &[] as &[u32]);
118 assert_eq!(idx.neighbors(3), &[0]);
119 assert_eq!(idx.edge_type(0, 0), Some(EDGE_TYPE_NEXT_CHUNK));
120 assert_eq!(idx.edge_type(0, 1), Some(EDGE_TYPE_SEMANTIC));
121 assert_eq!(idx.edge_type(0, 2), None);
122 assert_eq!(idx.neighbors(99), &[] as &[u32]);
124 }
125
126 #[test]
127 fn rejects_node_count_mismatch() {
128 let edges = vec![edge(0, 1, EDGE_TYPE_NEXT_CHUNK)];
129 let payload = encode_graph_adjacency(2, &edges).unwrap();
130 assert!(CsrIndex::from_bytes(&payload, 3).is_err());
131 }
132
133 #[test]
134 fn truncated_payload_errors_never_panics() {
135 let edges = vec![
136 edge(0, 1, EDGE_TYPE_NEXT_CHUNK),
137 edge(1, 2, EDGE_TYPE_SEMANTIC),
138 edge(2, 0, EDGE_TYPE_SEMANTIC),
139 ];
140 let payload = encode_graph_adjacency(3, &edges).unwrap();
141 for cut in 0..payload.len() {
142 let _ = CsrIndex::from_bytes(&payload[..cut], 3);
144 }
145 }
146
147 #[test]
148 fn empty_graph_parses() {
149 let payload = encode_graph_adjacency(0, &[]).unwrap();
150 let idx = CsrIndex::from_bytes(&payload, 0).unwrap();
151 assert_eq!(idx.n_nodes(), 0);
152 assert_eq!(idx.neighbors(0), &[] as &[u32]);
153 }
154}