use crate::{Dir, Graph};
#[derive(Debug, Default, Clone)]
pub struct Snapshot {
ids: Vec<u64>,
out_at: Vec<u64>,
out_to: Vec<u32>,
in_at: Vec<u64>,
in_to: Vec<u32>,
}
impl Snapshot {
#[must_use]
pub fn of(g: &Graph) -> Snapshot {
let labels = g.labels().to_vec();
Snapshot::labelled(g, &labels)
}
#[must_use]
pub fn labelled(g: &Graph, labels: &[u32]) -> Snapshot {
project(g, labels, None).0
}
#[must_use]
pub fn weighted(g: &Graph, labels: &[u32], field: &[u8], missing: u32) -> (Snapshot, Vec<u32>) {
project(g, labels, Some((field, missing)))
}
#[must_use]
pub fn nodes(&self) -> u32 {
self.ids.len() as u32
}
#[must_use]
pub fn edges(&self) -> u64 {
self.out_to.len() as u64
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.ids.is_empty()
}
#[must_use]
pub fn id(&self, node: u32) -> u64 {
self.ids[node as usize]
}
#[must_use]
pub fn dense(&self, id: u64) -> Option<u32> {
self.ids.binary_search(&id).ok().map(|at| at as u32)
}
#[must_use]
pub fn out(&self, node: u32) -> &[u32] {
run(&self.out_at, &self.out_to, node)
}
#[must_use]
pub fn out_at(&self, node: u32) -> usize {
self.out_at[node as usize] as usize
}
#[must_use]
pub fn into_(&self, node: u32) -> &[u32] {
run(&self.in_at, &self.in_to, node)
}
#[must_use]
pub fn neighbours(&self, node: u32, dir: Dir) -> &[u32] {
match dir {
Dir::Out => self.out(node),
Dir::In => self.into_(node),
}
}
#[must_use]
pub fn out_degree(&self, node: u32) -> u32 {
self.out(node).len() as u32
}
#[must_use]
pub fn in_degree(&self, node: u32) -> u32 {
self.into_(node).len() as u32
}
pub fn prefetch(&self, node: u32) {
let at = self.out_at[node as usize] as usize;
if at < self.out_to.len() {
yo_common::prefetch(&self.out_to[at]);
}
}
#[must_use]
pub fn memory_bytes(&self) -> usize {
self.ids.capacity() * size_of::<u64>()
+ (self.out_at.capacity() + self.in_at.capacity()) * size_of::<u64>()
+ (self.out_to.capacity() + self.in_to.capacity()) * size_of::<u32>()
}
}
fn project(g: &Graph, labels: &[u32], weight: Option<(&[u8], u32)>) -> (Snapshot, Vec<u32>) {
let mut ids: Vec<u64> = g.node_props().iter().map(|(id, _)| id).collect();
ids.sort_unstable();
let n = ids.len();
let map = Map::of(&ids);
let mut out_at = vec![0u64; n + 1];
for label in labels {
g.adjacency().for_each_run(*label, Dir::Out, |node, ns, _| {
let Some(at) = map.dense(node) else { return };
out_at[at as usize + 1] += ns.len() as u64;
});
}
for i in 0..n {
out_at[i + 1] += out_at[i];
}
let mut out_to = vec![0u32; out_at[n] as usize];
let mut weights = match weight {
Some(_) => vec![0u32; out_at[n] as usize],
None => Vec::new(),
};
let mut cursor = out_at.clone();
for label in labels {
g.adjacency()
.for_each_run(*label, Dir::Out, |node, ns, es| {
let Some(at) = map.dense(node) else { return };
for (i, to) in ns.iter().enumerate() {
let Some(to) = map.dense(*to) else { continue };
let put = cursor[at as usize] as usize;
out_to[put] = to;
if let Some((field, missing)) = weight {
weights[put] = weigh(g, es[i], field, missing);
}
cursor[at as usize] += 1;
}
});
}
let (in_at, in_to) = transpose(n, &out_at, &out_to);
let s = Snapshot {
ids,
out_at,
out_to,
in_at,
in_to,
};
(s, weights)
}
fn weigh(g: &Graph, slot: u32, field: &[u8], missing: u32) -> u32 {
let Some(value) = g.edge(slot).and_then(|doc| doc.get(field)) else {
return missing;
};
if let Some(n) = value.as_int() {
return u32::try_from(n).unwrap_or(if n < 0 { missing } else { u32::MAX });
}
if let Some(n) = value.as_float() {
if n.is_nan() || n < 0.0 {
return missing;
}
return if n >= f64::from(u32::MAX) {
u32::MAX
} else {
n.round() as u32
};
}
missing
}
#[inline]
fn run<'a>(at: &[u64], to: &'a [u32], node: u32) -> &'a [u32] {
let i = node as usize;
let (from, upto) = (at[i] as usize, at[i + 1] as usize);
&to[from..upto]
}
fn transpose(n: usize, out_at: &[u64], out_to: &[u32]) -> (Vec<u64>, Vec<u32>) {
let mut in_at = vec![0u64; n + 1];
for to in out_to {
in_at[*to as usize + 1] += 1;
}
for i in 0..n {
in_at[i + 1] += in_at[i];
}
let mut in_to = vec![0u32; out_to.len()];
let mut cursor = in_at.clone();
for from in 0..n {
let (a, b) = (out_at[from] as usize, out_at[from + 1] as usize);
for to in &out_to[a..b] {
in_to[cursor[*to as usize] as usize] = from as u32;
cursor[*to as usize] += 1;
}
}
(in_at, in_to)
}
#[derive(Debug)]
enum Map<'a> {
Table(Vec<u32>),
Search(&'a [u64]),
}
const NONE: u32 = u32::MAX;
impl<'a> Map<'a> {
fn of(ids: &'a [u64]) -> Map<'a> {
let top = ids.last().copied().unwrap_or(0);
if !ids.is_empty() && top < 2 * ids.len() as u64 {
let mut table = vec![NONE; top as usize + 1];
for (at, id) in ids.iter().enumerate() {
table[*id as usize] = at as u32;
}
return Map::Table(table);
}
Map::Search(ids)
}
#[inline]
fn dense(&self, id: u64) -> Option<u32> {
match self {
Map::Table(table) => match table.get(id as usize) {
Some(&NONE) | None => None,
Some(at) => Some(*at),
},
Map::Search(ids) => ids.binary_search(&id).ok().map(|at| at as u32),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::graph::NO_PROPS;
fn chain(n: u64) -> Graph {
let mut g = Graph::new();
for i in 0..n - 1 {
g.link(i, i + 1, 1, NO_PROPS).unwrap();
}
g
}
#[test]
fn the_dense_ids_are_the_graphs_ids_in_order() {
let mut g = Graph::new();
g.link(900, 100, 1, NO_PROPS).unwrap();
g.link(100, 500, 1, NO_PROPS).unwrap();
let s = Snapshot::of(&g);
assert_eq!(s.nodes(), 3);
assert_eq!(s.edges(), 2);
assert_eq!((s.id(0), s.id(1), s.id(2)), (100, 500, 900));
assert_eq!(s.dense(500), Some(1));
assert_eq!(s.dense(501), None);
assert_eq!(s.out(0), [1]);
assert_eq!(s.out(2), [0]);
assert_eq!(s.into_(0), [2]);
assert!(s.out(1).is_empty());
}
#[test]
fn a_node_with_no_edges_is_still_a_node() {
let mut g = Graph::new();
g.link(0, 2, 1, NO_PROPS).unwrap();
g.add_node(1).unwrap();
let s = Snapshot::of(&g);
assert_eq!(s.nodes(), 3);
assert_eq!(s.id(1), 1);
assert!(s.out(1).is_empty());
assert!(s.into_(1).is_empty());
assert_eq!(s.out(0), [2]);
}
#[test]
fn only_the_labels_asked_for_come_along() {
let mut g = Graph::new();
g.link(0, 1, 7, NO_PROPS).unwrap();
g.link(0, 2, 9, NO_PROPS).unwrap();
let all = Snapshot::of(&g);
assert_eq!(all.out(0), [1, 2]);
let one = Snapshot::labelled(&g, &[9]);
assert_eq!(one.nodes(), 3, "every node, whatever the labels");
assert_eq!(one.out(0), [2]);
assert_eq!(one.edges(), 1);
let none = Snapshot::labelled(&g, &[]);
assert_eq!(none.nodes(), 3);
assert_eq!(none.edges(), 0);
}
#[test]
fn the_reverse_index_is_the_forward_one_transposed() {
let mut g = Graph::new();
g.link(0, 1, 1, NO_PROPS).unwrap();
g.link(0, 1, 1, NO_PROPS).unwrap();
g.link(1, 1, 1, NO_PROPS).unwrap();
let s = Snapshot::of(&g);
assert_eq!(s.out(0), [1, 1]);
assert_eq!(s.into_(1), [0, 0, 1]);
assert_eq!(s.out(1), [1]);
assert_eq!(s.edges(), 3);
assert_eq!(s.in_degree(1), 3);
assert_eq!(s.out_degree(0), 2);
let mut forward = 0;
let mut back = 0;
for i in 0..s.nodes() {
forward += s.out(i).len();
back += s.into_(i).len();
}
assert_eq!(forward, back);
}
#[test]
fn an_out_only_graph_still_has_predecessors() {
let mut g = Graph::out_only();
g.link(0, 1, 1, NO_PROPS).unwrap();
g.link(2, 1, 1, NO_PROPS).unwrap();
assert!(g.neighbours(1, 1, Dir::In).is_empty(), "not in the plane");
let s = Snapshot::of(&g);
assert_eq!(s.into_(1), [0, 2]);
}
#[test]
fn a_dense_numbering_and_a_scattered_one_agree() {
let dense: Vec<u64> = (0..64).collect();
let scattered: Vec<u64> = (0..64).map(|i| i * 1000 + 7).collect();
let table = Map::of(&dense);
let search = Map::of(&scattered);
assert!(matches!(table, Map::Table(_)), "a counter gets the table");
assert!(matches!(search, Map::Search(_)), "hashes get the search");
for i in 0..64u64 {
assert_eq!(table.dense(i), Some(i as u32));
assert_eq!(search.dense(i * 1000 + 7), Some(i as u32));
}
assert_eq!(table.dense(64), None);
assert_eq!(table.dense(u64::MAX), None);
assert_eq!(search.dense(8), None);
let holed = [0u64, 1, 3];
let map = Map::of(&holed);
assert!(matches!(map, Map::Table(_)));
assert_eq!(map.dense(2), None);
assert_eq!(map.dense(3), Some(2));
}
#[test]
fn an_empty_graph_snapshots_to_nothing() {
let s = Snapshot::of(&Graph::new());
assert!(s.is_empty());
assert_eq!(s.nodes(), 0);
assert_eq!(s.edges(), 0);
assert_eq!(s.dense(0), None);
}
#[test]
fn a_long_chain_reads_back_end_to_end() {
let s = Snapshot::of(&chain(10_000));
assert_eq!(s.nodes(), 10_000);
assert_eq!(s.edges(), 9_999);
for i in 0..s.nodes() - 1 {
assert_eq!(s.out(i), [i + 1], "at {i}");
}
assert!(s.out(9_999).is_empty());
s.prefetch(0);
assert!(s.memory_bytes() > 0);
}
}