Skip to content

Commit

Permalink
Add convexity algorithm (#97)
Browse files Browse the repository at this point in the history
  • Loading branch information
lmondada authored Jul 31, 2023
1 parent 32ea9f6 commit 19bc312
Show file tree
Hide file tree
Showing 6 changed files with 354 additions and 0 deletions.
15 changes: 15 additions & 0 deletions RELEASES.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
1 change: 1 addition & 0 deletions benches/bench_main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ criterion_main! {
benchmarks::hierarchy::benches,
benchmarks::portgraph::benches,
benchmarks::toposort::benches,
benchmarks::convex::benches,
}
47 changes: 47 additions & 0 deletions benches/benchmarks/convex.rs
Original file line number Diff line number Diff line change
@@ -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
}
1 change: 1 addition & 0 deletions benches/benchmarks/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
pub mod generators;

pub mod convex;
pub mod hierarchy;
pub mod portgraph;
pub mod toposort;
2 changes: 2 additions & 0 deletions src/algorithms.rs
Original file line number Diff line number Diff line change
@@ -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};
288 changes: 288 additions & 0 deletions src/algorithms/convex.rs
Original file line number Diff line number Diff line change
@@ -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<G> {
graph: G,
// The nodes in topological order
topsort_nodes: Vec<NodeIndex>,
// The index of a node in the topological order (the inverse of topsort_nodes)
topsort_ind: UnmanagedDenseMap<NodeIndex, usize>,
// A temporary datastructure used during `is_convex`
causal: CausalVec,
}

impl<G> ConvexChecker<G>
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<Item = NodeIndex>) -> 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<Item = NodeIndex>,
inputs: impl IntoIterator<Item = PortIndex>,
outputs: impl IntoIterator<Item = PortIndex>,
) -> 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<Causal>`.
struct CausalVec(BitVec);

impl From<bool> for Causal {
fn from(b: bool) -> Self {
match b {
true => Self::Future,
false => Self::Past,
}
}
}

impl From<Causal> 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]));
}
}

0 comments on commit 19bc312

Please sign in to comment.