diff --git a/RELEASES.md b/RELEASES.md index 73e1f74..3cd4d14 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -1,5 +1,20 @@ # Release notes +## Unreleased (2023-XX-XX) + +### Added + +- `algorithms::ConvexChecker` to check convexity property of subgraphs of `LinkView`s ([#97][]) + +### Changed + +- References to `PortView`s and `LinkView`s also implement the traits ([#94][]) +- `Toposort` now works with any `LinkView` object ([#96][]) + + [#94]: https://github.com/CQCL/portgraph/issues/94 + [#96]: https://github.com/CQCL/portgraph/issues/96 + [#97]: https://github.com/CQCL/portgraph/issues/97 + ## v0.7.1 (2023-07-13) ### Fixed diff --git a/benches/bench_main.rs b/benches/bench_main.rs index 32b87d0..94e10e0 100644 --- a/benches/bench_main.rs +++ b/benches/bench_main.rs @@ -7,4 +7,5 @@ criterion_main! { benchmarks::hierarchy::benches, benchmarks::portgraph::benches, benchmarks::toposort::benches, + benchmarks::convex::benches, } diff --git a/benches/benchmarks/convex.rs b/benches/benchmarks/convex.rs new file mode 100644 index 0000000..7daf59f --- /dev/null +++ b/benches/benchmarks/convex.rs @@ -0,0 +1,47 @@ +use criterion::{black_box, criterion_group, AxisScale, BenchmarkId, Criterion, PlotConfiguration}; +use portgraph::{algorithms::ConvexChecker, PortView}; + +use super::generators::make_two_track_dag; + +fn bench_convex_construction(c: &mut Criterion) { + let mut g = c.benchmark_group("initialize convex checker object"); + g.plot_config(PlotConfiguration::default().summary_scale(AxisScale::Logarithmic)); + + for size in [100, 1_000, 10_000] { + g.bench_with_input( + BenchmarkId::new("initalize_convexity", size), + &size, + |b, size| { + let graph = make_two_track_dag(*size); + b.iter(|| black_box(ConvexChecker::new(&graph))) + }, + ); + } + g.finish(); +} + +/// We benchmark the worst case scenario, where the "subgraph" is the +/// entire graph itself. +fn bench_convex(c: &mut Criterion) { + let mut g = c.benchmark_group("Runtime convexity check"); + g.plot_config(PlotConfiguration::default().summary_scale(AxisScale::Logarithmic)); + + for size in [100, 1_000, 10_000] { + let graph = make_two_track_dag(size); + let mut checker = ConvexChecker::new(&graph); + g.bench_with_input( + BenchmarkId::new("check_convexity", size), + &size, + |b, _size| b.iter(|| black_box(checker.is_node_convex(graph.nodes_iter()))), + ); + } + g.finish(); +} + +criterion_group! { + name = benches; + config = Criterion::default(); + targets = + bench_convex, + bench_convex_construction +} diff --git a/benches/benchmarks/mod.rs b/benches/benchmarks/mod.rs index 2682cb9..6664f76 100644 --- a/benches/benchmarks/mod.rs +++ b/benches/benchmarks/mod.rs @@ -1,5 +1,6 @@ pub mod generators; +pub mod convex; pub mod hierarchy; pub mod portgraph; pub mod toposort; diff --git a/src/algorithms.rs b/src/algorithms.rs index 6c52858..cd15b75 100644 --- a/src/algorithms.rs +++ b/src/algorithms.rs @@ -1,9 +1,11 @@ //! Algorithm implementations for portgraphs. +mod convex; mod dominators; mod post_order; mod toposort; +pub use convex::ConvexChecker; pub use dominators::{dominators, dominators_filtered, DominatorTree}; pub use post_order::{postorder, postorder_filtered, PostOrder}; pub use toposort::{toposort, toposort_filtered, TopoSort}; diff --git a/src/algorithms/convex.rs b/src/algorithms/convex.rs new file mode 100644 index 0000000..d4bd75c --- /dev/null +++ b/src/algorithms/convex.rs @@ -0,0 +1,288 @@ +//! Convexity checking for portgraphs. +//! +//! This is based on a [`ConvexChecker`] object that is expensive to create +//! (linear in the size of the graph), but can be reused to check multiple +//! subgraphs for convexity quickly. + +use std::collections::BTreeSet; + +use bitvec::bitvec; +use bitvec::vec::BitVec; + +use crate::algorithms::toposort; +use crate::{Direction, LinkView, NodeIndex, PortIndex, SecondaryMap, UnmanagedDenseMap}; + +use super::TopoSort; + +/// A pre-computed datastructure for fast convexity checking. +pub struct ConvexChecker { + graph: G, + // The nodes in topological order + topsort_nodes: Vec, + // The index of a node in the topological order (the inverse of topsort_nodes) + topsort_ind: UnmanagedDenseMap, + // A temporary datastructure used during `is_convex` + causal: CausalVec, +} + +impl ConvexChecker +where + G: LinkView + Copy, +{ + /// Create a new ConvexChecker. + pub fn new(graph: G) -> Self { + let inputs = graph + .nodes_iter() + .filter(|&n| graph.input_neighbours(n).count() == 0); + let topsort: TopoSort<_> = toposort(graph, inputs, Direction::Outgoing); + let topsort_nodes: Vec<_> = topsort.collect(); + let mut topsort_ind = UnmanagedDenseMap::with_capacity(graph.node_count()); + for (i, &n) in topsort_nodes.iter().enumerate() { + topsort_ind.set(n, i); + } + let causal = CausalVec::new(topsort_nodes.len()); + Self { + graph, + topsort_nodes, + topsort_ind, + causal, + } + } + + /// The graph on which convexity queries can be made. + pub fn graph(&self) -> G { + self.graph + } + + /// Whether the subgraph induced by the node set is convex. + /// + /// An induced subgraph is convex if there is no node that is both in the + /// past and in the future of another node of the subgraph. + /// + /// This function requires mutable access to `self` because it uses a + /// temporary datastructure within the object. + /// + /// ## Arguments + /// + /// - `nodes`: The nodes inducing a subgraph of `self.graph()`. + /// + /// ## Algorithm + /// + /// Each node in the "vicinity" of the subgraph will be assigned a causal + /// property, either of being in the past or in the future of the subgraph. + /// It can then be checked whether there is a node in the past that is also + /// in the future, violating convexity. + /// + /// Currently, the "vicinity" of a subgraph is defined as the set of nodes + /// that are in the interval between the first and last node of the subgraph + /// in some topological order. In the worst case this will traverse every + /// node in the graph and can be improved on in the future. + pub fn is_node_convex(&mut self, nodes: impl IntoIterator) -> bool { + let nodes: BTreeSet<_> = nodes.into_iter().map(|n| self.topsort_ind[n]).collect(); + let min_ind = *nodes.first().unwrap(); + let max_ind = *nodes.last().unwrap(); + for ind in min_ind..=max_ind { + let n = self.topsort_nodes[ind]; + let mut in_inds = { + let in_neighs = self.graph.input_neighbours(n); + in_neighs + .map(|n| self.topsort_ind[n]) + .filter(|&ind| ind >= min_ind) + }; + if nodes.contains(&ind) { + if in_inds.any(|ind| self.causal.get(ind) == Causal::Future) { + // There is a node in the past that is also in the future! + return false; + } + self.causal.set(ind, Causal::Past); + } else { + let ind_causal = match in_inds + .any(|ind| nodes.contains(&ind) || self.causal.get(ind) == Causal::Future) + { + true => Causal::Future, + false => Causal::Past, + }; + self.causal.set(ind, ind_causal); + } + } + true + } + + /// Whether a subgraph is convex. + /// + /// A subgraph is convex if there is no path between two nodes of the + /// sugraph that has an edge outside of the subgraph. + /// + /// Equivalently, we check the following two conditions: + /// - There is no node that is both in the past and in the future of + /// another node of the subgraph (convexity on induced subgraph), + /// - There is no edge from an output port to an input port. + /// + /// This function requires mutable access to `self` because it uses a + /// temporary datastructure within the object. + /// + /// ## Arguments + /// + /// - `nodes`: The nodes of the subgraph of `self.graph`, + /// - `inputs`: The input ports of the subgraph of `self.graph`. These must + /// be [`Direction::Incoming`] ports of a node in `nodes`, + /// - `outputs`: The output ports of the subgraph of `self.graph`. These + /// must be [`Direction::Outgoing`] ports of a node in `nodes`. + /// + /// Any edge between two nodes of the subgraph that does not have an explicit + /// input or output port is considered within the subgraph. + pub fn is_convex( + &mut self, + nodes: impl IntoIterator, + inputs: impl IntoIterator, + outputs: impl IntoIterator, + ) -> bool { + let pre_outputs: BTreeSet<_> = outputs + .into_iter() + .filter_map(|p| Some(self.graph.port_link(p)?.into())) + .collect(); + if inputs.into_iter().any(|p| pre_outputs.contains(&p)) { + return false; + } + self.is_node_convex(nodes) + } +} + +/// Whether a node is in the past or in the future of a subgraph. +#[derive(Default, Clone, Debug, PartialEq, Eq)] +enum Causal { + #[default] + Past, + Future, +} + +/// A memory-efficient substitute for `Vec`. +struct CausalVec(BitVec); + +impl From for Causal { + fn from(b: bool) -> Self { + match b { + true => Self::Future, + false => Self::Past, + } + } +} + +impl From for bool { + fn from(c: Causal) -> Self { + match c { + Causal::Past => false, + Causal::Future => true, + } + } +} + +impl CausalVec { + fn new(len: usize) -> Self { + Self(bitvec![0; len]) + } + + fn set(&mut self, index: usize, causal: Causal) { + self.0.set(index, causal.into()); + } + + fn get(&self, index: usize) -> Causal { + self.0[index].into() + } +} + +#[cfg(test)] +mod tests { + use crate::{LinkMut, NodeIndex, PortGraph, PortMut, PortView}; + + use super::ConvexChecker; + + fn graph() -> (PortGraph, [NodeIndex; 7]) { + let mut g = PortGraph::new(); + let i1 = g.add_node(0, 2); + let i2 = g.add_node(0, 1); + let i3 = g.add_node(0, 1); + + let n1 = g.add_node(2, 2); + g.link_nodes(i1, 0, n1, 0).unwrap(); + g.link_nodes(i2, 0, n1, 1).unwrap(); + + let n2 = g.add_node(2, 2); + g.link_nodes(i1, 1, n2, 0).unwrap(); + g.link_nodes(i3, 0, n2, 1).unwrap(); + + let o1 = g.add_node(2, 0); + g.link_nodes(n1, 0, o1, 0).unwrap(); + g.link_nodes(n2, 0, o1, 1).unwrap(); + + let o2 = g.add_node(2, 0); + g.link_nodes(n1, 1, o2, 0).unwrap(); + g.link_nodes(n2, 1, o2, 1).unwrap(); + + (g, [i1, i2, i3, n1, n2, o1, o2]) + } + + #[test] + fn induced_convexity_test() { + let (g, [i1, i2, i3, n1, n2, o1, o2]) = graph(); + let mut checker = ConvexChecker::new(&g); + + assert!(checker.is_node_convex([i1, i2, i3])); + assert!(checker.is_node_convex([i1, n2])); + assert!(!checker.is_node_convex([i1, n2, o2])); + assert!(!checker.is_node_convex([i1, n2, o1])); + assert!(checker.is_node_convex([i1, n2, o1, n1])); + assert!(checker.is_node_convex([i1, n2, o2, n1])); + assert!(checker.is_node_convex([i1, i3, n2])); + assert!(!checker.is_node_convex([i1, i3, o2])); + } + + #[test] + fn edge_convexity_test() { + let (g, [i1, i2, _, n1, n2, _, o2]) = graph(); + let mut checker = ConvexChecker::new(&g); + + assert!(checker.is_convex( + [i1, n2], + [g.input(n2, 1).unwrap()], + [ + g.output(i1, 0).unwrap(), + g.output(n2, 0).unwrap(), + g.output(n2, 1).unwrap() + ] + )); + + assert!(checker.is_convex( + [i2, n1, o2], + [g.input(n1, 0).unwrap(), g.input(o2, 1).unwrap()], + [g.output(n1, 0).unwrap(),] + )); + + assert!(!checker.is_convex( + [i2, n1, o2], + [ + g.input(n1, 0).unwrap(), + g.input(o2, 1).unwrap(), + g.input(o2, 0).unwrap() + ], + [g.output(n1, 0).unwrap(), g.output(n1, 1).unwrap()] + )); + } + + #[test] + fn dangling_input() { + let mut g = PortGraph::new(); + let n = g.add_node(1, 1); + let mut checker = ConvexChecker::new(&g); + assert!(checker.is_node_convex([n])); + } + + #[test] + fn disconnected_graph() { + let mut g = PortGraph::new(); + let n = g.add_node(1, 1); + g.add_node(1, 1); + let mut checker = ConvexChecker::new(&g); + assert!(checker.is_node_convex([n])); + } +}