use faer::MatRef;
use rand::prelude::*;
use rand::rngs::StdRng;
use rayon::prelude::*;
use rustc_hash::FxHashSet;
use std::time::Instant;
use thousands::Separable;
use crate::core::math::sparse::*;
use crate::prelude::*;
#[derive(Clone, Copy, Default)]
pub enum KnnSymmetrisation {
#[default]
Intersection,
Union,
}
pub fn parse_knn_symmetrisation(s: &str) -> Option<KnnSymmetrisation> {
match s.to_lowercase().as_str() {
"intersection" => Some(KnnSymmetrisation::Intersection),
"union" => Some(KnnSymmetrisation::Union),
_ => None,
}
}
#[derive(Clone, Debug)]
pub struct SEACellsParams {
pub n_sea_cells: usize,
pub max_fw_iters: usize,
pub convergence_epsilon: f32,
pub max_iter: usize,
pub min_iter: usize,
pub greedy_threshold: usize,
pub graph_building: String,
pub pruning: bool,
pub pruning_threshold: f32,
pub knn_params: KnnParams,
}
pub fn assignments_to_metacells(assignments: &[usize], k: usize) -> Vec<Vec<usize>> {
let mut metacells = vec![Vec::new(); k];
for (cell_id, &seacell_id) in assignments.iter().enumerate() {
metacells[seacell_id].push(cell_id);
}
metacells
}
fn sparse_to_dense_csr_scaled(mat: &CompressedSparseData2<f32>, scale: f32, dense: &mut [f32]) {
let (nrows, ncols) = mat.shape;
dense.fill(0.0);
for row in 0..nrows {
let row_start = mat.indptr[row];
let row_end = mat.indptr[row + 1];
for idx in row_start..row_end {
let col = mat.indices[idx];
dense[row * ncols + col] = mat.data[idx] * scale;
}
}
}
fn prune_and_renormalise(mat: &mut CompressedSparseData2<f32>, threshold: f32) {
let mut new_data = Vec::new();
let mut new_indices = Vec::new();
let mut new_indptr = vec![0];
for row in 0..mat.shape.0 {
let start = mat.indptr[row];
let end = mat.indptr[row + 1];
for idx in start..end {
if mat.data[idx].abs() > threshold {
new_data.push(mat.data[idx]);
new_indices.push(mat.indices[idx]);
}
}
new_indptr.push(new_data.len());
}
mat.data = new_data;
mat.indices = new_indices;
mat.indptr = new_indptr;
normalise_csr_columns_l1(mat);
}
fn matrix_trace(mat: &CompressedSparseData2<f32>) -> f32 {
let n = mat.shape.0.min(mat.shape.1);
let mut trace = 0.0;
for i in 0..n {
let row_start = mat.indptr[i];
let row_end = mat.indptr[i + 1];
for idx in row_start..row_end {
if mat.indices[idx] == i {
trace += mat.data[idx];
break;
}
}
}
trace
}
fn compute_diffusion_kernel(
knn_indices: &[Vec<usize>],
knn_distances: &[Vec<f32>],
knn: usize,
) -> CompressedSparseData2<f32> {
let n = knn_indices.len();
let adaptive_k = (knn / 3).max(1);
let adaptive_std: Vec<f32> = knn_distances
.iter()
.map(|dists| {
let mut sorted = dists.clone();
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
sorted[adaptive_k - 1]
})
.collect();
let mut rows = Vec::new();
let mut cols = Vec::new();
let mut vals = Vec::new();
for (i, neighbours) in knn_indices.iter().enumerate() {
for (idx, &j) in neighbours.iter().enumerate() {
let dist = knn_distances[i][idx].sqrt();
let weight = (-dist / adaptive_std[i]).exp();
rows.push(i);
cols.push(j);
vals.push(weight);
}
}
let w = coo_to_csr(&rows, &cols, &vals, (n, n));
let w_t = w.transpose_and_convert();
sparse_add_csr(&w, &w_t)
}
fn diffusion_map_from_kernel(
kernel: &mut CompressedSparseData2<f32>,
n_components: usize,
seed: u64,
) -> (Vec<f32>, Vec<Vec<f32>>) {
let row_sums: Vec<f32> = (0..kernel.shape.0)
.map(|i| {
(kernel.indptr[i]..kernel.indptr[i + 1])
.map(|idx| kernel.data[idx])
.sum()
})
.collect();
for i in 0..kernel.shape.0 {
let d_i_sqrt = row_sums[i].sqrt();
for idx in kernel.indptr[i]..kernel.indptr[i + 1] {
let j = kernel.indices[idx];
let d_j_sqrt = row_sums[j].sqrt();
kernel.data[idx] /= d_i_sqrt * d_j_sqrt;
}
}
compute_largest_eigenpairs_lanczos(kernel, n_components, seed)
}
fn determine_multiscale_space(
eigenvalues: &[f32],
eigenvectors: &[Vec<f32>],
n_eigs: Option<usize>,
) -> Vec<Vec<f32>> {
let n = eigenvectors.len();
let use_n_eigs = if let Some(n) = n_eigs {
n
} else {
let gaps: Vec<f32> = eigenvalues.windows(2).map(|w| w[0] - w[1]).collect();
let max_gap_idx = gaps
.iter()
.enumerate()
.max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())
.map(|(idx, _)| idx + 1)
.unwrap_or(3);
println!("Gaps: {:?}", &gaps[..gaps.len().min(10)]);
println!("Max gap at index: {}", max_gap_idx);
max_gap_idx.max(3).min(eigenvalues.len())
};
let use_indices: Vec<usize> = (1..use_n_eigs).collect();
let mut scaled = vec![vec![0.0f32; use_indices.len()]; n];
for (out_idx, &eig_idx) in use_indices.iter().enumerate() {
let lambda = eigenvalues[eig_idx];
let scale = lambda / (1.0 - lambda);
for i in 0..n {
scaled[i][out_idx] = eigenvectors[i][eig_idx] * scale;
}
}
scaled
}
fn max_min_sampling(data: &[Vec<f32>], num_waypoints: usize, seed: u64) -> Vec<usize> {
let n = data.len();
let n_dims = data[0].len();
let no_iterations = (num_waypoints / n_dims).max(1);
let mut rng = StdRng::seed_from_u64(seed);
let mut waypoint_set = FxHashSet::default();
for dim in 0..n_dims {
let vec: Vec<f32> = data.iter().map(|row| row[dim]).collect();
let mut iter_set = vec![rng.random_range(0..n)];
let mut min_dists = vec![f32::MAX; n];
for i in 0..n {
min_dists[i] = (vec[i] - vec[iter_set[0]]).abs();
}
for _ in 1..no_iterations {
let new_wp = min_dists
.iter()
.enumerate()
.max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())
.map(|(idx, _)| idx)
.unwrap();
iter_set.push(new_wp);
for i in 0..n {
let dist = (vec[i] - vec[new_wp]).abs();
min_dists[i] = min_dists[i].min(dist);
}
}
waypoint_set.extend(iter_set);
}
waypoint_set.into_iter().collect()
}
pub struct SEACells<'a> {
n_cells: usize,
kernel_mat: Option<CompressedSparseData2<f32>>,
a: Option<CompressedSparseData2<f32>>,
b: Option<CompressedSparseData2<f32>>,
archetypes: Option<Vec<usize>>,
rss_history: Vec<f32>,
convergence_threshold: Option<f32>,
k_frobenius_norm_sq: Option<f32>,
params: &'a SEACellsParams,
}
impl<'a> SEACells<'a> {
pub fn new(n_cells: usize, params: &'a SEACellsParams) -> Self {
Self {
n_cells,
kernel_mat: None,
a: None,
b: None,
archetypes: None,
convergence_threshold: None,
k_frobenius_norm_sq: None,
rss_history: Vec::new(),
params,
}
}
pub fn construct_kernel_mat(
&mut self,
pca: MatRef<f32>,
knn_indices: &[Vec<usize>],
knn_distances: &[Vec<f32>],
verbose: bool,
) {
let n = pca.nrows();
let k = knn_indices[0].len();
if verbose {
println!("Computing adaptive bandwidth RBF kernel...");
}
let graph_construction =
parse_knn_symmetrisation(&self.params.graph_building).unwrap_or_default();
let median_idx = k / 2;
let median_dist = knn_distances
.iter()
.map(|d| d[median_idx].sqrt())
.collect::<Vec<f32>>();
let mut edges = FxHashSet::default();
for (i, neighbours) in knn_indices.iter().enumerate() {
for &j in neighbours {
edges.insert((i, j));
}
}
match graph_construction {
KnnSymmetrisation::Union => {
let to_add: Vec<_> = edges
.iter()
.filter_map(|&(i, j)| (!edges.contains(&(j, i))).then_some((j, i)))
.collect();
edges.extend(to_add);
}
KnnSymmetrisation::Intersection => {
let to_keep: FxHashSet<_> = edges
.iter()
.copied()
.filter(|&(i, j)| edges.contains(&(j, i)))
.collect();
edges = to_keep;
}
}
for i in 0..n {
edges.insert((i, i));
}
let mut rows: Vec<usize> = Vec::new();
let mut cols: Vec<usize> = Vec::new();
let mut vals: Vec<f32> = Vec::new();
for &(i, j) in &edges {
let mut dist_square = 0_f32;
for dim in 0..pca.ncols() {
let diff = pca.get(i, dim) - pca.get(j, dim);
dist_square += diff * diff;
}
let sigma_prod = median_dist[i] * median_dist[j];
let val = (-dist_square / sigma_prod).exp();
rows.push(i);
cols.push(j);
vals.push(val);
}
if verbose {
println!(
"Built kernel with {} non-zeros",
vals.len().separate_with_underscores()
);
}
let kernel = coo_to_csr(&rows, &cols, &vals, (n, n));
if self.n_cells > 20000 {
if verbose {
println!("Pre-computing kernel Frobenius norm...");
}
let k_frob = frobenius_norm(&kernel);
self.k_frobenius_norm_sq = Some(k_frob * k_frob);
}
self.kernel_mat = Some(kernel);
}
fn k_squared_matmul(&self, x: &CompressedSparseData2<f32>) -> CompressedSparseData2<f32> {
let k = self.kernel_mat.as_ref().unwrap();
let kx = csr_matmul_csr(k, x);
csr_matmul_csr(k, &kx)
}
fn k_squared_matvec(&self, v: &[f32]) -> Vec<f32> {
let k = self.kernel_mat.as_ref().unwrap();
let kv = csr_matvec(k, v);
csr_matvec(k, &kv)
}
pub fn fit(&mut self, seed: usize, verbose: bool) {
assert!(
self.kernel_mat.is_some(),
"Must construct kernel matrix first"
);
assert!(self.archetypes.is_some(), "Must find archetypes first");
self.initialise_matrices(verbose, seed as u64);
let a = self.a.as_ref().unwrap();
let b = self.b.as_ref().unwrap();
let initial_rss = self.compute_rss(a, b);
self.rss_history.push(initial_rss);
self.convergence_threshold = Some(self.params.convergence_epsilon * initial_rss);
if verbose {
println!("Initial RSS: {:.6}", initial_rss);
println!(
"Convergence threshold: {:.6}",
self.convergence_threshold.unwrap()
);
}
let mut converged = false;
let mut n_iter = 0;
while (!converged && n_iter < self.params.max_iter) || n_iter < self.params.min_iter {
let iter_start = Instant::now();
n_iter += 1;
let b_current = self.b.take().unwrap();
let a_current = self.a.take().unwrap();
let a_new = self.update_a_mat(&b_current, &a_current, verbose);
let b_new = self.update_b_mat(&a_new, &b_current, verbose);
let rss = self.compute_rss(&a_new, &b_new);
self.rss_history.push(rss);
self.a = Some(a_new);
self.b = Some(b_new);
let iter_duration = iter_start.elapsed();
if verbose {
println!(
"Iteration {}: RSS = {:.6}, Time = {:.2}s",
n_iter,
rss,
iter_duration.as_secs_f32()
);
}
if n_iter > 1 {
let rss_diff = (self.rss_history[n_iter - 1] - self.rss_history[n_iter]).abs();
if rss_diff < self.convergence_threshold.unwrap() && n_iter >= self.params.min_iter
{
if verbose {
println!("Converged after {} iterations!", n_iter);
}
converged = true;
}
}
}
if !converged && verbose {
println!(
"Warning: Algorithm did not converge after {} iterations",
self.params.max_iter
);
}
}
pub fn initialise_archetypes(
&mut self,
knn_indices: &[Vec<usize>],
knn_distances: &[Vec<f32>],
verbose: bool,
seed: u64,
) {
if self.n_cells > self.params.greedy_threshold {
if verbose {
println!(
"Dataset large (n={}), using fast random init (threshold: {})",
self.n_cells.separate_with_underscores(),
self.params.greedy_threshold
);
}
self.initialise_archetypes_random(verbose, seed);
} else {
self.initialise_archetypes_combined(knn_indices, knn_distances, verbose, seed);
}
}
fn initialise_archetypes_random(&mut self, verbose: bool, seed: u64) {
let mut rng = StdRng::seed_from_u64(seed);
let mut indices: Vec<usize> = (0..self.n_cells).collect();
indices.shuffle(&mut rng);
let archetypes: Vec<usize> = indices.into_iter().take(self.params.n_sea_cells).collect();
if verbose {
println!("Selected {} random archetypes", archetypes.len());
}
self.archetypes = Some(archetypes);
}
fn initialise_archetypes_combined(
&mut self,
knn_indices: &[Vec<usize>],
knn_distances: &[Vec<f32>],
verbose: bool,
seed: u64,
) {
let k = self.params.n_sea_cells;
if verbose {
println!("Computing diffusion maps for waypoint initialisation...");
}
let mut kernel =
compute_diffusion_kernel(knn_indices, knn_distances, self.params.knn_params.k);
let (eigenvalues, eigenvectors) =
diffusion_map_from_kernel(&mut kernel, self.params.knn_params.k, seed);
let multiscale = determine_multiscale_space(&eigenvalues, &eigenvectors, Some(10));
let waypoint_ix = max_min_sampling(&multiscale, k, seed);
if verbose {
println!(
"Selecting {} cells from waypoint initialisation.",
waypoint_ix.len()
);
}
let from_greedy = k.saturating_sub(waypoint_ix.len());
if verbose {
println!("Initialising residual matrix using greedy column selection");
}
let greedy_ix = self.get_greedy_centres(from_greedy + 10);
if verbose {
println!(
"Selecting {} cells from greedy initialisation.",
from_greedy
);
}
let mut all_ix = waypoint_ix;
all_ix.extend(greedy_ix);
let mut seen = FxHashSet::default();
let unique_ix: Vec<usize> = all_ix
.into_iter()
.filter(|&x| seen.insert(x))
.take(k)
.collect();
self.archetypes = Some(unique_ix);
}
fn get_greedy_centres(&self, n_centres: usize) -> Vec<usize> {
let kernel = self.kernel_mat.as_ref().unwrap();
let n = kernel.shape.0;
const CHUNK_SIZE: usize = 256;
let mut f = vec![0_f32; n];
let mut g = vec![0_f32; n];
for chunk_start in (0..n).step_by(CHUNK_SIZE) {
let chunk_end = (chunk_start + CHUNK_SIZE).min(n);
let chunk_results: Vec<(usize, Vec<f32>)> = (chunk_start..chunk_end)
.into_par_iter()
.map(|i| {
let mut row_i = vec![0_f32; n];
for idx in kernel.indptr[i]..kernel.indptr[i + 1] {
row_i[kernel.indices[idx]] = kernel.data[idx];
}
let k2_col_i = csr_matvec(kernel, &row_i);
(i, k2_col_i)
})
.collect();
for (i, k2_col_i) in chunk_results {
g[i] = k2_col_i[i];
for j in 0..n {
f[j] += k2_col_i[j] * k2_col_i[j];
}
}
}
let mut omega: Vec<Vec<f32>> = vec![vec![0_f32; n]; n_centres];
let mut centres: Vec<usize> = Vec::with_capacity(n_centres);
let mut e_p = vec![0.0f32; n];
let mut omega_new = vec![0.0f32; n];
for iter in 0..n_centres {
let mut best_idx = 0;
let mut best_score = f32::MIN;
for i in 0..n {
if g[i] > 1e-15 {
let score = f[i] / g[i];
if score > best_score {
best_score = score;
best_idx = i;
}
}
}
centres.push(best_idx);
e_p.fill(0.0);
e_p[best_idx] = 1.0;
let k2_col = self.k_squared_matvec(&e_p);
let mut delta = k2_col.clone();
for i in 0..n {
let omega_sum: f32 = (0..iter).map(|r| omega[r][best_idx] * omega[r][i]).sum();
delta[i] -= omega_sum;
}
delta[best_idx] = delta[best_idx].max(0.0);
let delta_p_sqrt = delta[best_idx].sqrt().max(1e-6);
for i in 0..n {
omega_new[i] = delta[i] / delta_p_sqrt;
}
let omega_sq_norm: f32 = omega_new.iter().map(|&x| x * x).sum();
let k_omega_new = self.k_squared_matvec(&omega_new);
for i in 0..n {
let omega_hadamard = omega_new[i] * omega_new[i];
let term1 = omega_sq_norm * omega_hadamard;
let pl: f32 = (0..iter).map(|r| omega[r][best_idx] * omega[r][i]).sum();
let term2 = omega_new[i] * (k_omega_new[i] - pl);
f[i] += -2.0 * term2 + term1;
g[i] += omega_hadamard;
}
omega[iter].copy_from_slice(&omega_new);
}
centres
}
fn initialise_matrices(&mut self, verbose: bool, seed: u64) {
let archetypes = self.archetypes.as_ref().unwrap();
let k = archetypes.len();
let n = self.n_cells;
if verbose {
println!("Initialising A and B matrices...");
}
let mut b_rows = Vec::new();
let mut b_cols = Vec::new();
let mut b_vals = Vec::new();
for (col, &row) in archetypes.iter().enumerate() {
b_rows.push(row);
b_cols.push(col);
b_vals.push(1_f32);
}
let b = coo_to_csr(&b_rows, &b_cols, &b_vals, (n, k));
let archetypes_per_cell = (k as f32 * 0.25).ceil() as usize;
let mut rng = StdRng::seed_from_u64(seed);
let mut a_rows = Vec::new();
let mut a_cols = Vec::new();
let mut a_vals = Vec::new();
for cell in 0..n {
for _ in 0..archetypes_per_cell {
let archetype = rng.random_range(0..k);
a_rows.push(archetype);
a_cols.push(cell);
a_vals.push(rng.random::<f32>());
}
}
let mut a = coo_to_csr(&a_rows, &a_cols, &a_vals, (k, n));
normalise_csr_columns_l1(&mut a);
a = self.update_a_mat(&b, &a, verbose);
self.a = Some(a);
self.b = Some(b);
}
fn update_a_mat(
&self,
b: &CompressedSparseData2<f32>,
a_prev: &CompressedSparseData2<f32>,
verbose: bool,
) -> CompressedSparseData2<f32> {
let k2_b = self.k_squared_matmul(b);
let t2 = k2_b.transpose_and_convert();
let t1 = csr_matmul_csr(&t2, b);
let mut a = a_prev.clone();
let n = a.shape.1;
let k = a.shape.0;
let mut g_dense = vec![0.0f32; k * n];
for t in 0..self.params.max_fw_iters {
let t1_a = csr_matmul_csr(&t1, &a);
let g_mat = sparse_subtract_csr(&t1_a, &t2);
sparse_to_dense_csr_scaled(&g_mat, 2.0, &mut g_dense);
let argmins: Vec<usize> = (0..n)
.into_par_iter()
.map(|col| {
let mut min_val = g_dense[col];
let mut min_idx = 0;
for row in 1..k {
let val = g_dense[row * n + col];
if val < min_val {
min_val = val;
min_idx = row;
}
}
min_idx
})
.collect();
let mut e_data: Vec<(usize, usize, f32)> = argmins
.iter()
.enumerate()
.map(|(col, &row)| (row, col, 1.0f32))
.collect();
e_data.sort_unstable_by_key(|&(r, c, _)| (r, c));
let e_rows: Vec<usize> = e_data.iter().map(|&(r, _, _)| r).collect();
let e_cols: Vec<usize> = e_data.iter().map(|&(_, c, _)| c).collect();
let e_vals: Vec<f32> = e_data.iter().map(|&(_, _, v)| v).collect();
let e = coo_to_csr_presorted(&e_rows, &e_cols, &e_vals, (k, n));
let step_size = 2.0 / (t as f32 + 2.0);
let retain = 1.0 - step_size;
for val in &mut a.data {
*val *= retain;
}
let e_scaled = sparse_scalar_multiply_csr(&e, step_size);
a = sparse_add_csr(&a, &e_scaled);
if self.params.pruning {
prune_and_renormalise(&mut a, self.params.pruning_threshold);
}
if verbose && (t + 1) % 10 == 0 {
println!(
" A matrix Frank-Wolfe iteration: {} / {}",
t + 1,
self.params.max_fw_iters
);
}
}
a
}
fn update_b_mat(
&self,
a: &CompressedSparseData2<f32>,
b_prev: &CompressedSparseData2<f32>,
verbose: bool,
) -> CompressedSparseData2<f32> {
let a_t = a.transpose_and_convert();
let t1 = csr_matmul_csr(a, &a_t);
let t2 = self.k_squared_matmul(&a_t);
const FW_TOLERANCE: f32 = 1e-4;
let mut b = b_prev.clone();
let n = b.shape.0;
let k = b.shape.1;
let mut g_dense = vec![0.0f32; n * k];
for t in 0..self.params.max_fw_iters {
let k2_b = self.k_squared_matmul(&b);
let k2_b_t1 = csr_matmul_csr(&k2_b, &t1);
let g_mat = sparse_subtract_csr(&k2_b_t1, &t2);
sparse_to_dense_csr_scaled(&g_mat, 2.0, &mut g_dense);
let argmins: Vec<usize> = (0..k)
.into_par_iter()
.map(|col| {
let mut min_val = g_dense[col];
let mut min_idx = 0;
for row in 1..n {
let val = g_dense[row * k + col];
if val < min_val {
min_val = val;
min_idx = row;
}
}
min_idx
})
.collect();
let mut e_data: Vec<(usize, usize, f32)> = argmins
.iter()
.enumerate()
.map(|(col, &row)| (row, col, 1.0f32))
.collect();
e_data.sort_unstable_by_key(|&(r, c, _)| (r, c));
let e_rows: Vec<usize> = e_data.iter().map(|&(r, _, _)| r).collect();
let e_cols: Vec<usize> = e_data.iter().map(|&(_, c, _)| c).collect();
let e_vals: Vec<f32> = e_data.iter().map(|&(_, _, v)| v).collect();
let e = coo_to_csr_presorted(&e_rows, &e_cols, &e_vals, (n, k));
let step_size = 2.0 / (t as f32 + 2.0);
let retain = 1.0 - step_size;
for val in &mut b.data {
*val *= retain;
}
let e_scaled = sparse_scalar_multiply_csr(&e, step_size);
let step_contribution = step_size * step_size * e_scaled.data.len() as f32;
b = sparse_add_csr(&b, &e_scaled);
if self.params.pruning {
prune_and_renormalise(&mut b, self.params.pruning_threshold);
}
if verbose && (t + 1) % 10 == 0 {
println!(
" B matrix Frank-Wolfe iteration: {} / {}",
t + 1,
self.params.max_fw_iters
);
}
if step_contribution.sqrt() < FW_TOLERANCE && t >= 10 {
if verbose {
println!(" B matrix FW converged early at iteration {}", t + 1);
}
break;
}
}
b
}
fn compute_rss(&self, a: &CompressedSparseData2<f32>, b: &CompressedSparseData2<f32>) -> f32 {
if self.n_cells <= 20000 {
self.compute_rss_simple(a, b)
} else {
self.compute_rss_trace(a, b)
}
}
fn compute_rss_simple(
&self,
a: &CompressedSparseData2<f32>,
b: &CompressedSparseData2<f32>,
) -> f32 {
let k_mat = self.kernel_mat.as_ref().unwrap();
let k_b = csr_matmul_csr(k_mat, b);
let reconstruction = csr_matmul_csr(&k_b, a);
let diff = sparse_subtract_csr(k_mat, &reconstruction);
frobenius_norm(&diff)
}
fn compute_rss_trace(
&self,
a: &CompressedSparseData2<f32>,
b: &CompressedSparseData2<f32>,
) -> f32 {
let k_frob_sq = self.k_frobenius_norm_sq.unwrap();
let k2_b = self.k_squared_matmul(b);
let a_k2b = csr_matmul_csr(a, &k2_b);
let trace_term = matrix_trace(&a_k2b);
let a_t = a.transpose_and_convert();
let a_at = csr_matmul_csr(a, &a_t);
let b_t = b.transpose_and_convert();
let bt_k2b = csr_matmul_csr(&b_t, &k2_b);
let result = csr_matmul_csr(&a_at, &bt_k2b); let reconstruction_frob_sq = matrix_trace(&result);
(k_frob_sq - 2.0 * trace_term + reconstruction_frob_sq).sqrt()
}
pub fn get_hard_assignments(&self) -> Vec<usize> {
let a = self.a.as_ref().expect("Model not fitted yet");
let n = a.shape.1;
let a_csc = a.transpose_and_convert();
let mut assignments = vec![0usize; n];
for cell in 0..n {
let start = a_csc.indptr[cell];
let end = a_csc.indptr[cell + 1];
let mut max_val = f32::NEG_INFINITY;
let mut max_arch = 0;
for idx in start..end {
if a_csc.data[idx] > max_val {
max_val = a_csc.data[idx];
max_arch = a_csc.indices[idx];
}
}
assignments[cell] = max_arch;
}
assignments
}
pub fn get_rss_history(&self) -> &[f32] {
&self.rss_history
}
pub fn get_archetypes(&self) -> Vec<usize> {
self.archetypes
.as_ref()
.expect("Archetypes not initialised yet")
.clone()
}
}