use alloc::vec::Vec;
use core::{
fmt::{self, Debug},
hash::{BuildHasher, Hash},
ops::{Deref, DerefMut},
};
use bevy_platform::{
collections::{HashMap, HashSet},
hash::FixedHasher,
};
use fixedbitset::FixedBitSet;
use indexmap::IndexSet;
use thiserror::Error;
use crate::{
error::Result,
schedule::graph::{
index, row_col, DiGraph, DiGraphToposortError,
Direction::{Incoming, Outgoing},
GraphNodeId, UnGraph,
},
};
#[derive(Clone)]
pub struct Dag<N: GraphNodeId, S: BuildHasher = FixedHasher> {
graph: DiGraph<N, S>,
toposort: Vec<N>,
dirty: bool,
}
impl<N: GraphNodeId, S: BuildHasher> Dag<N, S> {
pub fn new() -> Self
where
S: Default,
{
Self::default()
}
#[must_use]
pub fn graph(&self) -> &DiGraph<N, S> {
&self.graph
}
#[must_use = "This function marks the graph as dirty, so it should be used."]
pub fn graph_mut(&mut self) -> &mut DiGraph<N, S> {
self.dirty = true;
&mut self.graph
}
#[must_use]
pub fn is_dirty(&self) -> bool {
self.dirty
}
#[must_use]
pub fn is_toposorted(&self) -> bool {
!self.dirty
}
pub fn ensure_toposorted(&mut self) -> Result<(), DiGraphToposortError<N>> {
if self.dirty {
self.toposort = self.graph.toposort(core::mem::take(&mut self.toposort))?;
self.dirty = false;
}
Ok(())
}
#[must_use = "This method only returns a cached value and does not compute anything."]
pub fn get_toposort(&self) -> Option<&[N]> {
if self.dirty {
None
} else {
Some(&self.toposort)
}
}
pub fn toposort(&mut self) -> Result<&[N], DiGraphToposortError<N>> {
self.ensure_toposorted()?;
Ok(&self.toposort)
}
pub fn toposort_and_graph(
&mut self,
) -> Result<(&[N], &DiGraph<N, S>), DiGraphToposortError<N>> {
self.ensure_toposorted()?;
Ok((&self.toposort, &self.graph))
}
pub fn analyze(&mut self) -> Result<DagAnalysis<N, S>, DiGraphToposortError<N>>
where
S: Default,
{
let (toposort, graph) = self.toposort_and_graph()?;
Ok(DagAnalysis::new(graph, toposort))
}
pub fn remove_redundant_edges(&mut self, analysis: &DagAnalysis<N, S>)
where
S: Clone,
{
self.graph = analysis.transitive_reduction.clone();
}
pub fn group_by_key<K, V>(
&mut self,
num_groups: usize,
) -> Result<DagGroups<K, V, S>, DiGraphToposortError<N>>
where
N: TryInto<K, Error = V>,
K: Eq + Hash,
V: Clone + Eq + Hash,
S: BuildHasher + Default,
{
let (toposort, graph) = self.toposort_and_graph()?;
Ok(DagGroups::with_capacity(num_groups, graph, toposort))
}
pub fn try_convert<T>(self) -> Result<Dag<T, S>, N::Error>
where
N: TryInto<T>,
T: GraphNodeId,
S: Default,
{
Ok(Dag {
graph: self.graph.try_convert()?,
toposort: Vec::new(),
dirty: true,
})
}
}
impl<N: GraphNodeId, S: BuildHasher> Deref for Dag<N, S> {
type Target = DiGraph<N, S>;
fn deref(&self) -> &Self::Target {
self.graph()
}
}
impl<N: GraphNodeId, S: BuildHasher> DerefMut for Dag<N, S> {
fn deref_mut(&mut self) -> &mut Self::Target {
self.graph_mut()
}
}
impl<N: GraphNodeId, S: BuildHasher + Default> Default for Dag<N, S> {
fn default() -> Self {
Self {
graph: Default::default(),
toposort: Default::default(),
dirty: false,
}
}
}
impl<N: GraphNodeId, S: BuildHasher> Debug for Dag<N, S> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.dirty {
f.debug_struct("Dag")
.field("graph", &self.graph)
.field("dirty", &self.dirty)
.finish()
} else {
f.debug_struct("Dag")
.field("graph", &self.graph)
.field("toposort", &self.toposort)
.finish()
}
}
}
pub struct DagAnalysis<N: GraphNodeId, S: BuildHasher = FixedHasher> {
reachable: FixedBitSet,
connected: HashSet<(N, N), S>,
disconnected: Vec<(N, N)>,
transitive_edges: Vec<(N, N)>,
transitive_reduction: DiGraph<N, S>,
transitive_closure: DiGraph<N, S>,
}
impl<N: GraphNodeId, S: BuildHasher> DagAnalysis<N, S> {
pub fn new(graph: &DiGraph<N, S>, topological_order: &[N]) -> Self
where
S: Default,
{
if graph.node_count() == 0 {
return DagAnalysis::default();
}
let n = graph.node_count();
let mut map = <HashMap<_, _>>::with_capacity_and_hasher(n, Default::default());
let mut topsorted =
DiGraph::<N>::with_capacity(topological_order.len(), graph.edge_count());
for (i, &node) in topological_order.iter().enumerate() {
map.insert(node, i);
topsorted.add_node(node);
for pred in graph.neighbors_directed(node, Incoming) {
topsorted.add_edge(pred, node);
}
}
let mut reachable = FixedBitSet::with_capacity(n * n);
let mut connected = HashSet::default();
let mut disconnected = Vec::default();
let mut transitive_edges = Vec::default();
let mut transitive_reduction = DiGraph::with_capacity(topsorted.node_count(), 0);
let mut transitive_closure = DiGraph::with_capacity(topsorted.node_count(), 0);
let mut visited = FixedBitSet::with_capacity(n);
for node in topsorted.nodes() {
transitive_reduction.add_node(node);
transitive_closure.add_node(node);
}
for a in topsorted.nodes().rev() {
let index_a = *map.get(&a).unwrap();
for b in topsorted.neighbors_directed(a, Outgoing) {
let index_b = *map.get(&b).unwrap();
debug_assert!(index_a < index_b);
if !visited[index_b] {
transitive_reduction.add_edge(a, b);
transitive_closure.add_edge(a, b);
reachable.insert(index(index_a, index_b, n));
let successors = transitive_closure
.neighbors_directed(b, Outgoing)
.collect::<Vec<_>>();
for c in successors {
let index_c = *map.get(&c).unwrap();
debug_assert!(index_b < index_c);
if !visited[index_c] {
visited.insert(index_c);
transitive_closure.add_edge(a, c);
reachable.insert(index(index_a, index_c, n));
}
}
} else {
transitive_edges.push((a, b));
}
}
visited.clear();
}
for i in 0..(n - 1) {
for index in index(i, i + 1, n)..=index(i, n - 1, n) {
let (a, b) = row_col(index, n);
let pair = (topological_order[a], topological_order[b]);
if reachable[index] {
connected.insert(pair);
} else {
disconnected.push(pair);
}
}
}
DagAnalysis {
reachable,
connected,
disconnected,
transitive_edges,
transitive_reduction,
transitive_closure,
}
}
pub fn reachable(&self) -> &FixedBitSet {
&self.reachable
}
pub fn connected(&self) -> &HashSet<(N, N), S> {
&self.connected
}
pub fn disconnected(&self) -> &[(N, N)] {
&self.disconnected
}
pub fn transitive_edges(&self) -> &[(N, N)] {
&self.transitive_edges
}
pub fn transitive_reduction(&self) -> &DiGraph<N, S> {
&self.transitive_reduction
}
pub fn transitive_closure(&self) -> &DiGraph<N, S> {
&self.transitive_closure
}
pub fn check_for_redundant_edges(&self) -> Result<(), DagRedundancyError<N>>
where
S: Clone,
{
if self.transitive_edges.is_empty() {
Ok(())
} else {
Err(DagRedundancyError(self.transitive_edges.clone()))
}
}
pub fn check_for_cross_dependencies(
&self,
other: &Self,
) -> Result<(), DagCrossDependencyError<N>> {
for &(a, b) in &self.connected {
if other.connected.contains(&(a, b)) || other.connected.contains(&(b, a)) {
return Err(DagCrossDependencyError(a, b));
}
}
Ok(())
}
pub fn check_for_overlapping_groups<K, V>(
&self,
groups: &DagGroups<K, V>,
) -> Result<(), DagOverlappingGroupError<K>>
where
N: TryInto<K>,
K: Eq + Hash,
V: Eq + Hash,
{
for &(a, b) in &self.connected {
let (Ok(a_key), Ok(b_key)) = (a.try_into(), b.try_into()) else {
continue;
};
let a_group = groups.get(&a_key).unwrap();
let b_group = groups.get(&b_key).unwrap();
if !a_group.is_disjoint(b_group) {
return Err(DagOverlappingGroupError(a_key, b_key));
}
}
Ok(())
}
}
impl<N: GraphNodeId, S: BuildHasher + Default> Default for DagAnalysis<N, S> {
fn default() -> Self {
Self {
reachable: Default::default(),
connected: Default::default(),
disconnected: Default::default(),
transitive_edges: Default::default(),
transitive_reduction: Default::default(),
transitive_closure: Default::default(),
}
}
}
impl<N: GraphNodeId, S: BuildHasher> Debug for DagAnalysis<N, S> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("DagAnalysis")
.field("reachable", &self.reachable)
.field("connected", &self.connected)
.field("disconnected", &self.disconnected)
.field("transitive_edges", &self.transitive_edges)
.field("transitive_reduction", &self.transitive_reduction)
.field("transitive_closure", &self.transitive_closure)
.finish()
}
}
pub struct DagGroups<K, V, S = FixedHasher>(HashMap<K, IndexSet<V, S>, S>);
impl<K: Eq + Hash, V: Clone + Eq + Hash, S: BuildHasher + Default> DagGroups<K, V, S> {
pub fn new<N>(graph: &DiGraph<N, S>, toposort: &[N]) -> Self
where
N: GraphNodeId + TryInto<K, Error = V>,
{
Self::with_capacity(0, graph, toposort)
}
pub fn with_capacity<N>(capacity: usize, graph: &DiGraph<N, S>, toposort: &[N]) -> Self
where
N: GraphNodeId + TryInto<K, Error = V>,
{
let mut groups: HashMap<K, IndexSet<V, S>, S> =
HashMap::with_capacity_and_hasher(capacity, Default::default());
for &id in toposort.iter().rev() {
let Ok(key) = id.try_into() else {
continue;
};
let mut children = IndexSet::default();
for node in graph.neighbors_directed(id, Outgoing) {
match node.try_into() {
Ok(key) => {
let key_children = groups.get(&key).unwrap();
children.extend(key_children.iter().cloned());
}
Err(value) => {
children.insert(value);
}
}
}
groups.insert(key, children);
}
Self(groups)
}
}
impl<K: GraphNodeId, V: GraphNodeId, S: BuildHasher> DagGroups<K, V, S> {
pub fn flatten<N>(
&self,
dag: Dag<N>,
mut collapse_group: impl FnMut(K, &IndexSet<V, S>, &Dag<N>, &mut Vec<(N, N)>),
) -> Dag<V>
where
N: GraphNodeId + TryInto<V, Error = K> + From<K> + From<V>,
{
let mut flattening = dag;
let mut temp = Vec::new();
for (&key, values) in self.iter() {
collapse_group(key, values, &flattening, &mut temp);
if values.is_empty() {
for a in flattening.neighbors_directed(N::from(key), Incoming) {
for b in flattening.neighbors_directed(N::from(key), Outgoing) {
temp.push((a, b));
}
}
} else {
for a in flattening.neighbors_directed(N::from(key), Incoming) {
for &value in values {
temp.push((a, N::from(value)));
}
}
for b in flattening.neighbors_directed(N::from(key), Outgoing) {
for &value in values {
temp.push((N::from(value), b));
}
}
}
flattening.remove_node(N::from(key));
flattening.reserve_edges(temp.len());
for (a, b) in temp.drain(..) {
flattening.add_edge(a, b);
}
}
flattening
.try_convert::<V>()
.unwrap_or_else(|n| unreachable!("Flattened graph has a leftover key {n:?}"))
}
pub fn flatten_undirected<N>(&self, graph: &UnGraph<N>) -> UnGraph<V>
where
N: GraphNodeId + TryInto<V, Error = K>,
{
let mut flattened = UnGraph::default();
for (lhs, rhs) in graph.all_edges() {
match (lhs.try_into(), rhs.try_into()) {
(Ok(lhs), Ok(rhs)) => {
flattened.add_edge(lhs, rhs);
}
(Err(lhs_key), Ok(rhs)) => {
let Some(lhs_group) = self.get(&lhs_key) else {
continue;
};
flattened.reserve_edges(lhs_group.len());
for &lhs in lhs_group {
flattened.add_edge(lhs, rhs);
}
}
(Ok(lhs), Err(rhs_key)) => {
let Some(rhs_group) = self.get(&rhs_key) else {
continue;
};
flattened.reserve_edges(rhs_group.len());
for &rhs in rhs_group {
flattened.add_edge(lhs, rhs);
}
}
(Err(lhs_key), Err(rhs_key)) => {
let Some(lhs_group) = self.get(&lhs_key) else {
continue;
};
let Some(rhs_group) = self.get(&rhs_key) else {
continue;
};
flattened.reserve_edges(lhs_group.len() * rhs_group.len());
for &lhs in lhs_group {
for &rhs in rhs_group {
flattened.add_edge(lhs, rhs);
}
}
}
}
}
flattened
}
}
impl<K, V, S> Deref for DagGroups<K, V, S> {
type Target = HashMap<K, IndexSet<V, S>, S>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<K, V, S> DerefMut for DagGroups<K, V, S> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl<K, V, S> Default for DagGroups<K, V, S>
where
S: BuildHasher + Default,
{
fn default() -> Self {
Self(Default::default())
}
}
impl<K: Debug, V: Debug, S> Debug for DagGroups<K, V, S> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("DagGroups").field(&self.0).finish()
}
}
#[derive(Error, Debug)]
#[error("DAG has redundant edges: {0:?}")]
pub struct DagRedundancyError<N: GraphNodeId>(pub Vec<(N, N)>);
#[derive(Error, Debug)]
#[error("DAG has a cross-dependency between nodes {0:?} and {1:?}")]
pub struct DagCrossDependencyError<N>(pub N, pub N);
#[derive(Error, Debug)]
#[error("DAG has overlapping groups between keys {0:?} and {1:?}")]
pub struct DagOverlappingGroupError<K>(pub K, pub K);
#[cfg(test)]
mod tests {
use core::ops::DerefMut;
use crate::schedule::graph::{index, Dag, Direction, GraphNodeId, UnGraph};
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
struct TestNode(u32);
impl GraphNodeId for TestNode {
type Adjacent = (TestNode, Direction);
type Edge = (TestNode, TestNode);
fn kind(&self) -> &'static str {
"test node"
}
}
#[test]
fn mark_dirty() {
{
let mut dag = Dag::<TestNode>::new();
dag.add_node(TestNode(1));
assert!(dag.is_dirty());
}
{
let mut dag = Dag::<TestNode>::new();
dag.add_edge(TestNode(1), TestNode(2));
assert!(dag.is_dirty());
}
{
let mut dag = Dag::<TestNode>::new();
dag.deref_mut();
assert!(dag.is_dirty());
}
{
let mut dag = Dag::<TestNode>::new();
let _ = dag.graph_mut();
assert!(dag.is_dirty());
}
}
#[test]
fn toposort() {
let mut dag = Dag::<TestNode>::new();
dag.add_edge(TestNode(1), TestNode(2));
dag.add_edge(TestNode(2), TestNode(3));
dag.add_edge(TestNode(1), TestNode(3));
assert_eq!(
dag.toposort().unwrap(),
&[TestNode(1), TestNode(2), TestNode(3)]
);
assert_eq!(
dag.get_toposort().unwrap(),
&[TestNode(1), TestNode(2), TestNode(3)]
);
}
#[test]
fn analyze() {
let mut dag1 = Dag::<TestNode>::new();
dag1.add_edge(TestNode(1), TestNode(2));
dag1.add_edge(TestNode(2), TestNode(3));
dag1.add_edge(TestNode(1), TestNode(3));
let analysis1 = dag1.analyze().unwrap();
assert!(analysis1.reachable().contains(index(0, 1, 3)));
assert!(analysis1.reachable().contains(index(1, 2, 3)));
assert!(analysis1.reachable().contains(index(0, 2, 3)));
assert!(analysis1.connected().contains(&(TestNode(1), TestNode(2))));
assert!(analysis1.connected().contains(&(TestNode(2), TestNode(3))));
assert!(analysis1.connected().contains(&(TestNode(1), TestNode(3))));
assert!(!analysis1
.disconnected()
.contains(&(TestNode(2), TestNode(1))));
assert!(!analysis1
.disconnected()
.contains(&(TestNode(3), TestNode(2))));
assert!(!analysis1
.disconnected()
.contains(&(TestNode(3), TestNode(1))));
assert!(analysis1
.transitive_edges()
.contains(&(TestNode(1), TestNode(3))));
assert!(analysis1.check_for_redundant_edges().is_err());
let mut dag2 = Dag::<TestNode>::new();
dag2.add_edge(TestNode(3), TestNode(4));
let analysis2 = dag2.analyze().unwrap();
assert!(analysis2.check_for_redundant_edges().is_ok());
assert!(analysis1.check_for_cross_dependencies(&analysis2).is_ok());
let mut dag3 = Dag::<TestNode>::new();
dag3.add_edge(TestNode(1), TestNode(2));
let analysis3 = dag3.analyze().unwrap();
assert!(analysis1.check_for_cross_dependencies(&analysis3).is_err());
dag1.remove_redundant_edges(&analysis1);
let analysis1 = dag1.analyze().unwrap();
assert!(analysis1.check_for_redundant_edges().is_ok());
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Node {
Key(Key),
Value(Value),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
struct Key(u32);
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
struct Value(u32);
impl GraphNodeId for Node {
type Adjacent = (Node, Direction);
type Edge = (Node, Node);
fn kind(&self) -> &'static str {
"node"
}
}
impl TryInto<Key> for Node {
type Error = Value;
fn try_into(self) -> Result<Key, Value> {
match self {
Node::Key(k) => Ok(k),
Node::Value(v) => Err(v),
}
}
}
impl TryInto<Value> for Node {
type Error = Key;
fn try_into(self) -> Result<Value, Key> {
match self {
Node::Value(v) => Ok(v),
Node::Key(k) => Err(k),
}
}
}
impl GraphNodeId for Key {
type Adjacent = (Key, Direction);
type Edge = (Key, Key);
fn kind(&self) -> &'static str {
"key"
}
}
impl GraphNodeId for Value {
type Adjacent = (Value, Direction);
type Edge = (Value, Value);
fn kind(&self) -> &'static str {
"value"
}
}
impl From<Key> for Node {
fn from(key: Key) -> Self {
Node::Key(key)
}
}
impl From<Value> for Node {
fn from(value: Value) -> Self {
Node::Value(value)
}
}
#[test]
fn group_by_key() {
let mut dag = Dag::<Node>::new();
dag.add_edge(Node::Key(Key(1)), Node::Value(Value(10)));
dag.add_edge(Node::Key(Key(1)), Node::Value(Value(11)));
dag.add_edge(Node::Key(Key(2)), Node::Value(Value(20)));
dag.add_edge(Node::Key(Key(2)), Node::Key(Key(1)));
dag.add_edge(Node::Value(Value(10)), Node::Value(Value(11)));
let groups = dag.group_by_key::<Key, Value>(2).unwrap();
assert_eq!(groups.len(), 2);
let group_key1 = groups.get(&Key(1)).unwrap();
assert!(group_key1.contains(&Value(10)));
assert!(group_key1.contains(&Value(11)));
let group_key2 = groups.get(&Key(2)).unwrap();
assert!(group_key2.contains(&Value(10)));
assert!(group_key2.contains(&Value(11)));
assert!(group_key2.contains(&Value(20)));
}
#[test]
fn flatten() {
let mut dag = Dag::<Node>::new();
dag.add_edge(Node::Key(Key(1)), Node::Value(Value(10)));
dag.add_edge(Node::Key(Key(1)), Node::Value(Value(11)));
dag.add_edge(Node::Key(Key(2)), Node::Value(Value(20)));
dag.add_edge(Node::Key(Key(2)), Node::Value(Value(21)));
dag.add_edge(Node::Value(Value(30)), Node::Key(Key(1)));
dag.add_edge(Node::Key(Key(1)), Node::Value(Value(40)));
let groups = dag.group_by_key::<Key, Value>(2).unwrap();
let flattened = groups.flatten(dag, |_key, _values, _dag, _temp| {});
assert!(flattened.contains_node(Value(10)));
assert!(flattened.contains_node(Value(11)));
assert!(flattened.contains_node(Value(20)));
assert!(flattened.contains_node(Value(21)));
assert!(flattened.contains_node(Value(30)));
assert!(flattened.contains_node(Value(40)));
assert!(flattened.contains_edge(Value(30), Value(10)));
assert!(flattened.contains_edge(Value(30), Value(11)));
assert!(flattened.contains_edge(Value(10), Value(40)));
assert!(flattened.contains_edge(Value(11), Value(40)));
}
#[test]
fn flatten_undirected() {
let mut dag = Dag::<Node>::new();
dag.add_edge(Node::Key(Key(1)), Node::Value(Value(10)));
dag.add_edge(Node::Key(Key(1)), Node::Value(Value(11)));
dag.add_edge(Node::Key(Key(2)), Node::Value(Value(20)));
dag.add_edge(Node::Key(Key(2)), Node::Value(Value(21)));
let groups = dag.group_by_key::<Key, Value>(2).unwrap();
let mut ungraph = UnGraph::<Node>::default();
ungraph.add_edge(Node::Value(Value(10)), Node::Value(Value(11)));
ungraph.add_edge(Node::Key(Key(1)), Node::Value(Value(30)));
ungraph.add_edge(Node::Value(Value(40)), Node::Key(Key(2)));
ungraph.add_edge(Node::Key(Key(1)), Node::Key(Key(2)));
let flattened = groups.flatten_undirected(&ungraph);
assert!(flattened.contains_edge(Value(10), Value(11)));
assert!(flattened.contains_edge(Value(10), Value(30)));
assert!(flattened.contains_edge(Value(11), Value(30)));
assert!(flattened.contains_edge(Value(40), Value(20)));
assert!(flattened.contains_edge(Value(40), Value(21)));
assert!(flattened.contains_edge(Value(10), Value(20)));
assert!(flattened.contains_edge(Value(10), Value(21)));
assert!(flattened.contains_edge(Value(11), Value(20)));
assert!(flattened.contains_edge(Value(11), Value(21)));
}
#[test]
fn overlapping_groups() {
let mut dag = Dag::<Node>::new();
dag.add_edge(Node::Key(Key(1)), Node::Value(Value(10)));
dag.add_edge(Node::Key(Key(1)), Node::Value(Value(11)));
dag.add_edge(Node::Key(Key(2)), Node::Value(Value(11))); dag.add_edge(Node::Key(Key(2)), Node::Value(Value(20)));
dag.add_edge(Node::Key(Key(1)), Node::Key(Key(2)));
let groups = dag.group_by_key::<Key, Value>(2).unwrap();
let analysis = dag.analyze().unwrap();
let result = analysis.check_for_overlapping_groups(&groups);
assert!(result.is_err());
}
#[test]
fn disjoint_groups() {
let mut dag = Dag::<Node>::new();
dag.add_edge(Node::Key(Key(1)), Node::Value(Value(10)));
dag.add_edge(Node::Key(Key(1)), Node::Value(Value(11)));
dag.add_edge(Node::Key(Key(2)), Node::Value(Value(20)));
dag.add_edge(Node::Key(Key(2)), Node::Value(Value(21)));
let groups = dag.group_by_key::<Key, Value>(2).unwrap();
let analysis = dag.analyze().unwrap();
let result = analysis.check_for_overlapping_groups(&groups);
assert!(result.is_ok());
}
}