use scirs2_core::ndarray::{s, Array1, Array2, Axis};
use scirs2_linalg::compat::{ArrayLinalgExt, UPLO};
use sklears_core::error::{Result as SklResult, SklearsError};
use std::hash::{Hash, Hasher};
pub fn commute_time_distance(adjacency: &Array2<f64>) -> SklResult<Array2<f64>> {
let n_nodes = adjacency.nrows();
let degrees: Array1<f64> = adjacency.sum_axis(Axis(1));
let mut laplacian = -adjacency.clone();
for i in 0..n_nodes {
laplacian[(i, i)] += degrees[i];
}
let laplacian_pinv = compute_pseudoinverse(&laplacian)?;
let vol_g: f64 = degrees.sum();
let mut commute_times = Array2::zeros((n_nodes, n_nodes));
for i in 0..n_nodes {
for j in 0..n_nodes {
if i != j {
let ct = vol_g
* (laplacian_pinv[(i, i)] + laplacian_pinv[(j, j)]
- 2.0 * laplacian_pinv[(i, j)]);
commute_times[(i, j)] = ct.max(0.0); }
}
}
Ok(commute_times)
}
pub fn resistance_distance(adjacency: &Array2<f64>) -> SklResult<Array2<f64>> {
let n_nodes = adjacency.nrows();
let degrees: Array1<f64> = adjacency.sum_axis(Axis(1));
let mut laplacian = -adjacency.clone();
for i in 0..n_nodes {
laplacian[(i, i)] += degrees[i];
}
let laplacian_pinv = compute_pseudoinverse(&laplacian)?;
let mut resistance_distances = Array2::zeros((n_nodes, n_nodes));
for i in 0..n_nodes {
for j in 0..n_nodes {
if i != j {
let rd =
laplacian_pinv[(i, i)] + laplacian_pinv[(j, j)] - 2.0 * laplacian_pinv[(i, j)];
resistance_distances[(i, j)] = rd.max(0.0); }
}
}
Ok(resistance_distances)
}
pub fn procrustes_distance(x1: &Array2<f64>, x2: &Array2<f64>) -> SklResult<f64> {
if x1.dim() != x2.dim() {
return Err(SklearsError::InvalidInput(
"Input matrices must have the same dimensions".to_string(),
));
}
let (_n, _m) = x1.dim();
let mean1 = x1.mean_axis(Axis(0)).expect("operation should succeed");
let mean2 = x2.mean_axis(Axis(0)).expect("operation should succeed");
let x1_centered = x1 - &mean1;
let x2_centered = x2 - &mean2;
let norm1 = x1_centered.mapv(|x| x * x).sum().sqrt();
let norm2 = x2_centered.mapv(|x| x * x).sum().sqrt();
if norm1 < 1e-10 || norm2 < 1e-10 {
return Ok(0.0);
}
let x1_scaled = &x1_centered / norm1;
let x2_scaled = &x2_centered / norm2;
let h = x1_scaled.t().dot(&x2_scaled);
let (u, _s, vt) = h
.svd(true)
.map_err(|e| SklearsError::NumericalError(format!("SVD failed: {:?}", e)))?;
let r = vt.t().dot(&u.t());
let x1_rotated = x1_scaled.dot(&r);
let diff = &x1_rotated - &x2_scaled;
let distance = diff.mapv(|x| x * x).sum().sqrt();
Ok(distance)
}
pub fn geodesic_kernel(
x: &Array2<f64>,
geodesic_distances: &Array2<f64>,
gamma: f64,
) -> SklResult<Array2<f64>> {
let (n_samples, _) = x.dim();
let mut kernel = Array2::zeros((n_samples, n_samples));
for i in 0..n_samples {
for j in 0..n_samples {
let geo_dist = geodesic_distances[(i, j)];
kernel[(i, j)] = (-gamma * geo_dist.powi(2)).exp();
}
}
Ok(kernel)
}
pub fn heat_kernel(laplacian: &Array2<f64>, t: f64) -> SklResult<Array2<f64>> {
let (eigenvalues, eigenvectors) = laplacian
.eigh(UPLO::Lower)
.map_err(|e| SklearsError::NumericalError(format!("Eigendecomposition failed: {:?}", e)))?;
let exp_eigenvalues: Array1<f64> = eigenvalues.mapv(|λ| (-t * λ).exp());
let exp_diag = Array2::from_diag(&exp_eigenvalues);
let heat_kernel = eigenvectors.dot(&exp_diag).dot(&eigenvectors.t());
Ok(heat_kernel)
}
pub fn local_tangent_space_kernel(
x: &Array2<f64>,
k_neighbors: usize,
gamma: f64,
) -> SklResult<Array2<f64>> {
let (n_samples, n_features) = x.dim();
let mut kernel = Array2::zeros((n_samples, n_samples));
for i in 0..n_samples {
let mut distances: Vec<(usize, f64)> = Vec::new();
for j in 0..n_samples {
if i != j {
let dist = x
.row(i)
.iter()
.zip(x.row(j).iter())
.map(|(a, b)| (a - b).powi(2))
.sum::<f64>()
.sqrt();
distances.push((j, dist));
}
}
distances.sort_by(|a, b| a.1.partial_cmp(&b.1).expect("operation should succeed"));
let neighbors: Vec<usize> = distances
.iter()
.take(k_neighbors)
.map(|(idx, _)| *idx)
.collect();
if neighbors.len() >= 2 {
let mut neighbor_matrix = Array2::zeros((neighbors.len(), n_features));
for (k, &neighbor_idx) in neighbors.iter().enumerate() {
for j in 0..n_features {
neighbor_matrix[(k, j)] = x[(neighbor_idx, j)] - x[(i, j)];
}
}
if let Ok((u, _s, _)) = neighbor_matrix.svd(true) {
for j in 0..n_samples {
let diff = &x.row(j) - &x.row(i);
let projection_norm = u.t().dot(&diff).mapv(|x| x * x).sum();
let tangent_dist = diff.mapv(|x| x * x).sum() - projection_norm;
kernel[(i, j)] = (-gamma * tangent_dist).exp();
}
}
}
}
Ok(kernel)
}
pub fn adaptive_diffusion_kernel(
x: &Array2<f64>,
k_neighbors: usize,
alpha: f64,
) -> SklResult<Array2<f64>> {
let (n_samples, _) = x.dim();
let mut local_densities = Array1::zeros(n_samples);
for i in 0..n_samples {
let mut distances: Vec<f64> = Vec::new();
for j in 0..n_samples {
if i != j {
let dist = x
.row(i)
.iter()
.zip(x.row(j).iter())
.map(|(a, b)| (a - b).powi(2))
.sum::<f64>()
.sqrt();
distances.push(dist);
}
}
distances.sort_by(|a, b| a.partial_cmp(b).expect("operation should succeed"));
if distances.len() >= k_neighbors {
local_densities[i] = distances[k_neighbors - 1];
}
}
let mut kernel = Array2::zeros((n_samples, n_samples));
for i in 0..n_samples {
for j in 0..n_samples {
let dist = x
.row(i)
.iter()
.zip(x.row(j).iter())
.map(|(a, b)| (a - b).powi(2))
.sum::<f64>()
.sqrt();
let adaptive_bandwidth =
(local_densities[i].powf(alpha) * local_densities[j].powf(alpha)).sqrt();
if adaptive_bandwidth > 1e-10 {
kernel[(i, j)] = (-dist.powi(2) / (2.0 * adaptive_bandwidth.powi(2))).exp();
}
}
}
Ok(kernel)
}
pub fn manifold_distance_kernel(
x: &Array2<f64>,
euclidean_weight: f64,
geodesic_weight: f64,
curvature_weight: f64,
gamma: f64,
) -> SklResult<Array2<f64>> {
let (n_samples, _) = x.dim();
let mut euclidean_dists = Array2::zeros((n_samples, n_samples));
for i in 0..n_samples {
for j in 0..n_samples {
euclidean_dists[(i, j)] = x
.row(i)
.iter()
.zip(x.row(j).iter())
.map(|(a, b)| (a - b).powi(2))
.sum::<f64>()
.sqrt();
}
}
let k = 10; let mut adjacency = Array2::zeros((n_samples, n_samples));
for i in 0..n_samples {
let mut distances: Vec<(usize, f64)> = Vec::new();
for j in 0..n_samples {
if i != j {
distances.push((j, euclidean_dists[(i, j)]));
}
}
distances.sort_by(|a, b| a.1.partial_cmp(&b.1).expect("operation should succeed"));
for &(j, dist) in distances.iter().take(k) {
adjacency[(i, j)] = dist;
adjacency[(j, i)] = dist;
}
}
let mut geodesic_dists = adjacency.clone();
for i in 0..n_samples {
for j in 0..n_samples {
if i != j && geodesic_dists[(i, j)] == 0.0 {
geodesic_dists[(i, j)] = f64::INFINITY;
}
}
}
for k in 0..n_samples {
for i in 0..n_samples {
for j in 0..n_samples {
if geodesic_dists[(i, k)] + geodesic_dists[(k, j)] < geodesic_dists[(i, j)] {
geodesic_dists[(i, j)] = geodesic_dists[(i, k)] + geodesic_dists[(k, j)];
}
}
}
}
let mut curvature_dists = Array2::zeros((n_samples, n_samples));
for i in 0..n_samples {
for j in 0..n_samples {
if i != j {
let euclidean = euclidean_dists[(i, j)];
let geodesic = if geodesic_dists[(i, j)].is_finite() {
geodesic_dists[(i, j)]
} else {
euclidean * 2.0
};
curvature_dists[(i, j)] = (geodesic - euclidean).abs();
}
}
}
let mut combined_dists = Array2::zeros((n_samples, n_samples));
for i in 0..n_samples {
for j in 0..n_samples {
combined_dists[(i, j)] = euclidean_weight * euclidean_dists[(i, j)]
+ geodesic_weight * geodesic_dists[(i, j)]
+ curvature_weight * curvature_dists[(i, j)];
}
}
let mut kernel = Array2::zeros((n_samples, n_samples));
for i in 0..n_samples {
for j in 0..n_samples {
kernel[(i, j)] = (-gamma * combined_dists[(i, j)].powi(2)).exp();
}
}
Ok(kernel)
}
pub fn spectral_kernel(x: &Array2<f64>, n_components: usize, gamma: f64) -> SklResult<Array2<f64>> {
let (n_samples, _) = x.dim();
let mut affinity = Array2::zeros((n_samples, n_samples));
for i in 0..n_samples {
for j in 0..n_samples {
if i != j {
let dist = x
.row(i)
.iter()
.zip(x.row(j).iter())
.map(|(a, b)| (a - b).powi(2))
.sum::<f64>()
.sqrt();
affinity[(i, j)] = (-dist.powi(2) / 2.0).exp();
}
}
}
let degrees: Array1<f64> = affinity.sum_axis(Axis(1));
let mut laplacian = Array2::eye(n_samples);
for i in 0..n_samples {
for j in 0..n_samples {
if i != j && affinity[(i, j)] > 0.0 {
let normalization = (degrees[i] * degrees[j]).sqrt();
if normalization > 1e-10 {
laplacian[(i, j)] = -affinity[(i, j)] / normalization;
}
}
}
}
if let Ok((_eigenvalues, eigenvectors)) = laplacian.eigh(UPLO::Lower) {
let features = eigenvectors.slice(s![.., 1..=n_components.min(n_samples - 1)]);
let mut kernel = Array2::zeros((n_samples, n_samples));
for i in 0..n_samples {
for j in 0..n_samples {
let mut dist_sq = 0.0;
for k in 0..features.ncols() {
let diff = features[(i, k)] - features[(j, k)];
dist_sq += diff * diff;
}
kernel[(i, j)] = (-gamma * dist_sq).exp();
}
}
Ok(kernel)
} else {
Err(SklearsError::NumericalError(
"Eigendecomposition failed".to_string(),
))
}
}
fn compute_pseudoinverse(matrix: &Array2<f64>) -> SklResult<Array2<f64>> {
let (u, s, vt) = matrix
.svd(true)
.map_err(|e| SklearsError::NumericalError(format!("SVD failed: {:?}", e)))?;
let tolerance = 1e-10;
let s_inv: Array1<f64> = s.mapv(|x| if x > tolerance { 1.0 / x } else { 0.0 });
let s_inv_diag = Array2::from_diag(&s_inv);
let pinv = vt.t().dot(&s_inv_diag).dot(&u.t());
Ok(pinv)
}
pub fn random_walk_kernel(
adjacency1: &Array2<f64>,
adjacency2: &Array2<f64>,
walk_length: usize,
lambda: f64,
) -> SklResult<f64> {
let n1 = adjacency1.nrows();
let n2 = adjacency2.nrows();
let mut trans1 = adjacency1.clone();
let mut trans2 = adjacency2.clone();
for i in 0..n1 {
let row_sum = trans1.row(i).sum();
if row_sum > 1e-10 {
for j in 0..n1 {
trans1[(i, j)] /= row_sum;
}
}
}
for i in 0..n2 {
let row_sum = trans2.row(i).sum();
if row_sum > 1e-10 {
for j in 0..n2 {
trans2[(i, j)] /= row_sum;
}
}
}
let mut kernel_sum = 0.0;
let mut current_prob = Array2::from_elem((n1, n2), 1.0 / (n1 * n2) as f64);
for step in 0..walk_length {
let step_contribution: f64 = current_prob.sum() * lambda.powi(step as i32);
kernel_sum += step_contribution;
let mut next_prob = Array2::zeros((n1, n2));
for i in 0..n1 {
for j in 0..n2 {
for ii in 0..n1 {
for jj in 0..n2 {
next_prob[(ii, jj)] +=
current_prob[(i, j)] * trans1[(i, ii)] * trans2[(j, jj)];
}
}
}
}
current_prob = next_prob;
}
Ok(kernel_sum)
}
pub fn shortest_path_kernel(adjacency1: &Array2<f64>, adjacency2: &Array2<f64>) -> SklResult<f64> {
let _n1 = adjacency1.nrows();
let _n2 = adjacency2.nrows();
let sp1 = floyd_warshall_shortest_paths(adjacency1)?;
let sp2 = floyd_warshall_shortest_paths(adjacency2)?;
let hist1 = compute_path_histogram(&sp1);
let hist2 = compute_path_histogram(&sp2);
let norm1 = hist1.mapv(|x| x * x).sum().sqrt();
let norm2 = hist2.mapv(|x| x * x).sum().sqrt();
if norm1 < 1e-10 || norm2 < 1e-10 {
return Ok(0.0);
}
let normalized_hist1 = &hist1 / norm1;
let normalized_hist2 = &hist2 / norm2;
let kernel_value = normalized_hist1
.iter()
.zip(normalized_hist2.iter())
.map(|(a, b)| a * b)
.sum();
Ok(kernel_value)
}
pub fn graph_laplacian_kernel(
adjacency1: &Array2<f64>,
adjacency2: &Array2<f64>,
n_eigenvalues: usize,
) -> SklResult<f64> {
let laplacian1 = compute_normalized_laplacian(adjacency1)?;
let laplacian2 = compute_normalized_laplacian(adjacency2)?;
let (eigenvals1, _) = laplacian1
.eigh(UPLO::Lower)
.map_err(|e| SklearsError::NumericalError(format!("Eigendecomposition failed: {:?}", e)))?;
let (eigenvals2, _) = laplacian2
.eigh(UPLO::Lower)
.map_err(|e| SklearsError::NumericalError(format!("Eigendecomposition failed: {:?}", e)))?;
let n_vals = n_eigenvalues.min(eigenvals1.len()).min(eigenvals2.len());
let mut kernel_value = 0.0;
for i in 0..n_vals {
let diff = eigenvals1[i] - eigenvals2[i];
kernel_value += (-diff * diff).exp();
}
Ok(kernel_value)
}
pub fn weisfeiler_lehman_kernel(
adjacency1: &Array2<f64>,
adjacency2: &Array2<f64>,
iterations: usize,
) -> SklResult<f64> {
let n1 = adjacency1.nrows();
let n2 = adjacency2.nrows();
let mut labels1: Vec<usize> = Vec::new();
let mut labels2: Vec<usize> = Vec::new();
for i in 0..n1 {
let degree = adjacency1.row(i).iter().filter(|&&x| x > 0.0).count();
labels1.push(degree);
}
for i in 0..n2 {
let degree = adjacency2.row(i).iter().filter(|&&x| x > 0.0).count();
labels2.push(degree);
}
let mut total_kernel = 0.0;
for _iter in 0..iterations {
let hist1 = compute_label_histogram(&labels1);
let hist2 = compute_label_histogram(&labels2);
total_kernel += compute_histogram_intersection(&hist1, &hist2);
labels1 = update_wl_labels(&labels1, adjacency1);
labels2 = update_wl_labels(&labels2, adjacency2);
}
Ok(total_kernel)
}
pub fn graphlet_kernel(adjacency1: &Array2<f64>, adjacency2: &Array2<f64>) -> SklResult<f64> {
let graphlets1 = count_graphlets(adjacency1)?;
let graphlets2 = count_graphlets(adjacency2)?;
let norm1 = graphlets1.mapv(|x| x * x).sum().sqrt();
let norm2 = graphlets2.mapv(|x| x * x).sum().sqrt();
if norm1 < 1e-10 || norm2 < 1e-10 {
return Ok(0.0);
}
let kernel_value = graphlets1
.iter()
.zip(graphlets2.iter())
.map(|(a, b)| a * b)
.sum::<f64>()
/ (norm1 * norm2);
Ok(kernel_value)
}
pub enum GraphKernelType {
RandomWalk { walk_length: usize, lambda: f64 },
ShortestPath,
Laplacian { n_eigenvalues: usize },
WeisfeilerLehman { iterations: usize },
Graphlet,
}
pub fn compute_graph_kernel_matrix(
graphs: &[Array2<f64>],
kernel_type: GraphKernelType,
) -> SklResult<Array2<f64>> {
let n_graphs = graphs.len();
let mut kernel_matrix = Array2::zeros((n_graphs, n_graphs));
for i in 0..n_graphs {
for j in i..n_graphs {
let kernel_value = match kernel_type {
GraphKernelType::RandomWalk {
walk_length,
lambda,
} => random_walk_kernel(&graphs[i], &graphs[j], walk_length, lambda)?,
GraphKernelType::ShortestPath => shortest_path_kernel(&graphs[i], &graphs[j])?,
GraphKernelType::Laplacian { n_eigenvalues } => {
graph_laplacian_kernel(&graphs[i], &graphs[j], n_eigenvalues)?
}
GraphKernelType::WeisfeilerLehman { iterations } => {
weisfeiler_lehman_kernel(&graphs[i], &graphs[j], iterations)?
}
GraphKernelType::Graphlet => graphlet_kernel(&graphs[i], &graphs[j])?,
};
kernel_matrix[(i, j)] = kernel_value;
kernel_matrix[(j, i)] = kernel_value; }
}
Ok(kernel_matrix)
}
fn floyd_warshall_shortest_paths(adjacency: &Array2<f64>) -> SklResult<Array2<f64>> {
let n = adjacency.nrows();
let mut distances = Array2::from_elem((n, n), f64::INFINITY);
for i in 0..n {
distances[(i, i)] = 0.0;
for j in 0..n {
if adjacency[(i, j)] > 0.0 {
distances[(i, j)] = 1.0; }
}
}
for k in 0..n {
for i in 0..n {
for j in 0..n {
if distances[(i, k)] + distances[(k, j)] < distances[(i, j)] {
distances[(i, j)] = distances[(i, k)] + distances[(k, j)];
}
}
}
}
Ok(distances)
}
fn compute_path_histogram(shortest_paths: &Array2<f64>) -> Array1<f64> {
let mut histogram = Array1::zeros(20);
for &distance in shortest_paths.iter() {
if distance.is_finite() && distance > 0.0 {
let bin = (distance as usize).min(19);
histogram[bin] += 1.0;
}
}
histogram
}
fn compute_normalized_laplacian(adjacency: &Array2<f64>) -> SklResult<Array2<f64>> {
let n = adjacency.nrows();
let degrees: Array1<f64> = adjacency.sum_axis(Axis(1));
let mut laplacian = Array2::eye(n);
for i in 0..n {
for j in 0..n {
if i != j && adjacency[(i, j)] > 0.0 {
let normalization = (degrees[i] * degrees[j]).sqrt();
if normalization > 1e-10 {
laplacian[(i, j)] = -adjacency[(i, j)] / normalization;
}
}
}
}
Ok(laplacian)
}
fn compute_label_histogram(labels: &[usize]) -> std::collections::HashMap<usize, usize> {
let mut histogram = std::collections::HashMap::new();
for &label in labels {
*histogram.entry(label).or_insert(0) += 1;
}
histogram
}
fn compute_histogram_intersection(
hist1: &std::collections::HashMap<usize, usize>,
hist2: &std::collections::HashMap<usize, usize>,
) -> f64 {
let mut intersection = 0.0;
for (&label, &count1) in hist1 {
if let Some(&count2) = hist2.get(&label) {
intersection += (count1.min(count2)) as f64;
}
}
intersection
}
fn update_wl_labels(labels: &[usize], adjacency: &Array2<f64>) -> Vec<usize> {
let n = labels.len();
let mut new_labels = Vec::new();
for i in 0..n {
let mut neighbor_labels = Vec::new();
for j in 0..n {
if adjacency[(i, j)] > 0.0 {
neighbor_labels.push(labels[j]);
}
}
neighbor_labels.sort_unstable();
let mut hasher = std::collections::hash_map::DefaultHasher::new();
labels[i].hash(&mut hasher);
neighbor_labels.hash(&mut hasher);
new_labels.push(hasher.finish() as usize);
}
new_labels
}
fn count_graphlets(adjacency: &Array2<f64>) -> SklResult<Array1<f64>> {
let n = adjacency.nrows();
let mut graphlet_counts = Array1::zeros(4);
let mut edge_count = 0.0;
for i in 0..n {
for j in i + 1..n {
if adjacency[(i, j)] > 0.0 {
edge_count += 1.0;
}
}
}
graphlet_counts[0] = edge_count;
let mut triangle_count = 0.0;
for i in 0..n {
for j in i + 1..n {
for k in j + 1..n {
if adjacency[(i, j)] > 0.0 && adjacency[(j, k)] > 0.0 && adjacency[(i, k)] > 0.0 {
triangle_count += 1.0;
}
}
}
}
graphlet_counts[1] = triangle_count;
let mut star_count = 0.0;
for i in 0..n {
let degree = adjacency.row(i).iter().filter(|&&x| x > 0.0).count();
if degree >= 3 {
star_count += (degree * (degree - 1) * (degree - 2)) as f64 / 6.0;
}
}
graphlet_counts[2] = star_count;
let mut clique4_count = 0.0;
for i in 0..n {
for j in i + 1..n {
for k in j + 1..n {
for l in k + 1..n {
if adjacency[(i, j)] > 0.0
&& adjacency[(i, k)] > 0.0
&& adjacency[(i, l)] > 0.0
&& adjacency[(j, k)] > 0.0
&& adjacency[(j, l)] > 0.0
&& adjacency[(k, l)] > 0.0
{
clique4_count += 1.0;
}
}
}
}
}
graphlet_counts[3] = clique4_count;
Ok(graphlet_counts)
}