use super::csr_edges::CsrEdges;
use super::vertex::VertexId;
use formualizer_common::Coord as AbsCoord;
use rustc_hash::{FxHashMap, FxHashSet};
#[cfg(test)]
mod tests {
use super::*;
use formualizer_common::Coord as AbsCoord;
#[test]
fn test_delta_slab_add_edge() {
let csr = CsrEdges::from_adjacency(
vec![(0u32, vec![1u32])],
&[
AbsCoord::new(0, 0),
AbsCoord::new(0, 1),
AbsCoord::new(0, 2),
],
);
let mut delta = DeltaEdgeSlab::new();
delta.add_edge(VertexId(0), VertexId(2));
let merged = delta.merged_view(&csr, VertexId(0));
assert_eq!(merged, vec![VertexId(1), VertexId(2)]);
}
#[test]
fn test_delta_slab_remove_edge() {
let csr = CsrEdges::from_adjacency(
vec![(0u32, vec![1u32, 2u32, 3u32])],
&[
AbsCoord::new(0, 0),
AbsCoord::new(0, 1),
AbsCoord::new(0, 2),
AbsCoord::new(0, 3),
],
);
let mut delta = DeltaEdgeSlab::new();
delta.remove_edge(VertexId(0), VertexId(2));
let merged = delta.merged_view(&csr, VertexId(0));
assert_eq!(merged, vec![VertexId(1), VertexId(3)]);
}
#[test]
fn test_delta_slab_rebuild_threshold() {
let mut edges = CsrMutableEdges::new();
for i in 0..1000 {
edges.add_edge(VertexId(i), VertexId(i + 1));
}
assert!(edges.delta_size() < 100); }
#[test]
fn test_delta_slab_multiple_operations() {
let csr = CsrEdges::from_adjacency(
vec![(0u32, vec![1u32, 2u32]), (1u32, vec![3u32])],
&[
AbsCoord::new(0, 0),
AbsCoord::new(0, 1),
AbsCoord::new(0, 2),
AbsCoord::new(1, 0),
],
);
let mut delta = DeltaEdgeSlab::new();
delta.add_edge(VertexId(0), VertexId(3));
delta.remove_edge(VertexId(0), VertexId(1));
delta.add_edge(VertexId(0), VertexId(4));
let merged = delta.merged_view(&csr, VertexId(0));
assert_eq!(merged, vec![VertexId(2), VertexId(3), VertexId(4)]);
}
#[test]
fn test_mutable_edges_exact_edge_count_includes_delta() {
let mut edges = CsrMutableEdges::with_coords(vec![
AbsCoord::new(0, 0),
AbsCoord::new(0, 1),
AbsCoord::new(0, 2),
]);
edges.add_edge(VertexId(0), VertexId(1));
edges.add_edge(VertexId(0), VertexId(2));
assert_eq!(edges.num_edges_exact(), 2);
edges.remove_edge(VertexId(0), VertexId(1));
assert_eq!(edges.num_edges_exact(), 1);
}
#[test]
fn test_delta_slab_empty_base() {
let csr = CsrEdges::empty();
let mut delta = DeltaEdgeSlab::new();
delta.add_edge(VertexId(0), VertexId(1));
delta.add_edge(VertexId(0), VertexId(2));
let merged = delta.merged_view(&csr, VertexId(0));
assert_eq!(merged, vec![VertexId(1), VertexId(2)]);
}
#[test]
fn test_delta_slab_remove_nonexistent() {
let csr = CsrEdges::from_adjacency(
vec![(0u32, vec![1u32])],
&[AbsCoord::new(0, 0), AbsCoord::new(0, 1)],
);
let mut delta = DeltaEdgeSlab::new();
delta.remove_edge(VertexId(0), VertexId(2));
let merged = delta.merged_view(&csr, VertexId(0));
assert_eq!(merged, vec![VertexId(1)]); }
#[test]
fn test_delta_slab_apply_to_csr() {
let csr = CsrEdges::from_adjacency(
vec![(0u32, vec![1u32]), (1u32, vec![2u32]), (2u32, vec![])],
&[
AbsCoord::new(0, 0),
AbsCoord::new(0, 1),
AbsCoord::new(1, 0),
],
);
let mut delta = DeltaEdgeSlab::new();
delta.add_edge(VertexId(0), VertexId(2));
delta.remove_edge(VertexId(1), VertexId(2));
delta.add_edge(VertexId(2), VertexId(0));
let coords = vec![
AbsCoord::new(0, 0),
AbsCoord::new(0, 1),
AbsCoord::new(1, 0),
];
let vertex_ids = vec![0u32, 1u32, 2u32];
let new_csr = delta.apply_to_csr(&csr, &coords, &vertex_ids);
assert_eq!(new_csr.out_edges(VertexId(0)), &[VertexId(1), VertexId(2)]);
assert_eq!(new_csr.out_edges(VertexId(1)), &[]);
assert_eq!(new_csr.out_edges(VertexId(2)), &[VertexId(0)]);
}
#[test]
fn test_mutable_edges_auto_rebuild() {
let mut edges = CsrMutableEdges::with_coords(vec![
AbsCoord::new(0, 0),
AbsCoord::new(0, 1),
AbsCoord::new(1, 0),
]);
edges.add_edge(VertexId(0), VertexId(1));
edges.add_edge(VertexId(1), VertexId(2));
for _ in 0..500 {
edges.add_edge(VertexId(2), VertexId(0));
edges.remove_edge(VertexId(2), VertexId(0));
}
assert!(edges.delta_size() < 50);
assert_eq!(edges.out_edges(VertexId(0)), vec![VertexId(1)]);
assert_eq!(edges.out_edges(VertexId(1)), vec![VertexId(2)]);
}
#[test]
fn test_mutable_edges_with_offset_vertex_ids() {
use crate::engine::vertex_store::FIRST_NORMAL_VERTEX;
let mut edges = CsrMutableEdges::new();
let base_id = FIRST_NORMAL_VERTEX;
edges.add_vertex(AbsCoord::new(0, 0), base_id);
edges.add_vertex(AbsCoord::new(0, 1), base_id + 1);
edges.add_vertex(AbsCoord::new(1, 0), base_id + 2);
edges.add_edge(VertexId(base_id), VertexId(base_id + 1));
edges.add_edge(VertexId(base_id + 1), VertexId(base_id + 2));
edges.add_edge(VertexId(base_id + 2), VertexId(base_id));
assert_eq!(
edges.out_edges(VertexId(base_id)),
vec![VertexId(base_id + 1)]
);
assert_eq!(
edges.out_edges(VertexId(base_id + 1)),
vec![VertexId(base_id + 2)]
);
assert_eq!(
edges.out_edges(VertexId(base_id + 2)),
vec![VertexId(base_id)]
);
edges.rebuild();
assert_eq!(
edges.out_edges(VertexId(base_id)),
vec![VertexId(base_id + 1)]
);
assert_eq!(
edges.out_edges(VertexId(base_id + 1)),
vec![VertexId(base_id + 2)]
);
assert_eq!(
edges.out_edges(VertexId(base_id + 2)),
vec![VertexId(base_id)]
);
}
#[test]
fn test_csr_coord_update() {
let mut edges = CsrMutableEdges::new();
edges.add_vertex(AbsCoord::new(1, 1), 1024);
edges.add_vertex(AbsCoord::new(2, 2), 1025);
edges.add_edge(VertexId(1024), VertexId(1025));
edges.update_coord(VertexId(1024), AbsCoord::new(5, 5));
edges.rebuild();
let out = edges.out_edges(VertexId(1024));
assert_eq!(out, vec![VertexId(1025)]);
}
#[test]
fn update_coord_uses_vertex_position_index() {
let mut edges = CsrMutableEdges::new();
let items: Vec<_> = (0..20_000u32)
.map(|id| (AbsCoord::new(id, id % 17), id))
.collect();
edges.add_vertices_batch(&items);
let started = std::time::Instant::now();
for id in 15_000..20_000u32 {
edges.update_coord(VertexId(id), AbsCoord::new(id + 1, (id + 2) % 100));
}
let elapsed = started.elapsed();
if !cfg!(debug_assertions) {
assert!(
elapsed < std::time::Duration::from_millis(50),
"update_coord took {elapsed:?}"
);
}
for id in 15_000..20_000u32 {
let pos = edges.vertex_pos[&id];
assert_eq!(edges.vertex_ids[pos], id);
assert_eq!(edges.coords[pos], AbsCoord::new(id + 1, (id + 2) % 100));
}
}
#[test]
fn test_last_op_wins_add_then_remove() {
let csr = CsrEdges::from_adjacency(vec![(0u32, vec![])], &[AbsCoord::new(0, 0)]);
let mut delta = DeltaEdgeSlab::new();
delta.add_edge(VertexId(0), VertexId(1));
delta.remove_edge(VertexId(0), VertexId(1));
let merged = delta.merged_view(&csr, VertexId(0));
assert_eq!(merged, vec![]);
}
#[test]
fn test_last_op_wins_remove_then_add() {
let csr = CsrEdges::from_adjacency(vec![(0u32, vec![])], &[AbsCoord::new(0, 0)]);
let mut delta = DeltaEdgeSlab::new();
delta.remove_edge(VertexId(0), VertexId(1));
delta.add_edge(VertexId(0), VertexId(1));
let merged = delta.merged_view(&csr, VertexId(0));
assert_eq!(merged, vec![VertexId(1)]);
}
#[test]
fn test_dedup_additions_and_sorted() {
let csr = CsrEdges::from_adjacency(
vec![(0u32, vec![2u32])],
&[
AbsCoord::new(0, 0),
AbsCoord::new(0, 1),
AbsCoord::new(0, 2),
],
);
let mut delta = DeltaEdgeSlab::new();
delta.add_edge(VertexId(0), VertexId(1));
delta.add_edge(VertexId(0), VertexId(3));
delta.add_edge(VertexId(0), VertexId(1)); let merged = delta.merged_view(&csr, VertexId(0));
assert_eq!(merged, vec![VertexId(1), VertexId(2), VertexId(3)]);
}
#[test]
fn test_merged_in_view_add_and_remove() {
let csr = CsrEdges::from_adjacency(
vec![(0u32, vec![2u32]), (1u32, vec![2u32])],
&[
AbsCoord::new(0, 0),
AbsCoord::new(0, 1),
AbsCoord::new(0, 2),
AbsCoord::new(0, 3),
],
);
let mut delta = DeltaEdgeSlab::new();
delta.add_edge(VertexId(3), VertexId(2));
delta.remove_edge(VertexId(0), VertexId(2));
let merged = delta.merged_in_view(&csr, VertexId(2));
assert_eq!(merged, vec![VertexId(1), VertexId(3)]);
}
#[test]
fn test_merged_in_view_last_op_wins() {
let csr = CsrEdges::from_adjacency(
vec![(0u32, vec![1u32])],
&[AbsCoord::new(0, 0), AbsCoord::new(0, 1)],
);
let mut delta = DeltaEdgeSlab::new();
delta.remove_edge(VertexId(0), VertexId(1));
delta.add_edge(VertexId(0), VertexId(1));
assert_eq!(delta.merged_in_view(&csr, VertexId(1)), vec![VertexId(0)]);
delta.add_edge(VertexId(2), VertexId(1));
delta.remove_edge(VertexId(2), VertexId(1));
assert_eq!(delta.merged_in_view(&csr, VertexId(1)), vec![VertexId(0)]);
}
#[test]
fn test_end_batch_below_threshold_defers_rebuild() {
let mut edges = CsrMutableEdges::with_coords(vec![
AbsCoord::new(0, 0),
AbsCoord::new(0, 1),
AbsCoord::new(0, 2),
]);
let before = edges.rebuild_count();
edges.begin_batch();
edges.add_edge(VertexId(0), VertexId(1));
edges.add_edge(VertexId(0), VertexId(2));
edges.end_batch();
assert_eq!(edges.rebuild_count(), before);
assert_eq!(edges.out_edges(VertexId(0)), vec![VertexId(1), VertexId(2)]);
assert_eq!(edges.in_edges_merged(VertexId(1)), vec![VertexId(0)]);
assert_eq!(edges.in_edges_merged(VertexId(2)), vec![VertexId(0)]);
}
#[test]
fn test_end_batch_rebuilds_at_threshold() {
let coords: Vec<AbsCoord> = (0..1200u32).map(|i| AbsCoord::new(i, 0)).collect();
let mut edges = CsrMutableEdges::with_coords(coords);
let before = edges.rebuild_count();
edges.begin_batch();
for i in 0..1100u32 {
edges.add_edge(VertexId(i), VertexId(i + 1));
}
edges.end_batch();
assert_eq!(edges.rebuild_count(), before + 1);
assert_eq!(edges.delta_size(), 0);
assert_eq!(edges.out_edges(VertexId(0)), vec![VertexId(1)]);
assert_eq!(edges.in_edges(VertexId(1)), &[VertexId(0)]);
}
#[test]
fn test_add_vertex_defers_rebuild() {
let mut edges = CsrMutableEdges::new();
edges.add_vertex(AbsCoord::new(0, 0), 1024);
edges.add_vertex(AbsCoord::new(0, 1), 1025);
assert_eq!(edges.rebuild_count(), 0);
edges.add_edge(VertexId(1024), VertexId(1025));
assert_eq!(edges.out_edges(VertexId(1024)), vec![VertexId(1025)]);
assert_eq!(edges.in_edges_merged(VertexId(1025)), vec![VertexId(1024)]);
edges.rebuild();
assert_eq!(edges.out_edges(VertexId(1024)), vec![VertexId(1025)]);
assert_eq!(edges.in_edges(VertexId(1025)), &[VertexId(1024)]);
}
#[test]
fn test_end_batch_rebuilds_on_coord_change_only() {
let mut edges =
CsrMutableEdges::with_coords(vec![AbsCoord::new(0, 0), AbsCoord::new(0, 1)]);
edges.begin_batch();
edges.update_coord(VertexId(0), AbsCoord::new(0, 2));
edges.end_batch();
assert_eq!(edges.out_edges(VertexId(0)), Vec::<VertexId>::new());
}
}
#[derive(Debug)]
pub struct DeltaEdgeSlab {
additions: FxHashMap<VertexId, FxHashSet<VertexId>>,
removals: FxHashMap<VertexId, FxHashSet<VertexId>>,
additions_in: FxHashMap<VertexId, FxHashSet<VertexId>>,
removals_in: FxHashMap<VertexId, FxHashSet<VertexId>>,
op_count: usize,
coord_changed: bool,
}
impl DeltaEdgeSlab {
pub fn new() -> Self {
Self {
additions: FxHashMap::default(),
removals: FxHashMap::default(),
additions_in: FxHashMap::default(),
removals_in: FxHashMap::default(),
op_count: 0,
coord_changed: false,
}
}
pub fn add_edge(&mut self, from: VertexId, to: VertexId) {
if let Some(rem) = self.removals.get_mut(&from) {
rem.remove(&to);
}
if let Some(rem) = self.removals_in.get_mut(&to) {
rem.remove(&from);
}
self.additions.entry(from).or_default().insert(to);
self.additions_in.entry(to).or_default().insert(from);
self.op_count += 1;
}
pub fn remove_edge(&mut self, from: VertexId, to: VertexId) {
if let Some(adds) = self.additions.get_mut(&from) {
adds.remove(&to);
}
if let Some(adds) = self.additions_in.get_mut(&to) {
adds.remove(&from);
}
self.removals.entry(from).or_default().insert(to);
self.removals_in.entry(to).or_default().insert(from);
self.op_count += 1;
}
pub fn merged_view(&self, csr: &CsrEdges, v: VertexId) -> Vec<VertexId> {
let mut result: Vec<_> = csr.out_edges(v).to_vec();
if let Some(removes) = self.removals.get(&v) {
result.retain(|e| !removes.contains(e));
}
if let Some(adds) = self.additions.get(&v) {
result.extend(adds.iter().copied());
}
let mut seen: FxHashSet<VertexId> = FxHashSet::default();
result.retain(|e| seen.insert(*e));
result.sort_by_key(|e| e.0);
result
}
pub fn merged_in_view(&self, csr: &CsrEdges, v: VertexId) -> Vec<VertexId> {
let mut result: Vec<_> = csr.in_edges(v).to_vec();
if let Some(removes) = self.removals_in.get(&v) {
result.retain(|e| !removes.contains(e));
}
if let Some(adds) = self.additions_in.get(&v) {
result.extend(adds.iter().copied());
}
let mut seen: FxHashSet<VertexId> = FxHashSet::default();
result.retain(|e| seen.insert(*e));
result.sort_by_key(|e| e.0);
result
}
pub fn needs_rebuild(&self) -> bool {
self.op_count >= 1000 || self.coord_changed
}
pub fn mark_dirty(&mut self) {
self.coord_changed = true;
}
pub fn op_count(&self) -> usize {
self.op_count
}
pub fn clear(&mut self) {
self.additions.clear();
self.removals.clear();
self.additions_in.clear();
self.removals_in.clear();
self.op_count = 0;
self.coord_changed = false;
}
pub fn additions_iter(&self) -> impl Iterator<Item = (&VertexId, &FxHashSet<VertexId>)> {
self.additions.iter()
}
pub fn removals_for(&self, from: VertexId) -> impl Iterator<Item = VertexId> + '_ {
self.removals
.get(&from)
.into_iter()
.flat_map(|set| set.iter().copied())
}
pub fn apply_to_csr(
&self,
base: &CsrEdges,
coords: &[AbsCoord],
vertex_ids: &[u32],
) -> CsrEdges {
let mut adjacency = Vec::with_capacity(vertex_ids.len());
for &vid in vertex_ids {
let v = VertexId(vid);
let merged = self.merged_view(base, v);
let targets: Vec<u32> = merged.into_iter().map(|id| id.0).collect();
adjacency.push((vid, targets));
}
CsrEdges::from_adjacency(adjacency, coords)
}
}
impl Default for DeltaEdgeSlab {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug)]
pub struct CsrMutableEdges {
base: CsrEdges,
delta: DeltaEdgeSlab,
coords: Vec<AbsCoord>,
vertex_ids: Vec<u32>,
vertex_pos: FxHashMap<u32, usize>,
batch_depth: usize,
rebuild_count: u64,
}
impl CsrMutableEdges {
pub fn new() -> Self {
Self {
base: CsrEdges::empty(),
delta: DeltaEdgeSlab::new(),
coords: Vec::new(),
vertex_ids: Vec::new(),
vertex_pos: FxHashMap::default(),
batch_depth: 0,
rebuild_count: 0,
}
}
pub fn with_coords(coords: Vec<AbsCoord>) -> Self {
let num_vertices = coords.len();
let vertex_ids: Vec<u32> = (0..num_vertices as u32).collect();
let adjacency: Vec<_> = vertex_ids.iter().map(|&id| (id, Vec::new())).collect();
let vertex_pos = vertex_ids
.iter()
.enumerate()
.map(|(idx, &id)| (id, idx))
.collect();
Self {
base: CsrEdges::from_adjacency(adjacency, &coords),
delta: DeltaEdgeSlab::new(),
coords,
vertex_ids,
vertex_pos,
batch_depth: 0,
rebuild_count: 0,
}
}
pub fn add_edge(&mut self, from: VertexId, to: VertexId) {
self.delta.add_edge(from, to);
self.maybe_rebuild();
}
pub fn remove_edge(&mut self, from: VertexId, to: VertexId) {
self.delta.remove_edge(from, to);
self.maybe_rebuild();
}
pub fn out_edges(&self, v: VertexId) -> Vec<VertexId> {
if self.delta.op_count() == 0 {
self.base.out_edges(v).to_vec()
} else {
self.delta.merged_view(&self.base, v)
}
}
#[inline]
pub fn out_edges_ref(&self, v: VertexId) -> Option<&[VertexId]> {
if self.delta.op_count() == 0 {
Some(self.base.out_edges(v))
} else {
None
}
}
pub fn in_edges(&self, v: VertexId) -> &[VertexId] {
self.base.in_edges(v)
}
pub fn in_edges_merged(&self, v: VertexId) -> Vec<VertexId> {
if self.delta.op_count() == 0 {
self.base.in_edges(v).to_vec()
} else {
self.delta.merged_in_view(&self.base, v)
}
}
#[inline]
pub fn in_edges_ref(&self, v: VertexId) -> Option<&[VertexId]> {
if self.delta.op_count() == 0 {
Some(self.base.in_edges(v))
} else {
None
}
}
pub fn delta_size(&self) -> usize {
self.delta.op_count()
}
pub fn num_edges_exact(&self) -> usize {
if self.delta.op_count() == 0 {
return self.base.num_edges();
}
self.vertex_ids
.iter()
.map(|&id| self.out_edges(VertexId(id)).len())
.sum()
}
pub fn rebuild(&mut self) {
if self.delta.op_count() > 0 || self.delta.needs_rebuild() {
self.base = self
.delta
.apply_to_csr(&self.base, &self.coords, &self.vertex_ids);
self.delta.clear();
self.rebuild_count += 1;
}
}
pub fn rebuild_count(&self) -> u64 {
self.rebuild_count
}
fn maybe_rebuild(&mut self) {
if self.batch_depth == 0 && self.delta.needs_rebuild() {
self.rebuild();
}
}
pub fn begin_batch(&mut self) {
self.batch_depth = self.batch_depth.saturating_add(1);
}
pub fn end_batch(&mut self) {
self.batch_depth = self.batch_depth.saturating_sub(1);
if self.batch_depth == 0 && self.delta.needs_rebuild() {
self.rebuild();
}
}
pub fn add_vertex(&mut self, coord: AbsCoord, vertex_id: u32) -> usize {
let idx = self.coords.len();
self.coords.push(coord);
self.vertex_ids.push(vertex_id);
self.vertex_pos.insert(vertex_id, idx);
idx
}
pub fn add_vertices_batch(&mut self, items: &[(AbsCoord, u32)]) {
if items.is_empty() {
return;
}
let start_len = self.coords.len();
self.coords.reserve(items.len());
self.vertex_ids.reserve(items.len());
for (coord, vid) in items {
let idx = self.coords.len();
self.coords.push(*coord);
self.vertex_ids.push(*vid);
self.vertex_pos.insert(*vid, idx);
}
self.rebuild();
debug_assert_eq!(self.coords.len(), start_len + items.len());
}
pub fn update_coord(&mut self, vertex_id: VertexId, new_coord: AbsCoord) {
if let Some(&pos) = self.vertex_pos.get(&vertex_id.0) {
debug_assert_eq!(
self.vertex_ids[pos], vertex_id.0,
"vertex_pos out of sync with vertex_ids at position {pos}"
);
self.coords[pos] = new_coord;
self.delta.mark_dirty();
}
}
pub fn adjacency_with_carried_forward_edges(
&self,
mut adjacency: Vec<(u32, Vec<u32>)>,
) -> Vec<(u32, Vec<u32>)> {
let covered: FxHashSet<u32> = adjacency.iter().map(|(vid, _)| *vid).collect();
for &vid in &self.vertex_ids {
if covered.contains(&vid) {
continue;
}
let v = VertexId(vid);
let merged = if self.delta.op_count() == 0 {
self.base.out_edges(v).to_vec()
} else {
self.delta.merged_view(&self.base, v)
};
if !merged.is_empty() {
adjacency.push((vid, merged.into_iter().map(|v| v.0).collect()));
}
}
for (&from, adds) in self.delta.additions_iter() {
if covered.contains(&from.0) {
continue;
}
if adjacency.iter().any(|(v, _)| *v == from.0) {
continue;
}
let removals: FxHashSet<u32> = self.delta.removals_for(from).map(|v| v.0).collect();
let mut base_set: FxHashSet<u32> =
self.base.out_edges(from).iter().map(|v| v.0).collect();
for r in &removals {
base_set.remove(r);
}
for &add in adds {
base_set.insert(add.0);
}
if !base_set.is_empty() {
let mut targets: Vec<u32> = base_set.into_iter().collect();
targets.sort_unstable();
adjacency.push((from.0, targets));
}
}
adjacency
}
pub fn build_from_adjacency(
&mut self,
adjacency: Vec<(u32, Vec<u32>)>,
coords: Vec<AbsCoord>,
vertex_ids: Vec<u32>,
) {
self.base = CsrEdges::from_adjacency(adjacency, &coords);
self.coords = coords;
self.vertex_ids = vertex_ids;
self.vertex_pos = self
.vertex_ids
.iter()
.enumerate()
.map(|(idx, &id)| (id, idx))
.collect();
self.delta.clear();
}
}
impl Default for CsrMutableEdges {
fn default() -> Self {
Self::new()
}
}