use log::{debug, info};
use rayon::prelude::*;
use smartcore::linalg::basic::{
arrays::{Array, Array2},
matrix::DenseMatrix,
};
use std::collections::HashSet;
use crate::analysis::motives::Motives;
use crate::analysis::subgraphs::{Subgraph, SubgraphConfig, SubgraphsMotive};
use crate::core::ArrowSpace;
use crate::graph::{GraphLaplacian, GraphParams};
use crate::laplacian::build_laplacian_matrix;
impl Subgraph {
pub fn from_parent(parent: &GraphLaplacian, nodes: &[usize], n_items: Option<usize>) -> Self {
let x_motif = nodes.len();
let (f_parent, n_parent) = parent.init_data.shape();
debug!(
"Building motif subgraph with {} centroids from parent with {} centroids (F={})",
x_motif, n_parent, f_parent
);
let sub_init_fx = extract_columns(&parent.init_data, nodes);
let params = &parent.graph_params;
let graph_params = GraphParams {
eps: params.eps,
k: params.k,
topk: params.topk,
p: params.p,
sigma: params.sigma,
normalise: params.normalise,
sparsity_check: params.sparsity_check,
};
let feature_gl =
build_laplacian_matrix(sub_init_fx.clone(), &graph_params, n_items, parent.energy);
let (lf_rows, lf_cols) = feature_gl.matrix.shape();
debug_assert_eq!(lf_rows, f_parent);
debug_assert_eq!(lf_cols, f_parent);
let local_gl = GraphLaplacian {
init_data: sub_init_fx,
matrix: feature_gl.matrix,
nnodes: x_motif,
graph_params: feature_gl.graph_params.clone(),
energy: feature_gl.energy,
};
debug!(
"Subgraph feature Laplacian built: {} motif centroids, {} features, {} nnz",
x_motif,
f_parent,
local_gl.matrix.nnz()
);
Subgraph {
node_indices: nodes.to_vec(),
item_indices: None,
laplacian: local_gl,
rayleigh: None,
}
}
pub fn compute_rayleigh(&mut self) {
let (f_dim, _) = self.laplacian.init_data.shape();
if f_dim == 0 {
self.rayleigh = Some(f64::INFINITY);
return;
}
let indicator = vec![1.0; f_dim];
let r = self.laplacian.rayleigh_quotient(&indicator);
debug!(
"Subgraph Rayleigh cohesion (feature-space): {:.6} over {} dims",
r, f_dim
);
self.rayleigh = Some(r);
}
}
impl SubgraphsMotive for GraphLaplacian {
fn spot_subg_motives(&self, aspace: &ArrowSpace, cfg: &SubgraphConfig) -> Vec<Subgraph> {
info!(
"Spotting subgraphs with motives: topl={}, mintri={}, minsize={}",
cfg.motives.top_l, cfg.motives.min_triangles, cfg.min_size
);
let item_motifs: Vec<Vec<usize>> = self.spot_motives_energy(&aspace, &cfg.motives);
info!(
"Motif detection returned {} item-space candidates",
item_motifs.len()
);
let centroid_map = if let Some(ref cmap) = aspace.centroid_map {
cmap.as_slice()
} else if !aspace.cluster_assignments.is_empty() {
let temp_map: Vec<usize> = aspace
.cluster_assignments
.iter()
.map(|&opt| opt.unwrap_or(0))
.collect();
Box::leak(temp_map.into_boxed_slice())
} else {
panic!("centroid_map or cluster_assignments required for energy subgraphs");
};
let (_f_parent, n_centroids) = self.init_data.shape();
let mut subgraphs: Vec<Subgraph> = item_motifs
.into_par_iter()
.filter(|items| items.len() >= cfg.min_size)
.filter_map(|item_nodes| {
let centroid_set: HashSet<usize> = item_nodes
.iter()
.filter_map(|&item_idx| {
if item_idx < centroid_map.len() {
let cid = centroid_map[item_idx];
if cid < n_centroids { Some(cid) } else { None }
} else {
None
}
})
.collect();
if centroid_set.len() < 2 {
debug!(
"Skipping motif with {} items → {} centroids (need >= 2 for graph)",
item_nodes.len(),
centroid_set.len()
);
return None;
}
let mut centroid_nodes: Vec<usize> = centroid_set.into_iter().collect();
centroid_nodes.sort_unstable();
let mut sg = Subgraph::from_parent(self, ¢roid_nodes, Some(aspace.nitems));
sg.item_indices = Some(item_nodes);
if cfg.rayleigh_max.is_some() {
sg.compute_rayleigh();
}
Some(sg)
})
.collect();
if let Some(max_r) = cfg.rayleigh_max {
subgraphs.retain(|sg| sg.rayleigh.map(|r| r <= max_r).unwrap_or(true));
debug!(
"After Rayleigh filter (max={:.3}): {} subgraphs remain",
max_r,
subgraphs.len()
);
}
info!(
"Extracted {} motives subgraphs (min_size={}, rayleigh_max={:?})",
subgraphs.len(),
cfg.min_size,
cfg.rayleigh_max
);
subgraphs
}
}
fn extract_columns(matrix: &DenseMatrix<f64>, col_indices: &[usize]) -> DenseMatrix<f64> {
let (rows, _cols) = matrix.shape();
let n_sub = col_indices.len();
let mut data = Vec::with_capacity(rows * n_sub);
for row_idx in 0..rows {
for &col_idx in col_indices {
data.push(*matrix.get((row_idx, col_idx)));
}
}
DenseMatrix::from_iterator(data.into_iter(), rows, n_sub, 1)
}