use std::collections::{HashMap, HashSet};
use petgraph::graph::UnGraph;
use crate::canonical::{all_connected_classes, canonical_by, class_to_adj, perms};
use crate::census::{count, Selector};
use crate::snapshot::{GraphAdapter, Snapshot};
use crate::template::{induced_matches_unlabelled, monomorphisms_unlabelled};
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Induced {
Yes,
No,
}
#[derive(Clone, Debug)]
pub struct Pattern {
k: usize,
adj: Vec<Vec<usize>>,
class: u64,
aut: u64,
}
impl Pattern {
#[must_use]
pub fn new(k: usize, edges: &[(usize, usize)]) -> Self {
assert!(
(2..=5).contains(&k),
"catalog patterns are supported for 2 <= k <= 5"
);
let mut adj = vec![Vec::new(); k];
for &(a, b) in edges {
assert!(a < k && b < k && a != b, "edge out of range / self-loop");
if !adj[a].contains(&b) {
adj[a].push(b);
adj[b].push(a);
}
}
assert!(
crate::canonical::connected(&adj),
"catalog patterns must be connected"
);
let ps = perms(k);
let class = canonical_by(k, &ps, |i, j| adj[i].contains(&j));
let aut = s_pc(&adj, &adj, &ps);
Pattern { k, adj, class, aut }
}
#[must_use]
pub fn path(k: usize) -> Self {
let edges: Vec<(usize, usize)> = (0..k.saturating_sub(1)).map(|i| (i, i + 1)).collect();
Pattern::new(k, &edges)
}
#[must_use]
pub fn cycle(k: usize) -> Self {
assert!((3..=5).contains(&k), "cycle C_k needs 3 <= k <= 5");
let mut edges: Vec<(usize, usize)> = (0..k - 1).map(|i| (i, i + 1)).collect();
edges.push((k - 1, 0));
Pattern::new(k, &edges)
}
#[must_use]
pub fn star(k: usize) -> Self {
let edges: Vec<(usize, usize)> = (1..k).map(|i| (0, i)).collect();
Pattern::new(k, &edges)
}
#[must_use]
pub fn complete(k: usize) -> Self {
let edges: Vec<(usize, usize)> = (0..k)
.flat_map(|i| ((i + 1)..k).map(move |j| (i, j)))
.collect();
Pattern::new(k, &edges)
}
#[must_use]
pub fn triangle() -> Self {
Pattern::complete(3)
}
#[must_use]
pub fn claw() -> Self {
Pattern::star(4)
}
#[must_use]
pub fn paw() -> Self {
Pattern::new(4, &[(0, 1), (1, 2), (2, 0), (0, 3)])
}
#[must_use]
pub fn diamond() -> Self {
Pattern::new(4, &[(0, 1), (1, 2), (2, 3), (3, 0), (0, 2)])
}
#[inline]
#[must_use]
pub fn order(&self) -> usize {
self.k
}
#[inline]
#[must_use]
pub fn class_id(&self) -> crate::canonical::ClassId {
crate::canonical::ClassId(self.class)
}
}
fn s_pc(padj: &[Vec<usize>], cadj: &[Vec<usize>], ps: &[Vec<usize>]) -> u64 {
let mut count = 0u64;
for perm in ps {
let mut ok = true;
'outer: for (i, nbrs) in padj.iter().enumerate() {
for &j in nbrs {
if j > i && !cadj[perm[i]].contains(&perm[j]) {
ok = false;
break 'outer;
}
}
}
if ok {
count += 1;
}
}
count
}
#[must_use]
pub fn count_pattern<G>(g: G, pattern: &Pattern, induced: Induced) -> u64
where
G: GraphAdapter,
{
let k = pattern.k;
let census = count(g, &Selector::connected_k_subsets(k));
let by_mask: HashMap<u64, u64> = census.into_iter().map(|(c, n)| (c.0, n)).collect();
match induced {
Induced::Yes => by_mask.get(&pattern.class).copied().unwrap_or(0),
Induced::No => {
let ps = perms(k);
let labelled: u64 = all_connected_classes(k)
.into_iter()
.map(|mask| {
let cnt = by_mask.get(&mask).copied().unwrap_or(0);
if cnt == 0 {
0
} else {
cnt * s_pc(&pattern.adj, &class_to_adj(mask, k), &ps)
}
})
.sum();
labelled / pattern.aut
}
}
}
#[derive(Clone, Debug, Default)]
pub struct MotifCatalog {
patterns: HashMap<String, Pattern>,
}
impl MotifCatalog {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn standard() -> Self {
let mut c = Self::new();
c.register("p3", Pattern::path(3));
c.register("triangle", Pattern::triangle());
c.register("p4", Pattern::path(4));
c.register("c4", Pattern::cycle(4));
c.register("claw", Pattern::claw());
c.register("paw", Pattern::paw());
c.register("diamond", Pattern::diamond());
c.register("k4", Pattern::complete(4));
c.register("p5", Pattern::path(5));
c.register("c5", Pattern::cycle(5));
c.register("star5", Pattern::star(5));
c.register("k5", Pattern::complete(5));
c
}
pub fn register(&mut self, name: impl Into<String>, pattern: Pattern) -> Option<Pattern> {
self.patterns.insert(name.into(), pattern)
}
#[must_use]
pub fn get(&self, name: &str) -> Option<&Pattern> {
self.patterns.get(name)
}
#[must_use]
pub fn names(&self) -> Vec<&str> {
let mut v: Vec<&str> = self.patterns.keys().map(String::as_str).collect();
v.sort_unstable();
v
}
#[must_use]
pub fn len(&self) -> usize {
self.patterns.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.patterns.is_empty()
}
}
fn pattern_automorphisms(pattern: &Pattern) -> Vec<Vec<usize>> {
let k = pattern.k;
perms(k)
.into_iter()
.filter(|p| {
(0..k).all(|i| {
pattern.adj[i]
.iter()
.all(|&j| pattern.adj[p[i]].contains(&p[j]))
})
})
.collect()
}
#[must_use]
pub fn count_motif<G>(g: G, pattern: &Pattern, induced: Induced) -> u64
where
G: GraphAdapter,
{
count_pattern(g, pattern, induced)
}
#[must_use]
pub fn find_motif<G>(g: G, pattern: &Pattern, induced: Induced) -> Vec<Vec<G::NodeId>>
where
G: GraphAdapter,
{
let snapshot = Snapshot::new(g);
let n = snapshot.len();
let k = pattern.k;
let mut host: UnGraph<(), ()> = UnGraph::default();
let hidx: Vec<_> = (0..n).map(|_| host.add_node(())).collect();
for i in 0..n {
for &j in snapshot.neighbors(i) {
if i < j {
host.add_edge(hidx[i], hidx[j], ());
}
}
}
let mut pat: UnGraph<(), ()> = UnGraph::default();
let pidx: Vec<_> = (0..k).map(|_| pat.add_node(())).collect();
for (i, nbrs) in pattern.adj.iter().enumerate() {
for &j in nbrs {
if i < j {
pat.add_edge(pidx[i], pidx[j], ());
}
}
}
let raw = match induced {
Induced::Yes => induced_matches_unlabelled(&pat, &host),
Induced::No => monomorphisms_unlabelled(&pat, &host),
};
let auts = pattern_automorphisms(pattern);
let mut seen: HashSet<Vec<usize>> = HashSet::new();
let mut reps: Vec<Vec<usize>> = Vec::new();
for e in raw {
let key = auts
.iter()
.map(|p| (0..k).map(|c| e[p[c]]).collect::<Vec<usize>>())
.min()
.expect("a pattern has at least the identity automorphism");
if seen.insert(key) {
reps.push(e);
}
}
reps.into_iter()
.map(|e| e.into_iter().map(|hi| snapshot.id(hi)).collect())
.collect()
}
#[derive(Clone, Copy, Debug)]
pub struct Diamond<N> {
pub spine: [N; 2],
pub tips: [N; 2],
}
#[must_use]
pub fn count_diamonds<G>(g: G, induced: Induced) -> u64
where
G: GraphAdapter,
{
count_pattern(g, &Pattern::diamond(), induced)
}
#[must_use]
pub fn find_diamonds<G>(g: G, induced: Induced) -> Vec<Diamond<G::NodeId>>
where
G: GraphAdapter,
{
find_motif(g, &Pattern::diamond(), induced)
.into_iter()
.map(|m| Diamond {
spine: [m[0], m[2]],
tips: [m[1], m[3]],
})
.collect()
}