use crate::base::SelectorMixin;
use scirs2_core::ndarray::{Array1, Array2, ArrayView1, ArrayView2, Axis};
use sklears_core::error::{Result as SklResult, SklearsError};
use sklears_core::traits::{Estimator, Fit, Transform};
use std::collections::HashMap;
use std::marker::PhantomData;
type Result<T> = SklResult<T>;
type Float = f64;
#[derive(Debug, Clone)]
pub struct Untrained;
#[derive(Debug, Clone)]
pub struct Trained {
selected_features: Vec<usize>,
_feature_scores: Array1<Float>,
_centrality_scores: Option<HashMap<String, Array1<Float>>>,
_community_assignments: Option<Array1<usize>>,
_structural_scores: Option<Array1<Float>>,
n_features: usize,
}
#[derive(Debug, Clone)]
pub struct GraphFeatureSelector<State = Untrained> {
include_centrality: bool,
include_community: bool,
include_structural: bool,
centrality_threshold: Float,
centrality_types: Vec<String>,
community_method: String,
min_community_size: usize,
community_weight: Float,
structural_weight: Float,
k: Option<usize>,
damping_factor: Float,
max_iterations: usize,
tolerance: Float,
adjacency: Option<Array2<Float>>,
state: PhantomData<State>,
trained_state: Option<Trained>,
}
impl Default for GraphFeatureSelector<Untrained> {
fn default() -> Self {
Self::new()
}
}
impl GraphFeatureSelector<Untrained> {
pub fn new() -> Self {
Self {
include_centrality: true,
include_community: true,
include_structural: true,
centrality_threshold: 0.1,
centrality_types: vec!["degree".to_string(), "pagerank".to_string()],
community_method: "modularity".to_string(),
min_community_size: 2,
community_weight: 0.5,
structural_weight: 0.3,
k: None,
damping_factor: 0.85,
max_iterations: 100,
tolerance: 1e-6,
adjacency: None,
state: PhantomData,
trained_state: None,
}
}
pub fn builder() -> GraphFeatureSelectorBuilder {
GraphFeatureSelectorBuilder::new()
}
}
#[derive(Debug)]
pub struct GraphFeatureSelectorBuilder {
include_centrality: bool,
include_community: bool,
include_structural: bool,
centrality_threshold: Float,
centrality_types: Vec<String>,
community_method: String,
min_community_size: usize,
community_weight: Float,
structural_weight: Float,
k: Option<usize>,
damping_factor: Float,
max_iterations: usize,
tolerance: Float,
adjacency: Option<Array2<Float>>,
}
impl Default for GraphFeatureSelectorBuilder {
fn default() -> Self {
Self::new()
}
}
impl GraphFeatureSelectorBuilder {
pub fn new() -> Self {
Self {
include_centrality: true,
include_community: true,
include_structural: true,
centrality_threshold: 0.1,
centrality_types: vec!["degree".to_string(), "pagerank".to_string()],
community_method: "modularity".to_string(),
min_community_size: 2,
community_weight: 0.5,
structural_weight: 0.3,
k: None,
damping_factor: 0.85,
max_iterations: 100,
tolerance: 1e-6,
adjacency: None,
}
}
pub fn include_centrality(mut self, include: bool) -> Self {
self.include_centrality = include;
self
}
pub fn include_community(mut self, include: bool) -> Self {
self.include_community = include;
self
}
pub fn include_structural(mut self, include: bool) -> Self {
self.include_structural = include;
self
}
pub fn centrality_threshold(mut self, threshold: Float) -> Self {
self.centrality_threshold = threshold;
self
}
pub fn centrality_types(mut self, types: Vec<&str>) -> Self {
self.centrality_types = types.iter().map(|s| s.to_string()).collect();
self
}
pub fn community_method(mut self, method: &str) -> Self {
self.community_method = method.to_string();
self
}
pub fn min_community_size(mut self, size: usize) -> Self {
self.min_community_size = size;
self
}
pub fn community_weight(mut self, weight: Float) -> Self {
self.community_weight = weight;
self
}
pub fn structural_weight(mut self, weight: Float) -> Self {
self.structural_weight = weight;
self
}
pub fn k(mut self, k: usize) -> Self {
self.k = Some(k);
self
}
pub fn damping_factor(mut self, factor: Float) -> Self {
self.damping_factor = factor;
self
}
pub fn max_iterations(mut self, iterations: usize) -> Self {
self.max_iterations = iterations;
self
}
pub fn tolerance(mut self, tol: Float) -> Self {
self.tolerance = tol;
self
}
pub fn with_adjacency(mut self, adjacency: Array2<Float>) -> Self {
self.adjacency = Some(adjacency);
self
}
pub fn build(self) -> GraphFeatureSelector<Untrained> {
GraphFeatureSelector {
include_centrality: self.include_centrality,
include_community: self.include_community,
include_structural: self.include_structural,
centrality_threshold: self.centrality_threshold,
centrality_types: self.centrality_types,
community_method: self.community_method,
min_community_size: self.min_community_size,
community_weight: self.community_weight,
structural_weight: self.structural_weight,
k: self.k,
damping_factor: self.damping_factor,
max_iterations: self.max_iterations,
tolerance: self.tolerance,
adjacency: self.adjacency,
state: PhantomData,
trained_state: None,
}
}
}
impl Estimator for GraphFeatureSelector<Untrained> {
type Config = ();
type Error = sklears_core::error::SklearsError;
type Float = Float;
fn config(&self) -> &Self::Config {
&()
}
}
impl Estimator for GraphFeatureSelector<Trained> {
type Config = ();
type Error = sklears_core::error::SklearsError;
type Float = Float;
fn config(&self) -> &Self::Config {
&()
}
}
impl Fit<Array2<Float>, Array1<Float>> for GraphFeatureSelector<Untrained> {
type Fitted = GraphFeatureSelector<Trained>;
fn fit(self, x: &Array2<Float>, y: &Array1<Float>) -> Result<Self::Fitted> {
let (n_samples, n_features) = x.dim();
if y.len() != n_samples {
return Err(SklearsError::InvalidInput(
"Number of samples in X and y must match".to_string(),
));
}
let adjacency = self.adjacency.ok_or_else(|| {
SklearsError::InvalidInput(
"Adjacency matrix is required for graph feature selection".to_string(),
)
})?;
if adjacency.dim() != (n_samples, n_samples) {
return Err(SklearsError::InvalidInput(
"Adjacency matrix must be square with same number of nodes as samples".to_string(),
));
}
let mut centrality_scores = None;
let mut community_assignments = None;
let mut structural_scores = None;
let mut combined_scores = Array1::zeros(n_features);
if self.include_centrality {
let mut centrality_map = HashMap::new();
for centrality_type in &self.centrality_types {
let scores = match centrality_type.as_str() {
"degree" => compute_degree_centrality(&adjacency.view()),
"pagerank" => compute_pagerank_centrality(
&adjacency.view(),
self.damping_factor,
self.max_iterations,
self.tolerance,
),
"betweenness" => compute_betweenness_centrality(&adjacency.view()),
"closeness" => compute_closeness_centrality(&adjacency.view()),
_ => Array1::zeros(n_samples),
};
centrality_map.insert(centrality_type.clone(), scores);
}
let feature_centrality_scores = compute_feature_centrality_scores(x, ¢rality_map)?;
combined_scores = &combined_scores + &feature_centrality_scores;
centrality_scores = Some(centrality_map);
}
if self.include_community {
let communities = match self.community_method.as_str() {
"modularity" => {
detect_communities_modularity(&adjacency.view(), self.min_community_size)
}
"louvain" => detect_communities_louvain(&adjacency.view(), self.min_community_size),
_ => Array1::zeros(n_samples),
};
let community_feature_scores =
compute_community_feature_scores(x, &communities, self.community_weight)?;
combined_scores = &combined_scores + &community_feature_scores;
community_assignments = Some(communities);
}
if self.include_structural {
let struct_scores =
compute_structural_feature_scores(x, &adjacency.view(), self.structural_weight)?;
combined_scores = &combined_scores + &struct_scores;
structural_scores = Some(struct_scores);
}
let selected_features = if let Some(k) = self.k {
select_top_k_features(&combined_scores, k)
} else {
select_features_by_threshold(&combined_scores, self.centrality_threshold)
};
let trained_state = Trained {
selected_features,
_feature_scores: combined_scores,
_centrality_scores: centrality_scores,
_community_assignments: community_assignments,
_structural_scores: structural_scores,
n_features,
};
Ok(GraphFeatureSelector {
include_centrality: self.include_centrality,
include_community: self.include_community,
include_structural: self.include_structural,
centrality_threshold: self.centrality_threshold,
centrality_types: self.centrality_types,
community_method: self.community_method,
min_community_size: self.min_community_size,
community_weight: self.community_weight,
structural_weight: self.structural_weight,
k: self.k,
damping_factor: self.damping_factor,
max_iterations: self.max_iterations,
tolerance: self.tolerance,
adjacency: None,
state: PhantomData,
trained_state: Some(trained_state),
})
}
}
impl Transform<Array2<Float>> for GraphFeatureSelector<Trained> {
fn transform(&self, x: &Array2<Float>) -> Result<Array2<Float>> {
let trained = self.trained_state.as_ref().ok_or_else(|| {
SklearsError::InvalidState("Selector must be fitted before transforming".to_string())
})?;
let (_n_samples, n_features) = x.dim();
if n_features != trained.n_features {
return Err(SklearsError::InvalidInput(format!(
"Expected {} features, got {}",
trained.n_features, n_features
)));
}
if trained.selected_features.is_empty() {
return Err(SklearsError::InvalidState(
"No features were selected".to_string(),
));
}
let selected_data = x.select(Axis(1), &trained.selected_features);
Ok(selected_data)
}
}
impl SelectorMixin for GraphFeatureSelector<Trained> {
fn get_support(&self) -> Result<Array1<bool>> {
let trained = self.trained_state.as_ref().ok_or_else(|| {
SklearsError::InvalidState("Selector must be fitted before getting support".to_string())
})?;
let mut support = Array1::from_elem(trained.n_features, false);
for &idx in &trained.selected_features {
support[idx] = true;
}
Ok(support)
}
fn transform_features(&self, indices: &[usize]) -> SklResult<Vec<usize>> {
let trained = self.trained_state.as_ref().ok_or_else(|| {
SklearsError::InvalidState(
"Selector must be fitted before transforming features".to_string(),
)
})?;
let selected: Vec<usize> = indices
.iter()
.filter(|&&idx| trained.selected_features.contains(&idx))
.cloned()
.collect();
Ok(selected)
}
}
fn compute_degree_centrality(adjacency: &ArrayView2<Float>) -> Array1<Float> {
let n = adjacency.nrows();
let mut centrality = Array1::zeros(n);
for i in 0..n {
let degree: Float = adjacency.row(i).sum();
centrality[i] = degree / (n - 1) as Float;
}
centrality
}
fn compute_pagerank_centrality(
adjacency: &ArrayView2<Float>,
damping: Float,
max_iter: usize,
tolerance: Float,
) -> Array1<Float> {
let n = adjacency.nrows();
let mut pagerank = Array1::from_elem(n, 1.0 / n as Float);
let mut new_pagerank = Array1::zeros(n);
for _ in 0..max_iter {
new_pagerank.fill(0.0);
for i in 0..n {
let out_degree: Float = adjacency.row(i).sum();
if out_degree > 0.0 {
for j in 0..n {
if adjacency[[i, j]] > 0.0 {
new_pagerank[j] += damping * pagerank[i] / out_degree;
}
}
}
}
for i in 0..n {
new_pagerank[i] += (1.0 - damping) / n as Float;
}
let diff: Float = (&new_pagerank - &pagerank).mapv(|x| x.abs()).sum();
if diff < tolerance {
break;
}
pagerank.assign(&new_pagerank);
}
pagerank
}
fn compute_betweenness_centrality(adjacency: &ArrayView2<Float>) -> Array1<Float> {
let n = adjacency.nrows();
let mut betweenness = Array1::zeros(n);
for i in 0..n {
let mut local_betweenness = 0.0;
for j in 0..n {
if i != j {
for k in 0..n {
if k != i && k != j {
let direct_jk = if adjacency[[j, k]] > 0.0 {
1.0
} else {
f64::INFINITY
};
let via_i = if adjacency[[j, i]] > 0.0 && adjacency[[i, k]] > 0.0 {
2.0
} else {
f64::INFINITY
};
if via_i < direct_jk {
local_betweenness += 1.0;
}
}
}
}
}
betweenness[i] = local_betweenness / ((n - 1) * (n - 2)) as Float;
}
betweenness
}
fn compute_closeness_centrality(adjacency: &ArrayView2<Float>) -> Array1<Float> {
let n = adjacency.nrows();
let mut closeness = Array1::zeros(n);
for i in 0..n {
let mut total_distance = 0.0;
let mut reachable_nodes = 0;
for j in 0..n {
if i != j {
let distance = if adjacency[[i, j]] > 0.0 {
1.0
} else {
let mut found_path = false;
for k in 0..n {
if adjacency[[i, k]] > 0.0 && adjacency[[k, j]] > 0.0 {
found_path = true;
break;
}
}
if found_path {
2.0
} else {
f64::INFINITY
}
};
if distance.is_finite() {
total_distance += distance;
reachable_nodes += 1;
}
}
}
if reachable_nodes > 0 {
closeness[i] = reachable_nodes as Float / total_distance;
}
}
closeness
}
fn compute_feature_centrality_scores(
x: &Array2<Float>,
centrality_scores: &HashMap<String, Array1<Float>>,
) -> Result<Array1<Float>> {
let (_n_samples, n_features) = x.dim();
let mut feature_scores = Array1::zeros(n_features);
for j in 0..n_features {
let feature = x.column(j);
let mut total_score = 0.0;
let mut weight_sum = 0.0;
for (centrality_type, centrality) in centrality_scores {
let weight = match centrality_type.as_str() {
"degree" => 1.0,
"pagerank" => 1.5,
"betweenness" => 1.2,
"closeness" => 1.1,
_ => 1.0,
};
let correlation = compute_pearson_correlation(&feature, ¢rality.view());
total_score += weight * correlation.abs();
weight_sum += weight;
}
feature_scores[j] = if weight_sum > 0.0 {
total_score / weight_sum
} else {
0.0
};
}
Ok(feature_scores)
}
fn compute_community_feature_scores(
x: &Array2<Float>,
communities: &Array1<usize>,
weight: Float,
) -> Result<Array1<Float>> {
let (_n_samples, n_features) = x.dim();
let mut feature_scores = Array1::zeros(n_features);
let max_community = communities.iter().max().cloned().unwrap_or(0);
for j in 0..n_features {
let feature = x.column(j);
let mut community_variance = 0.0;
for c in 0..=max_community {
let community_indices: Vec<usize> = communities
.iter()
.enumerate()
.filter(|(_, &comm)| comm == c)
.map(|(i, _)| i)
.collect();
if community_indices.len() > 1 {
let community_values: Vec<Float> =
community_indices.iter().map(|&i| feature[i]).collect();
let mean = community_values.iter().sum::<Float>() / community_values.len() as Float;
let variance = community_values
.iter()
.map(|&val| (val - mean).powi(2))
.sum::<Float>()
/ community_values.len() as Float;
community_variance += variance;
}
}
feature_scores[j] = weight * community_variance;
}
Ok(feature_scores)
}
fn compute_structural_feature_scores(
x: &Array2<Float>,
adjacency: &ArrayView2<Float>,
weight: Float,
) -> Result<Array1<Float>> {
let (_n_samples, n_features) = x.dim();
let mut feature_scores = Array1::zeros(n_features);
let clustering_coeffs = compute_clustering_coefficients(adjacency);
for j in 0..n_features {
let feature = x.column(j);
let correlation = compute_pearson_correlation(&feature, &clustering_coeffs.view());
feature_scores[j] = weight * correlation.abs();
}
Ok(feature_scores)
}
fn detect_communities_modularity(adjacency: &ArrayView2<Float>, min_size: usize) -> Array1<usize> {
let n = adjacency.nrows();
let mut communities = Array1::from_iter(0..n);
let total_edges: Float = adjacency.sum() / 2.0;
if total_edges == 0.0 {
return communities;
}
let mut improved = true;
while improved {
improved = false;
for i in 0..n {
let current_community = communities[i];
let mut best_community = current_community;
let mut best_modularity_gain = 0.0;
for j in 0..n {
if i != j {
let target_community = communities[j];
if target_community != current_community {
let modularity_gain = compute_modularity_gain(
i,
current_community,
target_community,
adjacency,
&communities,
total_edges,
);
if modularity_gain > best_modularity_gain {
best_modularity_gain = modularity_gain;
best_community = target_community;
}
}
}
}
if best_community != current_community {
communities[i] = best_community;
improved = true;
}
}
}
let mut community_counts = HashMap::new();
for &comm in communities.iter() {
*community_counts.entry(comm).or_insert(0) += 1;
}
let small_communities: Vec<usize> = community_counts
.iter()
.filter(|(_, &count)| count < min_size)
.map(|(&comm, _)| comm)
.collect();
for &small_comm in &small_communities {
let nodes_in_small: Vec<usize> = communities
.iter()
.enumerate()
.filter(|(_, &comm)| comm == small_comm)
.map(|(i, _)| i)
.collect();
if !nodes_in_small.is_empty() {
let target_comm = find_best_merge_community(&nodes_in_small, adjacency, &communities);
for &node in &nodes_in_small {
communities[node] = target_comm;
}
}
}
communities
}
fn detect_communities_louvain(adjacency: &ArrayView2<Float>, min_size: usize) -> Array1<usize> {
detect_communities_modularity(adjacency, min_size)
}
fn compute_modularity_gain(
node: usize,
from_comm: usize,
to_comm: usize,
adjacency: &ArrayView2<Float>,
communities: &Array1<usize>,
total_edges: Float,
) -> Float {
if total_edges == 0.0 {
return 0.0;
}
let node_degree: Float = adjacency.row(node).sum();
let mut edges_to_from = 0.0;
let mut edges_to_to = 0.0;
for i in 0..adjacency.nrows() {
if communities[i] == from_comm && i != node {
edges_to_from += adjacency[[node, i]];
}
if communities[i] == to_comm {
edges_to_to += adjacency[[node, i]];
}
}
(edges_to_to - edges_to_from) / (2.0 * total_edges)
- node_degree * node_degree / (4.0 * total_edges * total_edges)
}
fn find_best_merge_community(
nodes: &[usize],
adjacency: &ArrayView2<Float>,
communities: &Array1<usize>,
) -> usize {
let mut community_connections = HashMap::new();
for &node in nodes {
for i in 0..adjacency.nrows() {
if adjacency[[node, i]] > 0.0 && !nodes.contains(&i) {
let comm = communities[i];
*community_connections.entry(comm).or_insert(0.0) += adjacency[[node, i]];
}
}
}
community_connections
.into_iter()
.max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))
.map(|(comm, _)| comm)
.unwrap_or(0)
}
fn compute_clustering_coefficients(adjacency: &ArrayView2<Float>) -> Array1<Float> {
let n = adjacency.nrows();
let mut clustering = Array1::zeros(n);
for i in 0..n {
let neighbors: Vec<usize> = (0..n)
.filter(|&j| i != j && adjacency[[i, j]] > 0.0)
.collect();
let degree = neighbors.len();
if degree < 2 {
clustering[i] = 0.0;
continue;
}
let mut triangles = 0;
for j in 0..neighbors.len() {
for k in (j + 1)..neighbors.len() {
if adjacency[[neighbors[j], neighbors[k]]] > 0.0 {
triangles += 1;
}
}
}
let possible_triangles = degree * (degree - 1) / 2;
clustering[i] = if possible_triangles > 0 {
triangles as Float / possible_triangles as Float
} else {
0.0
};
}
clustering
}
fn compute_pearson_correlation(x: &ArrayView1<Float>, y: &ArrayView1<Float>) -> Float {
let n = x.len();
if n != y.len() || n == 0 {
return 0.0;
}
let mean_x = x.sum() / n as Float;
let mean_y = y.sum() / n as Float;
let mut numerator = 0.0;
let mut sum_sq_x = 0.0;
let mut sum_sq_y = 0.0;
for i in 0..n {
let dx = x[i] - mean_x;
let dy = y[i] - mean_y;
numerator += dx * dy;
sum_sq_x += dx * dx;
sum_sq_y += dy * dy;
}
let denominator = (sum_sq_x * sum_sq_y).sqrt();
if denominator == 0.0 {
0.0
} else {
numerator / denominator
}
}
fn select_top_k_features(scores: &Array1<Float>, k: usize) -> Vec<usize> {
let mut indexed_scores: Vec<(usize, Float)> = scores
.iter()
.enumerate()
.map(|(i, &score)| (i, score))
.collect();
indexed_scores.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
indexed_scores
.into_iter()
.take(k.min(scores.len()))
.map(|(i, _)| i)
.collect()
}
fn select_features_by_threshold(scores: &Array1<Float>, threshold: Float) -> Vec<usize> {
scores
.iter()
.enumerate()
.filter(|(_, &score)| score >= threshold)
.map(|(i, _)| i)
.collect()
}
#[allow(non_snake_case)]
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::Array2;
#[test]
fn test_graph_feature_selector_creation() {
let selector = GraphFeatureSelector::new();
assert!(selector.include_centrality);
assert!(selector.include_community);
assert!(selector.include_structural);
}
#[test]
fn test_graph_feature_selector_builder() {
let selector = GraphFeatureSelector::builder()
.include_centrality(true)
.include_community(false)
.centrality_threshold(0.5)
.k(3)
.build();
assert!(selector.include_centrality);
assert!(!selector.include_community);
assert_eq!(selector.centrality_threshold, 0.5);
assert_eq!(selector.k, Some(3));
}
#[test]
fn test_degree_centrality() {
let adjacency =
Array2::from_shape_vec((3, 3), vec![0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0])
.expect("operation should succeed");
let centrality = compute_degree_centrality(&adjacency.view());
assert_eq!(centrality.len(), 3);
for &c in centrality.iter() {
assert!((c - 1.0).abs() < 1e-6);
}
}
#[test]
fn test_fit_transform_basic() {
let adjacency = Array2::from_shape_vec(
(4, 4),
vec![
0.0, 1.0, 1.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0,
],
)
.expect("operation should succeed");
let features = Array2::from_shape_vec(
(4, 3),
vec![1.0, 2.0, 3.0, 2.0, 3.0, 1.0, 3.0, 1.0, 4.0, 1.0, 4.0, 2.0],
)
.expect("operation should succeed");
let target = Array1::from_vec(vec![0.0, 1.0, 1.0, 0.0]);
let selector = GraphFeatureSelector::builder()
.k(2)
.with_adjacency(adjacency)
.build();
let trained = selector
.fit(&features, &target)
.expect("operation should succeed");
let transformed = trained
.transform(&features)
.expect("operation should succeed");
assert_eq!(transformed.ncols(), 2);
assert_eq!(transformed.nrows(), 4);
}
#[test]
fn test_get_support() {
let adjacency =
Array2::from_shape_vec((3, 3), vec![0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0])
.expect("operation should succeed");
let features = Array2::from_shape_vec(
(3, 4),
vec![1.0, 2.0, 3.0, 4.0, 2.0, 3.0, 1.0, 5.0, 3.0, 1.0, 4.0, 2.0],
)
.expect("operation should succeed");
let target = Array1::from_vec(vec![0.0, 1.0, 1.0]);
let selector = GraphFeatureSelector::builder()
.k(2)
.with_adjacency(adjacency)
.build();
let trained = selector
.fit(&features, &target)
.expect("operation should succeed");
let support = trained.get_support().expect("operation should succeed");
assert_eq!(support.len(), 4);
assert_eq!(support.iter().filter(|&&x| x).count(), 2);
}
#[test]
fn test_clustering_coefficients() {
let adjacency = Array2::from_shape_vec(
(4, 4),
vec![
0.0, 1.0, 1.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0,
],
)
.expect("operation should succeed");
let clustering = compute_clustering_coefficients(&adjacency.view());
assert_eq!(clustering.len(), 4);
assert!((clustering[1] - 2.0 / 3.0).abs() < 1e-6);
}
#[test]
fn test_pagerank_centrality() {
let adjacency =
Array2::from_shape_vec((3, 3), vec![0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0])
.expect("operation should succeed");
let pagerank = compute_pagerank_centrality(&adjacency.view(), 0.85, 100, 1e-6);
assert_eq!(pagerank.len(), 3);
assert!(pagerank[0] > pagerank[1]);
assert!(pagerank[0] > pagerank[2]);
}
}