Skip to content

Commit 6fd9529

Browse files
committed
feat: add graph representation, csv and n-tripples readers, one-hop reachability example algo
1 parent 34b4a80 commit 6fd9529

10 files changed

Lines changed: 1641 additions & 0 deletions

File tree

Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,8 @@ version = "0.1.0"
44
edition = "2024"
55

66
[dependencies]
7+
csv = "1.4.0"
8+
oxrdf = "0.3.3"
9+
oxttl = "0.2.3"
10+
thiserror = "1.0"
11+
suitesparse_graphblas_sys = "0.4.3"

src/algo/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
//! Graph algorithms for pathrex.
2+
3+
pub mod one_hop;
4+
5+
pub use one_hop::one_hop_reachability;

src/algo/one_hop.rs

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
//! One-hop reachability algorithm.
2+
3+
use suitesparse_graphblas_sys::{
4+
GrB_Index, GrB_LOR_LAND_SEMIRING_BOOL, GrB_Vector_extractTuples_BOOL, GrB_Vector_nvals,
5+
GrB_Vector_setElement_BOOL, GrB_vxm,
6+
};
7+
8+
use crate::graph::{GraphDecomposition, GraphError, GraphblasVector, grb_ok};
9+
10+
/// Returns all nodes reachable from `source` via one hop.
11+
/// # Example
12+
///
13+
/// ```
14+
/// use pathrex::graph::{InMemoryBuilder, GraphBuilder, Edge};
15+
/// use pathrex::algo::one_hop_reachability;
16+
///
17+
/// let mut builder = InMemoryBuilder::new();
18+
/// builder.push_edge(Edge { source: "A".into(), target: "B".into(), label: "knows".into() }).unwrap();
19+
/// builder.push_edge(Edge { source: "A".into(), target: "C".into(), label: "knows".into() }).unwrap();
20+
/// let graph = builder.build().unwrap();
21+
///
22+
/// let mut neighbours = one_hop_reachability(&graph, "A", "knows").unwrap();
23+
/// neighbours.sort();
24+
/// assert_eq!(neighbours, vec!["B", "C"]);
25+
/// ```
26+
pub fn one_hop_reachability<G>(
27+
graph: &G,
28+
source: &str,
29+
label: &str,
30+
) -> Result<Vec<String>, GraphError>
31+
where
32+
G: GraphDecomposition<Error = GraphError>,
33+
{
34+
let src_idx = match graph.get_node_id(source) {
35+
Some(id) => id as GrB_Index,
36+
None => return Ok(Vec::new()),
37+
};
38+
39+
let matrix = graph.get_matrix(label, false)?;
40+
let n = graph.num_nodes() as GrB_Index;
41+
42+
unsafe {
43+
let src_vec = GraphblasVector::new_bool(n)?;
44+
grb_ok(GrB_Vector_setElement_BOOL(src_vec.inner, true, src_idx))?;
45+
46+
let result_vec = GraphblasVector::new_bool(n)?;
47+
grb_ok(GrB_vxm(
48+
result_vec.inner,
49+
std::ptr::null_mut(),
50+
std::ptr::null_mut(),
51+
GrB_LOR_LAND_SEMIRING_BOOL,
52+
src_vec.inner,
53+
matrix.inner,
54+
std::ptr::null_mut(),
55+
))?;
56+
57+
let mut nvals: GrB_Index = 0;
58+
grb_ok(GrB_Vector_nvals(&mut nvals, result_vec.inner))?;
59+
60+
if nvals == 0 {
61+
return Ok(Vec::new());
62+
}
63+
64+
let mut indices: Vec<GrB_Index> = vec![0; nvals as usize];
65+
let mut values: Vec<bool> = vec![false; nvals as usize];
66+
let mut nvals_out = nvals;
67+
grb_ok(GrB_Vector_extractTuples_BOOL(
68+
indices.as_mut_ptr(),
69+
values.as_mut_ptr(),
70+
&mut nvals_out,
71+
result_vec.inner,
72+
))?;
73+
74+
Ok(indices[..nvals_out as usize]
75+
.iter()
76+
.filter_map(|&idx| graph.get_node_name(idx as usize))
77+
.collect())
78+
}
79+
}
80+
81+
#[cfg(test)]
82+
mod tests {
83+
use super::*;
84+
use crate::graph::{Edge, GraphBuilder, GraphError, InMemoryBuilder, InMemoryGraph};
85+
86+
fn make_graph(edges: &[(&str, &str, &str)]) -> InMemoryGraph {
87+
let mut builder = InMemoryBuilder::new();
88+
for &(src, tgt, lbl) in edges {
89+
builder
90+
.push_edge(Edge {
91+
source: src.to_owned(),
92+
target: tgt.to_owned(),
93+
label: lbl.to_owned(),
94+
})
95+
.unwrap();
96+
}
97+
builder.build().unwrap()
98+
}
99+
100+
#[test]
101+
fn test_single_neighbour() {
102+
let graph = make_graph(&[("A", "B", "knows")]);
103+
assert_eq!(
104+
one_hop_reachability(&graph, "A", "knows").unwrap(),
105+
vec!["B"]
106+
);
107+
}
108+
109+
#[test]
110+
fn test_multiple_neighbours() {
111+
let graph = make_graph(&[
112+
("A", "B", "knows"),
113+
("A", "C", "knows"),
114+
("A", "D", "knows"),
115+
]);
116+
let mut result = one_hop_reachability(&graph, "A", "knows").unwrap();
117+
result.sort();
118+
assert_eq!(result, vec!["B", "C", "D"]);
119+
}
120+
121+
#[test]
122+
fn test_no_outgoing_edges() {
123+
let graph = make_graph(&[("A", "B", "knows")]);
124+
assert!(
125+
one_hop_reachability(&graph, "B", "knows")
126+
.unwrap()
127+
.is_empty()
128+
);
129+
}
130+
131+
#[test]
132+
fn test_unknown_source_returns_empty() {
133+
let graph = make_graph(&[("A", "B", "knows")]);
134+
assert!(
135+
one_hop_reachability(&graph, "Z", "knows")
136+
.unwrap()
137+
.is_empty()
138+
);
139+
}
140+
141+
#[test]
142+
fn test_unknown_label_returns_error() {
143+
let graph = make_graph(&[("A", "B", "knows")]);
144+
assert!(matches!(
145+
one_hop_reachability(&graph, "A", "likes"),
146+
Err(GraphError::LabelNotFound(_))
147+
));
148+
}
149+
150+
#[test]
151+
fn test_self_loop() {
152+
let graph = make_graph(&[("A", "A", "self")]);
153+
assert_eq!(
154+
one_hop_reachability(&graph, "A", "self").unwrap(),
155+
vec!["A"]
156+
);
157+
}
158+
159+
#[test]
160+
fn test_label_isolation() {
161+
let graph = make_graph(&[("A", "B", "knows"), ("A", "C", "likes")]);
162+
assert_eq!(
163+
one_hop_reachability(&graph, "A", "knows").unwrap(),
164+
vec!["B"]
165+
);
166+
}
167+
}

0 commit comments

Comments
 (0)