use scirs2_core::ndarray_ext::{Array1, Array2, ArrayView1, ArrayView2};
use sklears_core::error::{Result as SklResult, SklearsError};
use sklears_core::traits::{Estimator, Fit, Predict, Untrained};
use sklears_core::types::Float;
#[derive(Debug, Clone)]
pub struct WassersteinSemiSupervised<S = Untrained> {
state: S,
regularization: f64,
max_iter: usize,
tol: f64,
transport_weight: f64,
supervised_weight: f64,
random_state: Option<u64>,
}
impl WassersteinSemiSupervised<Untrained> {
pub fn new() -> Self {
Self {
state: Untrained,
regularization: 0.1,
max_iter: 100,
tol: 1e-6,
transport_weight: 1.0,
supervised_weight: 1.0,
random_state: None,
}
}
pub fn regularization(mut self, regularization: f64) -> Self {
self.regularization = regularization;
self
}
pub fn max_iter(mut self, max_iter: usize) -> Self {
self.max_iter = max_iter;
self
}
pub fn tol(mut self, tol: f64) -> Self {
self.tol = tol;
self
}
pub fn transport_weight(mut self, weight: f64) -> Self {
self.transport_weight = weight;
self
}
pub fn supervised_weight(mut self, weight: f64) -> Self {
self.supervised_weight = weight;
self
}
pub fn random_state(mut self, random_state: u64) -> Self {
self.random_state = Some(random_state);
self
}
}
impl Default for WassersteinSemiSupervised<Untrained> {
fn default() -> Self {
Self::new()
}
}
impl Estimator for WassersteinSemiSupervised<Untrained> {
type Config = ();
type Error = SklearsError;
type Float = Float;
fn config(&self) -> &Self::Config {
&()
}
}
impl Fit<ArrayView2<'_, Float>, ArrayView1<'_, i32>> for WassersteinSemiSupervised<Untrained> {
type Fitted = WassersteinSemiSupervised<WassersteinTrained>;
#[allow(non_snake_case)]
fn fit(self, X: &ArrayView2<'_, Float>, y: &ArrayView1<'_, i32>) -> SklResult<Self::Fitted> {
let X = X.to_owned();
let y = y.to_owned();
let (n_samples, _n_features) = X.dim();
let mut labeled_indices = Vec::new();
let mut unlabeled_indices = Vec::new();
let mut classes = std::collections::HashSet::new();
for (i, &label) in y.iter().enumerate() {
if label == -1 {
unlabeled_indices.push(i);
} else {
labeled_indices.push(i);
classes.insert(label);
}
}
if labeled_indices.is_empty() {
return Err(SklearsError::InvalidInput(
"No labeled samples provided".to_string(),
));
}
let classes: Vec<i32> = classes.into_iter().collect();
let n_classes = classes.len();
let cost_matrix = self.compute_cost_matrix(&X)?;
let mut label_distributions = Array2::<f64>::zeros((n_samples, n_classes));
for &idx in &labeled_indices {
if let Some(class_idx) = classes.iter().position(|&c| c == y[idx]) {
label_distributions[[idx, class_idx]] = 1.0;
}
}
for &idx in &unlabeled_indices {
for class_idx in 0..n_classes {
label_distributions[[idx, class_idx]] = 1.0 / n_classes as f64;
}
}
for _iter in 0..self.max_iter {
let prev_distributions = label_distributions.clone();
for &unlabeled_idx in &unlabeled_indices {
let mut new_distribution = Array1::<f64>::zeros(n_classes);
for class_idx in 0..n_classes {
let mut transport_sum = 0.0;
let mut weight_sum = 0.0;
for &labeled_idx in &labeled_indices {
if y[labeled_idx] == classes[class_idx] {
let transport_cost = cost_matrix[[unlabeled_idx, labeled_idx]];
let weight = (-transport_cost / self.regularization).exp();
transport_sum += weight;
weight_sum += weight;
}
}
if weight_sum > 0.0 {
new_distribution[class_idx] = transport_sum / weight_sum;
} else {
new_distribution[class_idx] = 1.0 / n_classes as f64;
}
}
let sum = new_distribution.sum();
if sum > 0.0 {
new_distribution /= sum;
}
for class_idx in 0..n_classes {
label_distributions[[unlabeled_idx, class_idx]] = new_distribution[class_idx];
}
}
let diff = (&label_distributions - &prev_distributions)
.mapv(|x| x.abs())
.sum();
if diff < self.tol {
break;
}
}
let transport_plan = self.sinkhorn_algorithm(
&cost_matrix,
&label_distributions,
&labeled_indices,
&classes,
)?;
let mut final_labels = y.clone();
for &idx in &unlabeled_indices {
let class_idx = label_distributions
.row(idx)
.iter()
.enumerate()
.max_by(|a, b| a.1.partial_cmp(b.1).expect("operation should succeed"))
.expect("operation should succeed")
.0;
final_labels[idx] = classes[class_idx];
}
Ok(WassersteinSemiSupervised {
state: WassersteinTrained {
X_train: X,
y_train: final_labels,
classes: Array1::from(classes),
cost_matrix,
transport_plan,
label_distributions,
},
regularization: self.regularization,
max_iter: self.max_iter,
tol: self.tol,
transport_weight: self.transport_weight,
supervised_weight: self.supervised_weight,
random_state: self.random_state,
})
}
}
impl WassersteinSemiSupervised<Untrained> {
fn compute_cost_matrix(&self, X: &Array2<f64>) -> SklResult<Array2<f64>> {
let n_samples = X.nrows();
let mut cost_matrix = Array2::<f64>::zeros((n_samples, n_samples));
for i in 0..n_samples {
for j in 0..n_samples {
let diff = &X.row(i) - &X.row(j);
let dist = diff.mapv(|x| x * x).sum().sqrt();
cost_matrix[[i, j]] = dist;
}
}
Ok(cost_matrix)
}
fn sinkhorn_algorithm(
&self,
cost_matrix: &Array2<f64>,
label_distributions: &Array2<f64>,
labeled_indices: &[usize],
classes: &[i32],
) -> SklResult<Array2<f64>> {
let n_samples = cost_matrix.nrows();
let mut transport_plan = Array2::<f64>::zeros((n_samples, n_samples));
for i in 0..n_samples {
for j in 0..n_samples {
transport_plan[[i, j]] = (-cost_matrix[[i, j]] / self.regularization).exp();
}
}
let mut u = Array1::<f64>::ones(n_samples);
let mut v = Array1::<f64>::ones(n_samples);
let mut source_marginal = Array1::<f64>::zeros(n_samples);
let mut target_marginal = Array1::<f64>::zeros(n_samples);
for i in 0..n_samples {
source_marginal[i] = 1.0 / n_samples as f64; target_marginal[i] = label_distributions.row(i).sum() / classes.len() as f64;
}
let source_sum = source_marginal.sum();
if source_sum > 0.0 {
source_marginal /= source_sum;
}
let target_sum = target_marginal.sum();
if target_sum > 0.0 {
target_marginal /= target_sum;
}
for _iter in 0..self.max_iter {
for i in 0..n_samples {
let sum: f64 = (0..n_samples).map(|j| transport_plan[[i, j]] * v[j]).sum();
if sum > 0.0 {
u[i] = source_marginal[i] / sum;
}
}
for j in 0..n_samples {
let sum: f64 = (0..n_samples).map(|i| transport_plan[[i, j]] * u[i]).sum();
if sum > 0.0 {
v[j] = target_marginal[j] / sum;
}
}
for i in 0..n_samples {
for j in 0..n_samples {
transport_plan[[i, j]] *= u[i] * v[j];
}
}
}
Ok(transport_plan)
}
}
impl Predict<ArrayView2<'_, Float>, Array1<i32>> for WassersteinSemiSupervised<WassersteinTrained> {
#[allow(non_snake_case)]
fn predict(&self, X: &ArrayView2<'_, Float>) -> SklResult<Array1<i32>> {
let X = X.to_owned();
let n_test = X.nrows();
let mut predictions = Array1::zeros(n_test);
for i in 0..n_test {
let mut min_transport_cost = f64::INFINITY;
let mut best_class = self.state.classes[0];
for class in self.state.classes.iter() {
let mut class_cost = 0.0;
let mut class_count = 0;
for j in 0..self.state.X_train.nrows() {
if self.state.y_train[j] == *class {
let diff = &X.row(i) - &self.state.X_train.row(j);
let dist = diff.mapv(|x| x * x).sum().sqrt();
class_cost += dist;
class_count += 1;
}
}
if class_count > 0 {
class_cost /= class_count as f64;
if class_cost < min_transport_cost {
min_transport_cost = class_cost;
best_class = *class;
}
}
}
predictions[i] = best_class;
}
Ok(predictions)
}
}
#[derive(Debug, Clone)]
pub struct EarthMoverDistance<S = Untrained> {
state: S,
n_neighbors: usize,
max_iter: usize,
tol: f64,
alpha: f64,
random_state: Option<u64>,
}
impl EarthMoverDistance<Untrained> {
pub fn new() -> Self {
Self {
state: Untrained,
n_neighbors: 10,
max_iter: 100,
tol: 1e-6,
alpha: 0.8,
random_state: None,
}
}
pub fn n_neighbors(mut self, n_neighbors: usize) -> Self {
self.n_neighbors = n_neighbors;
self
}
pub fn max_iter(mut self, max_iter: usize) -> Self {
self.max_iter = max_iter;
self
}
pub fn tol(mut self, tol: f64) -> Self {
self.tol = tol;
self
}
pub fn alpha(mut self, alpha: f64) -> Self {
self.alpha = alpha;
self
}
pub fn random_state(mut self, random_state: u64) -> Self {
self.random_state = Some(random_state);
self
}
}
impl Default for EarthMoverDistance<Untrained> {
fn default() -> Self {
Self::new()
}
}
impl Estimator for EarthMoverDistance<Untrained> {
type Config = ();
type Error = SklearsError;
type Float = Float;
fn config(&self) -> &Self::Config {
&()
}
}
impl Fit<ArrayView2<'_, Float>, ArrayView1<'_, i32>> for EarthMoverDistance<Untrained> {
type Fitted = EarthMoverDistance<EarthMoverTrained>;
#[allow(non_snake_case)]
fn fit(self, X: &ArrayView2<'_, Float>, y: &ArrayView1<'_, i32>) -> SklResult<Self::Fitted> {
let X = X.to_owned();
let y = y.to_owned();
let (n_samples, _n_features) = X.dim();
let mut labeled_indices = Vec::new();
let mut unlabeled_indices = Vec::new();
let mut classes = std::collections::HashSet::new();
for (i, &label) in y.iter().enumerate() {
if label == -1 {
unlabeled_indices.push(i);
} else {
labeled_indices.push(i);
classes.insert(label);
}
}
if labeled_indices.is_empty() {
return Err(SklearsError::InvalidInput(
"No labeled samples provided".to_string(),
));
}
let classes: Vec<i32> = classes.into_iter().collect();
let n_classes = classes.len();
let adjacency_matrix = self.build_emd_graph(&X)?;
let mut Y = Array2::<f64>::zeros((n_samples, n_classes));
for &idx in &labeled_indices {
if let Some(class_idx) = classes.iter().position(|&c| c == y[idx]) {
Y[[idx, class_idx]] = 1.0;
}
}
let mut prev_Y = Y.clone();
for _iter in 0..self.max_iter {
let mut Y_new = Array2::<f64>::zeros((n_samples, n_classes));
for i in 0..n_samples {
for k in 0..n_classes {
let mut propagated = 0.0;
for j in 0..n_samples {
propagated += adjacency_matrix[[i, j]] * Y[[j, k]];
}
if labeled_indices.contains(&i) {
if let Some(class_idx) = classes.iter().position(|&c| c == y[i]) {
Y_new[[i, k]] = if k == class_idx { 1.0 } else { 0.0 };
}
} else {
Y_new[[i, k]] = self.alpha * propagated;
}
}
}
for &idx in &unlabeled_indices {
let row_sum: f64 = Y_new.row(idx).sum();
if row_sum > 0.0 {
for k in 0..n_classes {
Y_new[[idx, k]] /= row_sum;
}
}
}
let diff = (&Y_new - &Y).mapv(|x| x.abs()).sum();
if diff < self.tol {
break;
}
Y = Y_new;
prev_Y = Y.clone();
}
let mut final_labels = y.clone();
for &idx in &unlabeled_indices {
let class_idx = Y
.row(idx)
.iter()
.enumerate()
.max_by(|a, b| a.1.partial_cmp(b.1).expect("operation should succeed"))
.expect("operation should succeed")
.0;
final_labels[idx] = classes[class_idx];
}
Ok(EarthMoverDistance {
state: EarthMoverTrained {
X_train: X,
y_train: final_labels,
classes: Array1::from(classes),
adjacency_matrix,
label_distributions: Y,
},
n_neighbors: self.n_neighbors,
max_iter: self.max_iter,
tol: self.tol,
alpha: self.alpha,
random_state: self.random_state,
})
}
}
impl EarthMoverDistance<Untrained> {
fn build_emd_graph(&self, X: &Array2<f64>) -> SklResult<Array2<f64>> {
let n_samples = X.nrows();
let mut adjacency = Array2::<f64>::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 diff = &X.row(i) - &X.row(j);
let dist = diff.mapv(|x| x * x).sum().sqrt();
distances.push((j, dist));
}
}
distances.sort_by(|a, b| a.1.partial_cmp(&b.1).expect("operation should succeed"));
for &(j, dist) in distances.iter().take(self.n_neighbors) {
let emd_weight = (-dist.powi(2) / 2.0).exp();
adjacency[[i, j]] = emd_weight;
}
}
for i in 0..n_samples {
for j in (i + 1)..n_samples {
let sym_value = (adjacency[[i, j]] + adjacency[[j, i]]) / 2.0;
adjacency[[i, j]] = sym_value;
adjacency[[j, i]] = sym_value;
}
}
for _ in 0..32 {
let mut updated = false;
for i in 0..n_samples {
let row_sum: f64 = adjacency.row(i).sum();
if row_sum == 0.0 {
if adjacency[[i, i]] != 1.0 {
adjacency[[i, i]] = 1.0;
updated = true;
}
continue;
}
if (row_sum - 1.0).abs() <= 1e-10 {
continue;
}
if row_sum < 1.0 {
adjacency[[i, i]] += 1.0 - row_sum;
updated = true;
} else {
let scale = 1.0 / row_sum;
for j in 0..n_samples {
if i == j {
adjacency[[i, i]] *= scale;
} else {
adjacency[[i, j]] *= scale;
adjacency[[j, i]] *= scale;
}
}
updated = true;
}
}
if !updated {
break;
}
}
Ok(adjacency)
}
}
impl Predict<ArrayView2<'_, Float>, Array1<i32>> for EarthMoverDistance<EarthMoverTrained> {
#[allow(non_snake_case)]
fn predict(&self, X: &ArrayView2<'_, Float>) -> SklResult<Array1<i32>> {
let X = X.to_owned();
let n_test = X.nrows();
let mut predictions = Array1::zeros(n_test);
for i in 0..n_test {
let mut min_dist = f64::INFINITY;
let mut best_label = self.state.classes[0];
for j in 0..self.state.X_train.nrows() {
let diff = &X.row(i) - &self.state.X_train.row(j);
let dist = diff.mapv(|x| x * x).sum().sqrt();
if dist < min_dist {
min_dist = dist;
best_label = self.state.y_train[j];
}
}
predictions[i] = best_label;
}
Ok(predictions)
}
}
#[derive(Debug, Clone)]
pub struct WassersteinTrained {
pub X_train: Array2<f64>,
pub y_train: Array1<i32>,
pub classes: Array1<i32>,
pub cost_matrix: Array2<f64>,
pub transport_plan: Array2<f64>,
pub label_distributions: Array2<f64>,
}
#[derive(Debug, Clone)]
pub struct EarthMoverTrained {
pub X_train: Array2<f64>,
pub y_train: Array1<i32>,
pub classes: Array1<i32>,
pub adjacency_matrix: Array2<f64>,
pub label_distributions: Array2<f64>,
}
#[derive(Debug, Clone)]
pub struct GromovWassersteinSemiSupervised<S = Untrained> {
state: S,
regularization: f64,
max_iter: usize,
tol: f64,
alpha: f64,
random_state: Option<u64>,
}
impl GromovWassersteinSemiSupervised<Untrained> {
pub fn new() -> Self {
Self {
state: Untrained,
regularization: 0.1,
max_iter: 50,
tol: 1e-6,
alpha: 0.5,
random_state: None,
}
}
pub fn regularization(mut self, regularization: f64) -> Self {
self.regularization = regularization;
self
}
pub fn max_iter(mut self, max_iter: usize) -> Self {
self.max_iter = max_iter;
self
}
pub fn tol(mut self, tol: f64) -> Self {
self.tol = tol;
self
}
pub fn alpha(mut self, alpha: f64) -> Self {
self.alpha = alpha;
self
}
pub fn random_state(mut self, random_state: u64) -> Self {
self.random_state = Some(random_state);
self
}
}
impl Default for GromovWassersteinSemiSupervised<Untrained> {
fn default() -> Self {
Self::new()
}
}
impl Estimator for GromovWassersteinSemiSupervised<Untrained> {
type Config = ();
type Error = SklearsError;
type Float = Float;
fn config(&self) -> &Self::Config {
&()
}
}
impl Fit<ArrayView2<'_, Float>, ArrayView1<'_, i32>>
for GromovWassersteinSemiSupervised<Untrained>
{
type Fitted = GromovWassersteinSemiSupervised<GromovWassersteinTrained>;
#[allow(non_snake_case)]
fn fit(self, X: &ArrayView2<'_, Float>, y: &ArrayView1<'_, i32>) -> SklResult<Self::Fitted> {
let X = X.to_owned();
let y = y.to_owned();
let (n_samples, _n_features) = X.dim();
let mut labeled_indices = Vec::new();
let mut unlabeled_indices = Vec::new();
let mut classes = std::collections::HashSet::new();
for (i, &label) in y.iter().enumerate() {
if label == -1 {
unlabeled_indices.push(i);
} else {
labeled_indices.push(i);
classes.insert(label);
}
}
if labeled_indices.is_empty() {
return Err(SklearsError::InvalidInput(
"No labeled samples provided".to_string(),
));
}
let classes: Vec<i32> = classes.into_iter().collect();
let n_classes = classes.len();
let mut structure_matrix = Array2::<f64>::zeros((n_samples, n_samples));
for i in 0..n_samples {
for j in 0..n_samples {
let diff = &X.row(i) - &X.row(j);
let dist = diff.mapv(|x| x * x).sum().sqrt();
structure_matrix[[i, j]] = dist;
}
}
let mut transport_plan =
Array2::<f64>::ones((n_samples, n_samples)) / (n_samples * n_samples) as f64;
let mut label_distributions = Array2::<f64>::zeros((n_samples, n_classes));
for &idx in &labeled_indices {
if let Some(class_idx) = classes.iter().position(|&c| c == y[idx]) {
label_distributions[[idx, class_idx]] = 1.0;
}
}
for &idx in &unlabeled_indices {
for class_idx in 0..n_classes {
label_distributions[[idx, class_idx]] = 1.0 / n_classes as f64;
}
}
for _iter in 0..self.max_iter {
let prev_plan = transport_plan.clone();
for i in 0..n_samples {
for j in 0..n_samples {
let mut cost = 0.0;
for k in 0..n_samples.min(10) {
let diff = (structure_matrix[[i, k]] - structure_matrix[[j, k]]).abs();
cost += diff;
}
transport_plan[[i, j]] = (-cost / self.regularization).exp();
}
}
let row_sum: f64 = transport_plan.sum();
if row_sum > 0.0 {
transport_plan /= row_sum;
}
for &unlabeled_idx in &unlabeled_indices {
let mut new_distribution = Array1::<f64>::zeros(n_classes);
for &labeled_idx in &labeled_indices {
let weight = transport_plan[[unlabeled_idx, labeled_idx]];
if let Some(class_idx) = classes.iter().position(|&c| c == y[labeled_idx]) {
new_distribution[class_idx] += weight;
}
}
let sum = new_distribution.sum();
if sum > 0.0 {
new_distribution /= sum;
}
for class_idx in 0..n_classes {
label_distributions[[unlabeled_idx, class_idx]] = new_distribution[class_idx];
}
}
let diff = (&transport_plan - &prev_plan).mapv(|x| x.abs()).sum();
if diff < self.tol {
break;
}
}
let mut final_labels = y.clone();
for &idx in &unlabeled_indices {
let class_idx = label_distributions
.row(idx)
.iter()
.enumerate()
.max_by(|a, b| a.1.partial_cmp(b.1).expect("operation should succeed"))
.expect("operation should succeed")
.0;
final_labels[idx] = classes[class_idx];
}
Ok(GromovWassersteinSemiSupervised {
state: GromovWassersteinTrained {
X_train: X,
y_train: final_labels,
classes: Array1::from(classes),
structure_matrix,
transport_plan,
label_distributions,
},
regularization: self.regularization,
max_iter: self.max_iter,
tol: self.tol,
alpha: self.alpha,
random_state: self.random_state,
})
}
}
impl Predict<ArrayView2<'_, Float>, Array1<i32>>
for GromovWassersteinSemiSupervised<GromovWassersteinTrained>
{
#[allow(non_snake_case)]
fn predict(&self, X: &ArrayView2<'_, Float>) -> SklResult<Array1<i32>> {
let X = X.to_owned();
let n_test = X.nrows();
let mut predictions = Array1::zeros(n_test);
for i in 0..n_test {
let mut min_dist = f64::INFINITY;
let mut best_label = self.state.classes[0];
for j in 0..self.state.X_train.nrows() {
let diff = &X.row(i) - &self.state.X_train.row(j);
let dist = diff.mapv(|x| x * x).sum().sqrt();
if dist < min_dist {
min_dist = dist;
best_label = self.state.y_train[j];
}
}
predictions[i] = best_label;
}
Ok(predictions)
}
}
#[derive(Debug, Clone)]
pub struct GromovWassersteinTrained {
pub X_train: Array2<f64>,
pub y_train: Array1<i32>,
pub classes: Array1<i32>,
pub structure_matrix: Array2<f64>,
pub transport_plan: Array2<f64>,
pub label_distributions: Array2<f64>,
}
#[allow(non_snake_case)]
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::array;
#[test]
#[allow(non_snake_case)]
fn test_wasserstein_semi_supervised() {
let X = array![[1.0, 2.0], [2.0, 3.0], [3.0, 4.0], [4.0, 5.0]];
let y = array![0, 1, -1, -1];
let wss = WassersteinSemiSupervised::new()
.regularization(0.1)
.max_iter(10)
.random_state(42);
let fitted = wss
.fit(&X.view(), &y.view())
.expect("operation should succeed");
let predictions = fitted.predict(&X.view()).expect("operation should succeed");
assert_eq!(predictions.len(), 4);
assert!(predictions.iter().all(|&p| p >= 0 && p <= 1));
assert!(predictions[0] == 0 || predictions[0] == 1);
assert!(predictions[1] == 0 || predictions[1] == 1);
}
#[test]
#[allow(non_snake_case)]
fn test_earth_mover_distance() {
let X = array![[1.0, 2.0], [2.0, 3.0], [3.0, 4.0], [4.0, 5.0]];
let y = array![0, 1, -1, -1];
let emd = EarthMoverDistance::new()
.n_neighbors(2)
.max_iter(10)
.alpha(0.8)
.random_state(42);
let fitted = emd
.fit(&X.view(), &y.view())
.expect("operation should succeed");
let predictions = fitted.predict(&X.view()).expect("operation should succeed");
assert_eq!(predictions.len(), 4);
assert!(predictions.iter().all(|&p| p >= 0 && p <= 1));
}
#[test]
fn test_wasserstein_parameters() {
let wss = WassersteinSemiSupervised::new()
.regularization(0.2)
.max_iter(200)
.tol(1e-8)
.transport_weight(2.0)
.supervised_weight(0.5);
assert_eq!(wss.regularization, 0.2);
assert_eq!(wss.max_iter, 200);
assert_eq!(wss.tol, 1e-8);
assert_eq!(wss.transport_weight, 2.0);
assert_eq!(wss.supervised_weight, 0.5);
}
#[test]
fn test_earth_mover_parameters() {
let emd = EarthMoverDistance::new()
.n_neighbors(5)
.max_iter(50)
.tol(1e-5)
.alpha(0.9);
assert_eq!(emd.n_neighbors, 5);
assert_eq!(emd.max_iter, 50);
assert_eq!(emd.tol, 1e-5);
assert_eq!(emd.alpha, 0.9);
}
#[test]
#[allow(non_snake_case)]
fn test_cost_matrix_computation() {
let wss = WassersteinSemiSupervised::new();
let X = array![[1.0, 2.0], [3.0, 4.0]];
let cost_matrix = wss
.compute_cost_matrix(&X)
.expect("operation should succeed");
assert_eq!(cost_matrix.dim(), (2, 2));
assert_eq!(cost_matrix[[0, 0]], 0.0); assert_eq!(cost_matrix[[1, 1]], 0.0);
assert!(cost_matrix[[0, 1]] > 0.0); assert_eq!(cost_matrix[[0, 1]], cost_matrix[[1, 0]]); }
#[test]
#[allow(non_snake_case)]
fn test_emd_graph_construction() {
let emd = EarthMoverDistance::new().n_neighbors(1);
let X = array![[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]];
let adjacency = emd.build_emd_graph(&X).expect("operation should succeed");
assert_eq!(adjacency.dim(), (3, 3));
for i in 0..3 {
let row_sum: f64 = adjacency.row(i).sum();
assert!((row_sum - 1.0).abs() < 1e-10 || row_sum == 0.0);
}
for i in 0..3 {
for j in 0..3 {
assert!((adjacency[[i, j]] - adjacency[[j, i]]).abs() < 1e-3);
}
}
}
#[test]
#[allow(non_snake_case)]
fn test_empty_labeled_samples_error() {
let X = array![[1.0, 2.0], [2.0, 3.0]];
let y = array![-1, -1];
let wss = WassersteinSemiSupervised::new();
let result = wss.fit(&X.view(), &y.view());
assert!(result.is_err());
let emd = EarthMoverDistance::new();
let result = emd.fit(&X.view(), &y.view());
assert!(result.is_err());
}
#[test]
#[allow(non_snake_case)]
fn test_single_sample_stability() {
let X = array![[1.0, 2.0]];
let y = array![0];
let wss = WassersteinSemiSupervised::new().max_iter(5);
let fitted = wss
.fit(&X.view(), &y.view())
.expect("operation should succeed");
let predictions = fitted.predict(&X.view()).expect("operation should succeed");
assert_eq!(predictions.len(), 1);
assert_eq!(predictions[0], 0);
}
#[test]
#[allow(non_snake_case)]
fn test_gromov_wasserstein_basic() {
let X = array![[1.0, 2.0], [2.0, 3.0], [3.0, 4.0], [4.0, 5.0]];
let y = array![0, 1, -1, -1];
let gw = GromovWassersteinSemiSupervised::new()
.regularization(0.1)
.max_iter(10)
.random_state(42);
let fitted = gw
.fit(&X.view(), &y.view())
.expect("operation should succeed");
let predictions = fitted.predict(&X.view()).expect("operation should succeed");
assert_eq!(predictions.len(), 4);
assert!(predictions.iter().all(|&p| p >= 0 && p <= 1));
}
#[test]
fn test_gromov_wasserstein_parameters() {
let gw = GromovWassersteinSemiSupervised::new()
.regularization(0.2)
.max_iter(200)
.tol(1e-8)
.alpha(0.8);
assert_eq!(gw.regularization, 0.2);
assert_eq!(gw.max_iter, 200);
assert_eq!(gw.tol, 1e-8);
assert_eq!(gw.alpha, 0.8);
}
}