Skip to main content

legume_numeric/matrix/
dmatrix_util.rs

1use nalgebra::{ComplexField, DMatrix, Matrix};
2use nalgebra_sparse::{coo::CooMatrix, csc::CscMatrix, csr::CsrMatrix};
3
4use crate::matrix::rand_util::{collect_seeded, entropy_seed};
5use num_traits::Float;
6use rand_distr::{Gamma, StandardNormal, Uniform};
7use rayon::prelude::*;
8
9pub use crate::matrix::dmatrix_rsvd::nystrom_basis;
10use crate::matrix::traits::*;
11
12/// Build a `DMatrix<f32>` column-by-column in parallel. `build_col(j, col)`
13/// fills the `j`-th output column (length `nrows`) directly into the
14/// matrix's backing slice — no per-column `Vec` allocation, no stitching.
15///
16/// Each task gets a disjoint `&mut [f32]`, so reductions stay deterministic
17/// (per-column accumulation order is whatever `build_col` does internally).
18pub fn build_columns_par<F>(nrows: usize, ncols: usize, build_col: F) -> DMatrix<f32>
19where
20    F: Fn(usize, &mut [f32]) + Sync + Send,
21{
22    let mut out = DMatrix::<f32>::zeros(nrows, ncols);
23    if nrows > 0 && ncols > 0 {
24        out.as_mut_slice()
25            .par_chunks_mut(nrows)
26            .enumerate()
27            .for_each(|(j, col)| build_col(j, col));
28    }
29    out
30}
31
32pub fn subset_columns<T, D, S, I>(
33    matrix: &Matrix<T, nalgebra::Dyn, D, S>,
34    indices: I,
35) -> anyhow::Result<DMatrix<T>>
36where
37    T: nalgebra::RealField,
38    D: nalgebra::Dim,
39    S: nalgebra::RawStorage<T, nalgebra::Dyn, D>,
40    I: IntoIterator<Item = usize>,
41{
42    let cols = indices
43        .into_iter()
44        .map(|j| matrix.column(j))
45        .collect::<Vec<_>>();
46
47    concatenate_horizontal(&cols)
48}
49
50pub fn subset_rows<T, D, S, I>(
51    matrix: &Matrix<T, D, nalgebra::Dyn, S>,
52    indices: I,
53) -> anyhow::Result<DMatrix<T>>
54where
55    T: nalgebra::RealField,
56    D: nalgebra::Dim,
57    S: nalgebra::RawStorage<T, D, nalgebra::Dyn>,
58    I: IntoIterator<Item = usize>,
59{
60    let rows = indices
61        .into_iter()
62        .map(|j| matrix.row(j))
63        .collect::<Vec<_>>();
64
65    concatenate_vertical(&rows)
66}
67
68pub fn assign_columns<T, D, S, R, I>(
69    source: &Matrix<T, nalgebra::Dyn, D, S>,
70    indices: I,
71    target: &mut Matrix<T, nalgebra::Dyn, D, R>,
72) where
73    T: nalgebra::RealField,
74    D: nalgebra::Dim,
75    S: nalgebra::RawStorage<T, nalgebra::Dyn, D>,
76    R: nalgebra::RawStorageMut<T, nalgebra::Dyn, D>,
77    I: IntoIterator<Item = usize>,
78{
79    indices
80        .into_iter()
81        .zip(source.column_iter())
82        .for_each(|(j, x_j)| {
83            target.column_mut(j).copy_from(&x_j);
84        });
85}
86
87/// concatenate matrices or row vectors vertically
88pub fn concatenate_vertical<T, D, S>(
89    matrices: &[Matrix<T, D, nalgebra::Dyn, S>],
90) -> anyhow::Result<DMatrix<T>>
91where
92    T: nalgebra::RealField,
93    D: nalgebra::Dim,
94    S: nalgebra::RawStorage<T, D, nalgebra::Dyn>,
95{
96    if matrices.is_empty() {
97        anyhow::bail!("empty in concatenate_vertical");
98    }
99
100    let ncols = matrices[0].ncols();
101    assert!(
102        matrices.iter().all(|m| m.ncols() == ncols),
103        "should have the same number of columns"
104    );
105
106    let rows = matrices
107        .iter()
108        .flat_map(|m| m.row_iter().map(|row| row.into_owned()))
109        .collect::<Vec<_>>();
110
111    Ok(DMatrix::from_rows(&rows))
112}
113
114/// concatenate matrices or column vectors horizontally
115pub fn concatenate_horizontal<T, D, S>(
116    matrices: &[Matrix<T, nalgebra::Dyn, D, S>],
117) -> anyhow::Result<DMatrix<T>>
118where
119    T: nalgebra::RealField,
120    D: nalgebra::Dim,
121    S: nalgebra::RawStorage<T, nalgebra::Dyn, D>,
122{
123    if matrices.is_empty() {
124        anyhow::bail!("empty in concatenate_horizontal");
125    }
126
127    let nrows = matrices[0].nrows();
128    assert!(
129        matrices.iter().all(|m| m.nrows() == nrows),
130        "should have the same number of rows"
131    );
132
133    let cols = matrices
134        .iter()
135        .flat_map(|m| m.column_iter().map(|col| col.into_owned()))
136        .collect::<Vec<_>>();
137
138    Ok(DMatrix::from_columns(&cols))
139}
140
141impl<T> AdjustByDivisionOp<nalgebra_sparse::CscMatrix<T>, T> for nalgebra_sparse::CscMatrix<T>
142where
143    T: nalgebra::RealField + Copy + std::iter::Sum<T>,
144{
145    fn adjust_by_division_inplace(&mut self, denom: &nalgebra_sparse::CscMatrix<T>) {
146        self.col_iter_mut()
147            .zip(denom.col_iter())
148            .for_each(|(mut x_j, d_j)| {
149                let dsum = d_j.values().iter().copied().sum::<T>();
150                let xsum = x_j.values().iter().copied().sum::<T>();
151                let scale = if dsum > T::zero() {
152                    xsum / dsum
153                } else {
154                    T::one()
155                };
156
157                let (x_rows, x_values) = x_j.rows_and_values_mut();
158
159                let mut d_j_values = vec![T::zero(); x_rows.len()];
160
161                x_rows.iter().enumerate().for_each(|(idx, &i)| {
162                    if let Some(pos) = d_j.row_indices().iter().position(|&d_i| d_i == i) {
163                        d_j_values[idx] = d_j.values()[pos];
164                    }
165                });
166
167                x_values
168                    .iter_mut()
169                    .zip(d_j_values)
170                    .for_each(|(x_ij, d_ij)| {
171                        if d_ij > T::zero() {
172                            *x_ij /= d_ij * scale;
173                        }
174                    });
175            });
176    }
177
178    fn adjust_by_division_of_selected_inplace(
179        &mut self,
180        denom_db: &nalgebra_sparse::CscMatrix<T>,
181        batches: &[usize],
182    ) {
183        self.col_iter_mut().zip(batches).for_each(|(mut x_j, &b)| {
184            let d_j = denom_db.col(b);
185
186            let dsum = d_j.values().iter().copied().sum::<T>();
187            let xsum = x_j.values().iter().copied().sum::<T>();
188            let scale = if dsum > T::zero() {
189                xsum / dsum
190            } else {
191                T::one()
192            };
193
194            let (x_rows, x_values) = x_j.rows_and_values_mut();
195
196            let mut d_j_values = vec![T::zero(); x_rows.len()];
197
198            x_rows.iter().enumerate().for_each(|(idx, &i)| {
199                if let Some(pos) = d_j.row_indices().iter().position(|&d_i| d_i == i) {
200                    d_j_values[idx] = d_j.values()[pos];
201                }
202            });
203
204            x_values
205                .iter_mut()
206                .zip(d_j_values)
207                .for_each(|(x_ij, d_ij)| {
208                    if d_ij > T::zero() {
209                        *x_ij /= d_ij * scale;
210                    }
211                });
212        });
213    }
214}
215
216/// Self-normalizing Poisson-ratio divide of one sparse column's values, in place.
217///
218/// Models `Y[g] ~ Poisson(X[g]·λ)`: estimate `λ = Σ Y / Σ divisor` over the
219/// column's nonzeros, then replace each `Y[g]` by the residual `Y[g] / (divisor(g)·λ)`
220/// when `divisor(g) > 0` (entries with a non-positive divisor are left unchanged).
221/// `divisor(k)` returns the divisor for the `k`-th value, `k` in `0..vals.len()`.
222///
223/// This is the shared core of the [`AdjustByDivisionOp`] `DMatrix` impls below; it
224/// is also callable directly on any `(values, divisor)` pair without materializing
225/// a matrix — e.g. one cell's counts indexed into a per-pseudobulk fold-factor.
226pub fn adjust_by_poisson_ratio<T, F>(vals: &mut [T], divisor: F)
227where
228    T: nalgebra::RealField + Copy + std::iter::Sum<T>,
229    F: Fn(usize) -> T,
230{
231    let dsum: T = (0..vals.len()).map(&divisor).sum();
232    let xsum: T = vals.iter().copied().sum();
233    let scale = if dsum > T::zero() {
234        xsum / dsum
235    } else {
236        T::one()
237    };
238    for (k, x) in vals.iter_mut().enumerate() {
239        let d = divisor(k);
240        if d > T::zero() {
241            *x /= d * scale;
242        }
243    }
244}
245
246impl<T> AdjustByDivisionOp<nalgebra::DMatrix<T>, T> for nalgebra_sparse::CscMatrix<T>
247where
248    T: nalgebra::RealField + Copy + std::iter::Sum<T>,
249{
250    fn adjust_by_division_of_selected_inplace(
251        &mut self,
252        denom_db: &nalgebra::DMatrix<T>,
253        batches: &[usize],
254    ) {
255        self.col_iter_mut().zip(batches).for_each(|(mut x_j, &b)| {
256            let (rows, vals) = x_j.rows_and_values_mut();
257            adjust_by_poisson_ratio(vals, |k| denom_db[(rows[k], b)]);
258        });
259    }
260
261    fn adjust_by_division_inplace(&mut self, denom: &nalgebra::DMatrix<T>) {
262        self.col_iter_mut()
263            .zip(denom.column_iter())
264            .for_each(|(mut x_j, d_col)| {
265                let (rows, vals) = x_j.rows_and_values_mut();
266                adjust_by_poisson_ratio(vals, |k| d_col[rows[k]]);
267            });
268    }
269}
270
271/// Generate one-hot membership matrix (`row x K`) where the number of
272/// rows corresponds to the length of the membership vector and the
273/// `K` corresponds to the maximum membership value + 1.
274///
275pub fn row_membership_matrix<T>(row_membership: Vec<usize>) -> anyhow::Result<DMatrix<T>>
276where
277    T: nalgebra::RealField + Copy,
278{
279    let kk = match row_membership.iter().max() {
280        Some(&m) => m + 1,
281        _ => 1,
282    };
283
284    let mut ret_dm = DMatrix::zeros(row_membership.len(), kk);
285    let oneval = T::from_f32(1.).ok_or(anyhow::anyhow!("cannot find 1 value"))?;
286    for (i, k) in row_membership.into_iter().enumerate() {
287        ret_dm[(i, k)] += oneval;
288    }
289
290    Ok(ret_dm)
291}
292
293fn csc_euclidean_distance_on_select_columns<T>(
294    lhs: &CscMatrix<T>,
295    rhs: &CscMatrix<T>,
296    select_columns_in_rhs: Option<&[usize]>,
297) -> anyhow::Result<Vec<(usize, usize, T)>>
298where
299    T: nalgebra::RealField + Copy + std::iter::Sum<T>,
300{
301    let all_rhs = (0..rhs.ncols()).collect::<Vec<_>>();
302    let select_columns_in_rhs = select_columns_in_rhs.unwrap_or(all_rhs.as_slice());
303
304    if select_columns_in_rhs.len() != rhs.ncols() {
305        return Err(anyhow::anyhow!(
306            "found mismatches in `select_columns` maps: lhs -> rhs"
307        ));
308    }
309
310    let mut ret = Vec::with_capacity(select_columns_in_rhs.len() * lhs.ncols());
311
312    for (src_pos, src_col) in lhs.col_iter().enumerate() {
313        for &tgt_pos in select_columns_in_rhs {
314            let tgt_col = rhs.col(tgt_pos);
315
316            let nn = src_col.nrows();
317            let denom = T::from_usize(nn).unwrap_or(T::one());
318
319            let idx_src = src_col.row_indices();
320            let idx_tgt = tgt_col.row_indices();
321            let val_src = src_col.values();
322            let val_tgt = tgt_col.values();
323
324            // sum_g (src[g] - tgt[g])^2 / sum_g 1
325            // sum_g tgt[g]^2 + sum_g src[g]^2 - 2 sum_g tgt[g] * src[g]
326            let mut s: usize = 0;
327            let mut t: usize = 0;
328
329            let tgt_sq_sum = val_tgt.iter().map(|&x| x * x).sum::<T>();
330            let src_sq_sum = val_src.iter().map(|&x| x * x).sum::<T>();
331            let mut overlap = T::zero();
332            while s < idx_src.len() && t < idx_tgt.len() {
333                if idx_src[s] == idx_tgt[t] {
334                    overlap += val_src[s] * val_tgt[t];
335                    s += 1;
336                    t += 1;
337                } else if idx_src[s] < idx_tgt[t] {
338                    s += 1;
339                } else {
340                    t += 1;
341                }
342            }
343
344            let dist = ((src_sq_sum + tgt_sq_sum - overlap - overlap) / denom).sqrt();
345
346            ret.push((src_pos, tgt_pos, dist));
347        }
348    }
349
350    Ok(ret)
351}
352
353impl<T> DistanceOps for CscMatrix<T>
354where
355    T: nalgebra::RealField + Copy + std::iter::Sum<T>,
356{
357    type Scalar = T;
358    type Other = CscMatrix<T>;
359
360    fn euclidean_distance(
361        &self,
362        other: &Self::Other,
363    ) -> anyhow::Result<Vec<(usize, usize, Self::Scalar)>> {
364        csc_euclidean_distance_on_select_columns(self, other, None)
365    }
366
367    fn euclidean_distance_on_select_columns(
368        &self,
369        other: &Self::Other,
370        select_columns_in_other: &[usize],
371    ) -> anyhow::Result<Vec<(usize, usize, Self::Scalar)>> {
372        csc_euclidean_distance_on_select_columns(self, other, Some(select_columns_in_other))
373    }
374}
375
376impl<T> MeltOps for DMatrix<T>
377where
378    T: nalgebra::RealField + Copy,
379{
380    type Scalar = T;
381    type Mat = Self;
382    fn melt_with_indexes(&self) -> (Vec<Self::Scalar>, Vec<Vec<usize>>) {
383        let nelem = self.nrows() * self.ncols();
384
385        let mut idx: Vec<Vec<usize>> = vec![Vec::with_capacity(nelem), Vec::with_capacity(nelem)];
386        let mut val: Vec<Self::Scalar> = Vec::with_capacity(nelem);
387
388        // Iterate column-major to match nalgebra's storage layout for cache efficiency
389        for c in 0..self.ncols() {
390            for r in 0..self.nrows() {
391                idx[0].push(r);
392                idx[1].push(c);
393                val.push(self[(r, c)]);
394            }
395        }
396
397        (val, idx)
398    }
399
400    fn melt(&self) -> Vec<Self::Scalar> {
401        let nelem = self.len();
402        let mut val: Vec<Self::Scalar> = Vec::with_capacity(nelem);
403        // Iterate column-major to match nalgebra's storage layout for cache efficiency
404        for c in 0..self.ncols() {
405            for r in 0..self.nrows() {
406                val.push(self[(r, c)]);
407            }
408        }
409        val
410    }
411
412    fn melt_many_with_indexes(
413        &self,
414        others: &[&Self],
415    ) -> (Vec<Vec<Self::Scalar>>, Vec<Vec<usize>>) {
416        let nrows = self.nrows();
417        let ncols = self.ncols();
418        let nelem = nrows * ncols;
419        let n_matrices = 1 + others.len();
420
421        // Pre-allocate all vectors
422        let mut values: Vec<Vec<Self::Scalar>> =
423            (0..n_matrices).map(|_| Vec::with_capacity(nelem)).collect();
424        let mut idx: Vec<Vec<usize>> = vec![Vec::with_capacity(nelem), Vec::with_capacity(nelem)];
425
426        // Single traversal in column-major order for cache efficiency
427        for c in 0..ncols {
428            for r in 0..nrows {
429                idx[0].push(r);
430                idx[1].push(c);
431                values[0].push(self[(r, c)]);
432                for (i, other) in others.iter().enumerate() {
433                    values[i + 1].push(other[(r, c)]);
434                }
435            }
436        }
437
438        (values, idx)
439    }
440}
441
442impl<T> EncodingOps for DMatrix<T>
443where
444    T: nalgebra::RealField + Float,
445    f32: From<T>,
446{
447    type Scalar = T;
448    type Mat = Self;
449
450    fn positional_embedding_columns(&self, emb_dim: usize) -> anyhow::Result<Self::Mat> {
451        let ncols = self.ncols();
452
453        let ncodes_per_col = emb_dim * 2;
454
455        let mut output = Self::zeros(self.nrows(), ncols * ncodes_per_col);
456
457        for j in 0..ncols {
458            let x_j = self.column(j);
459
460            for i in 0..emb_dim {
461                let power = T::from(2.0 * i as f32 / emb_dim as f32).unwrap();
462                let denom = T::from(10000_f32.powf(f32::from(power))).unwrap();
463
464                let column_data = if i % 2 == 0 {
465                    x_j.map(|x_ij| ComplexField::sin(x_ij / denom))
466                } else {
467                    x_j.map(|x_ij| ComplexField::cos(x_ij / denom))
468                };
469
470                // Assign the computed column to the output matrix
471                output
472                    .column_mut(j * ncodes_per_col + i)
473                    .copy_from(&column_data);
474            }
475        }
476        Ok(output)
477    }
478}
479
480impl<T> CompositeOps for DMatrix<T>
481where
482    T: nalgebra::RealField + Copy,
483{
484    type Scalar = T;
485    type Mat = Self;
486    type Other = CscMatrix<T>;
487
488    /// `self[:,col] += other[:,col]`
489    /// * `col` - column index
490    fn add_assign_column(&mut self, other: &Self::Other, j: usize) {
491        debug_assert_eq!(self.nrows(), other.nrows());
492        debug_assert_eq!(self.ncols(), other.ncols());
493        if let Some(x_j) = other.get_col(j) {
494            let vals = x_j.values();
495            let rows = x_j.row_indices();
496            for k in 0..vals.len() {
497                let i = rows[k];
498                let x_ij = vals[k];
499                self[(i, j)] += x_ij;
500            }
501        }
502    }
503
504    /// `self += other`
505    /// * `other` - `CscMatrix`
506    fn add_assign(&mut self, other: &Self::Other) {
507        debug_assert_eq!(self.nrows(), other.nrows());
508        debug_assert_eq!(self.ncols(), other.ncols());
509        for j in 0..other.ncols() {
510            if let Some(x_j) = other.get_col(j) {
511                let vals = x_j.values();
512                let rows = x_j.row_indices();
513                for k in 0..vals.len() {
514                    let i = rows[k];
515                    let x_ij = vals[k];
516                    self[(i, j)] += x_ij;
517                }
518            }
519        }
520    }
521}
522
523impl<T> ConvertMatOps for DMatrix<T>
524where
525    T: nalgebra::RealField + Copy + candle_core::WithDType,
526{
527    type Mat = Self;
528    type Scalar = T;
529
530    fn from_tensor(tensor: &candle_core::Tensor) -> anyhow::Result<Self::Mat> {
531        if tensor.dims().len() != 2 {
532            return Err(anyhow::anyhow!("expected 2D tensor"));
533        }
534
535        let nrows = tensor.dims()[0];
536        let ncols = tensor.dims()[1];
537        let data: Vec<T> = tensor.flatten_all()?.to_vec1()?;
538        Ok(Self::from_row_iterator(nrows, ncols, data.iter().cloned()))
539    }
540
541    fn to_tensor(&self, dev: &candle_core::Device) -> anyhow::Result<candle_core::Tensor> {
542        use candle_core::Tensor;
543        // Note: x.as_slice() will take values in the column-major order
544        // However, Tensor::from_slice will take them in the row-major order
545        let nrow = self.nrows();
546        let ncol = self.ncols();
547        Ok(Tensor::from_slice(self.as_slice(), (ncol, nrow), dev)?.transpose(0, 1)?)
548    }
549}
550
551impl<T> ConvertMatOps for CscMatrix<T>
552where
553    T: nalgebra::RealField + Copy + candle_core::WithDType,
554{
555    type Mat = Self;
556    type Scalar = T;
557    fn from_tensor(tensor: &candle_core::Tensor) -> anyhow::Result<Self::Mat> {
558        if tensor.dims().len() != 2 {
559            return Err(anyhow::anyhow!("expected 2D tensor"));
560        }
561
562        let nrows = tensor.dims()[0];
563        let ncols = tensor.dims()[1];
564        let data: Vec<T> = tensor.flatten_all()?.to_vec1()?;
565
566        let dense_matrix = DMatrix::from_row_iterator(nrows, ncols, data.iter().cloned());
567        let csc_matrix = CscMatrix::from(&dense_matrix);
568
569        Ok(csc_matrix)
570    }
571
572    fn to_tensor(&self, dev: &candle_core::Device) -> anyhow::Result<candle_core::Tensor> {
573        use candle_core::Tensor;
574        let dense_matrix = DMatrix::from(self);
575        let nrow = dense_matrix.nrows();
576        let ncol = dense_matrix.ncols();
577        // Note: x.as_slice() will take values in the column-major order
578        // However, Tensor::from_slice will take them in the row-major order
579        Ok(Tensor::from_slice(dense_matrix.as_slice(), (ncol, nrow), dev)?.transpose(0, 1)?)
580    }
581}
582
583impl<T> SampleOps for DMatrix<T>
584where
585    T: nalgebra::RealField + Float,
586{
587    type Mat = Self;
588    type Scalar = T;
589
590    fn runif(nrow: usize, ncol: usize) -> Self::Mat {
591        Self::runif_seeded(nrow, ncol, entropy_seed())
592    }
593
594    fn rnorm(nrow: usize, ncol: usize) -> Self::Mat {
595        Self::rnorm_seeded(nrow, ncol, entropy_seed())
596    }
597
598    fn rgamma(nrow: usize, ncol: usize, param: (f32, f32)) -> Self::Mat {
599        Self::rgamma_seeded(nrow, ncol, param, entropy_seed())
600    }
601
602    fn runif_seeded(nrow: usize, ncol: usize, seed: u64) -> Self::Mat {
603        let u01 = Uniform::<f32>::new(0., 1.).expect("failed to create uniform distribution");
604        DMatrix::<T>::from_vec(nrow, ncol, collect_seeded(nrow * ncol, u01, seed))
605    }
606
607    fn rnorm_seeded(nrow: usize, ncol: usize, seed: u64) -> Self::Mat {
608        DMatrix::<T>::from_vec(
609            nrow,
610            ncol,
611            collect_seeded(nrow * ncol, StandardNormal, seed),
612        )
613    }
614
615    fn rgamma_seeded(nrow: usize, ncol: usize, param: (f32, f32), seed: u64) -> Self::Mat {
616        let (shape, scale) = param;
617        let pdf = Gamma::new(shape, scale).unwrap();
618        DMatrix::<T>::from_vec(nrow, ncol, collect_seeded(nrow * ncol, pdf, seed))
619    }
620}
621
622impl<T> MatElemOps for CscMatrix<T>
623where
624    T: nalgebra::RealField + Copy,
625{
626    type Mat = Self;
627    type Scalar = T;
628
629    fn log1p_inplace(&mut self) {
630        for x in self.values_mut() {
631            *x = (*x).ln_1p();
632        }
633    }
634
635    fn log1p(&self) -> Self::Mat {
636        let mut ret = self.clone();
637        ret.log1p_inplace();
638        ret
639    }
640}
641
642impl<T> MatOps for CscMatrix<T>
643where
644    T: nalgebra::RealField + Copy,
645{
646    type Mat = Self;
647    type Scalar = T;
648
649    fn normalize_exp_logits_columns_inplace(&mut self) {
650        let ncol = self.ncols();
651
652        for j in 0..ncol {
653            if let Some(log_j) = self.get_col(j) {
654                let mut log_max = log_j.values()[0];
655                for &logx_ij in log_j.values() {
656                    log_max = log_max.min(logx_ij);
657                }
658
659                let mut denom = T::zero();
660                for &logx_ij in log_j.values() {
661                    denom += (logx_ij - log_max).exp();
662                }
663
664                if let Some(mut log_j) = self.get_col_mut(j) {
665                    for x_ij in log_j.values_mut() {
666                        *x_ij = (*x_ij - log_max).exp() / denom;
667                    }
668                }
669            }
670        }
671    }
672
673    fn normalize_exp_logits_columns(&self) -> Self::Mat {
674        let mut ret = self.clone();
675        ret.normalize_exp_logits_columns_inplace();
676        ret
677    }
678
679    fn log_softmax_columns_inplace(&mut self) {
680        for j in 0..self.ncols() {
681            let lse = {
682                let Some(col) = self.get_col(j) else { continue };
683                let vals = col.values();
684                if vals.is_empty() {
685                    continue;
686                }
687                let mut log_max = vals[0];
688                for &v in vals {
689                    if v > log_max {
690                        log_max = v;
691                    }
692                }
693                let mut denom = T::zero();
694                for &v in vals {
695                    denom += (v - log_max).exp();
696                }
697                log_max + denom.ln()
698            };
699            if let Some(mut col) = self.get_col_mut(j) {
700                for x in col.values_mut() {
701                    *x -= lse;
702                }
703            }
704        }
705    }
706
707    fn log_softmax_columns(&self) -> Self::Mat {
708        let mut ret = self.clone();
709        ret.log_softmax_columns_inplace();
710        ret
711    }
712
713    fn sum_to_one_columns_inplace(&mut self) {
714        let ncol = self.ncols();
715
716        for j in 0..ncol {
717            if let Some(x_j) = self.get_col(j) {
718                let mut denom = T::zero();
719                for &x_ij in x_j.values() {
720                    denom += x_ij;
721                }
722                if let Some(mut x_j) = self.get_col_mut(j) {
723                    if denom > T::zero() {
724                        for x_ij in x_j.values_mut() {
725                            *x_ij /= denom;
726                        }
727                    }
728                }
729            }
730        }
731    }
732
733    fn sum_to_one_columns(&self) -> Self::Mat {
734        let mut ret = self.clone();
735        ret.sum_to_one_columns_inplace();
736        ret
737    }
738
739    fn sum_to_one_rows(&self) -> Self::Mat {
740        let mut ret = self.clone();
741        ret.sum_to_one_rows_inplace();
742        ret
743    }
744
745    fn sum_to_one_rows_inplace(&mut self) {
746        let nrow = self.nrows();
747        let mut denom = vec![T::zero(); nrow];
748
749        for col in self.col_iter() {
750            col.row_indices()
751                .iter()
752                .zip(col.values().iter())
753                .for_each(|(&i, &x_ij)| {
754                    denom[i] += x_ij;
755                });
756        }
757
758        for mut col in self.col_iter_mut() {
759            let (row_indices, values) = col.rows_and_values_mut();
760            row_indices.iter().zip(values).for_each(|(&i, x_ij)| {
761                *x_ij /= denom[i];
762            });
763        }
764    }
765
766    fn normalize_columns_inplace(&mut self) {
767        let ncol = self.ncols();
768
769        for j in 0..ncol {
770            if let Some(x_j) = self.get_col(j) {
771                let mut denom = T::zero();
772                for &x_ij in x_j.values() {
773                    denom += x_ij * x_ij;
774                }
775                denom = denom.sqrt().max(T::from_f64(1e-8).unwrap());
776
777                if let Some(mut x_j) = self.get_col_mut(j) {
778                    for x_ij in x_j.values_mut() {
779                        *x_ij /= denom;
780                    }
781                }
782            }
783        }
784    }
785
786    fn normalize_columns(&self) -> Self::Mat {
787        let mut ret = self.clone();
788        ret.normalize_columns_inplace();
789        ret
790    }
791
792    fn scale_columns_inplace(&mut self) {
793        let ncol = self.ncols();
794
795        for j in 0..ncol {
796            if let Some(x_j) = self.get_col(j) {
797                let mut s0 = T::zero();
798                let mut s1 = T::zero();
799                let mut s2 = T::zero();
800
801                for &x_ij in x_j.values() {
802                    s0 += T::one();
803                    s1 += x_ij;
804                    s2 += x_ij * x_ij;
805                }
806
807                let mu = s1 / s0.max(T::one());
808                let sig = (s2 / s0.max(T::one()) - mu * mu).sqrt();
809
810                if let Some(mut x_j) = self.get_col_mut(j) {
811                    if sig > T::zero() {
812                        for x_ij in x_j.values_mut() {
813                            *x_ij = (*x_ij - mu) / sig;
814                        }
815                    } else {
816                        for x_ij in x_j.values_mut() {
817                            *x_ij -= mu;
818                        }
819                    }
820                }
821            }
822        }
823    }
824
825    fn scale_rows_inplace(&mut self) {
826        let nrow = self.nrows();
827        let ncol = self.ncols();
828        let mut s0 = vec![T::zero(); nrow];
829        let mut s1 = vec![T::zero(); nrow];
830        let mut s2 = vec![T::zero(); nrow];
831
832        for j in 0..ncol {
833            if let Some(x_j) = self.get_col(j) {
834                for (&x_ij, &i) in x_j.values().iter().zip(x_j.row_indices()) {
835                    s0[i] += T::one();
836                    s1[i] += x_ij;
837                    s2[i] += x_ij * x_ij;
838                }
839            }
840        }
841
842        let mu = s1
843            .into_iter()
844            .zip(s0.iter())
845            .map(|(x, &n)| x / n.max(T::one()))
846            .collect::<Vec<_>>();
847
848        let sig = s2
849            .into_iter()
850            .zip(mu.iter())
851            .zip(s0.iter())
852            .map(|((s2, &mu), &s0)| (s2 / s0.max(T::one()) - mu * mu).sqrt())
853            .collect::<Vec<_>>();
854
855        for j in 0..ncol {
856            if let Some(mut x_j) = self.get_col_mut(j) {
857                let (rows, values) = x_j.rows_and_values_mut();
858
859                for (&i, x_ij) in rows.iter().zip(values) {
860                    let mu_i = mu[i];
861                    let sig_i = sig[i];
862                    if sig_i > T::zero() {
863                        *x_ij = (*x_ij - mu_i) / sig_i;
864                    } else {
865                        *x_ij -= mu_i;
866                    }
867                }
868            }
869        }
870    }
871
872    fn scale_columns(&self) -> Self::Mat {
873        let mut ret = self.clone();
874        ret.scale_columns_inplace();
875        ret
876    }
877
878    fn scale_rows(&self) -> Self::Mat {
879        let mut ret = self.clone();
880        ret.scale_rows_inplace();
881        ret
882    }
883
884    fn centre_columns_inplace(&mut self) {
885        let ncol = self.ncols();
886
887        for j in 0..ncol {
888            if let Some(x_j) = self.get_col(j) {
889                let mut s0 = T::zero();
890                let mut s1 = T::zero();
891
892                for &x_ij in x_j.values() {
893                    s0 += T::one();
894                    s1 += x_ij;
895                }
896
897                let mu = s1 / s0.max(T::one());
898
899                if let Some(mut x_j) = self.get_col_mut(j) {
900                    for x_ij in x_j.values_mut() {
901                        *x_ij -= mu;
902                    }
903                }
904            }
905        }
906    }
907
908    fn centre_columns(&self) -> Self::Mat {
909        let mut ret = self.clone();
910        ret.centre_columns_inplace();
911        ret
912    }
913}
914
915impl<T> MatOps for DMatrix<T>
916where
917    T: nalgebra::RealField,
918{
919    type Mat = Self;
920    type Scalar = T;
921
922    fn normalize_exp_logits_columns_inplace(&mut self) {
923        for mut x_j in self.column_iter_mut() {
924            let log_max = x_j.max();
925            let denom = x_j.map(|l| (l - log_max.clone()).exp()).sum();
926            x_j.iter_mut()
927                .for_each(|l| *l = (l.clone() - log_max.clone()).exp() / denom.clone());
928        }
929    }
930
931    fn normalize_exp_logits_columns(&self) -> Self::Mat {
932        let mut ret = self.clone();
933        ret.normalize_exp_logits_columns_inplace();
934        ret
935    }
936
937    fn log_softmax_columns_inplace(&mut self) {
938        for mut x_j in self.column_iter_mut() {
939            let log_max = x_j.max();
940            let denom = x_j.map(|l| (l - log_max.clone()).exp()).sum();
941            let lse = log_max + denom.ln();
942            x_j.iter_mut().for_each(|l| *l = l.clone() - lse.clone());
943        }
944    }
945
946    fn log_softmax_columns(&self) -> Self::Mat {
947        let mut ret = self.clone();
948        ret.log_softmax_columns_inplace();
949        ret
950    }
951
952    fn sum_to_one_columns_inplace(&mut self) {
953        self.column_iter_mut()
954            .for_each(|mut c| c.unscale_mut(c.sum()));
955    }
956
957    fn sum_to_one_columns(&self) -> Self::Mat {
958        let mut ret = self.clone();
959        ret.sum_to_one_columns_inplace();
960        ret
961    }
962
963    fn sum_to_one_rows_inplace(&mut self) {
964        self.row_iter_mut().for_each(|mut r| r.unscale_mut(r.sum()));
965    }
966
967    fn sum_to_one_rows(&self) -> Self::Mat {
968        let mut ret = self.clone();
969        ret.sum_to_one_rows_inplace();
970        ret
971    }
972
973    fn normalize_columns_inplace(&mut self) {
974        for mut xx_j in self.column_iter_mut() {
975            let denom = xx_j.norm().max(T::from_f64(1e-8).unwrap());
976            xx_j /= denom;
977        }
978    }
979
980    fn normalize_columns(&self) -> Self::Mat {
981        let mut ret = self.clone();
982        ret.normalize_columns_inplace();
983        ret
984    }
985
986    fn scale_columns_inplace(&mut self) {
987        for mut xx_j in self.column_iter_mut() {
988            let mu = xx_j.mean();
989            let sig = xx_j.variance().sqrt();
990            xx_j.add_scalar_mut(-mu);
991            if sig > T::zero() {
992                xx_j /= sig;
993            }
994        }
995    }
996
997    fn scale_rows_inplace(&mut self) {
998        for mut xx_i in self.row_iter_mut() {
999            let mu = xx_i.mean();
1000            let sig = xx_i.variance().sqrt();
1001            xx_i.add_scalar_mut(-mu);
1002            if sig > T::zero() {
1003                xx_i /= sig;
1004            }
1005        }
1006    }
1007
1008    fn scale_columns(&self) -> Self::Mat {
1009        let mut ret = self.clone();
1010        ret.scale_columns_inplace();
1011        ret
1012    }
1013
1014    fn scale_rows(&self) -> Self::Mat {
1015        let mut ret = self.clone();
1016        ret.scale_rows_inplace();
1017        ret
1018    }
1019
1020    fn centre_columns_inplace(&mut self) {
1021        for mut xx_j in self.column_iter_mut() {
1022            let mu = xx_j.mean();
1023            xx_j.add_scalar_mut(-mu);
1024        }
1025    }
1026
1027    fn centre_columns(&self) -> Self::Mat {
1028        let mut ret = self.clone();
1029        ret.centre_columns_inplace();
1030        ret
1031    }
1032}
1033
1034/// TF-IDF (Term Frequency–Inverse Document Frequency)
1035///
1036/// A numerical statistic reflecting how important a word (term) is to a document
1037/// in a collection or corpus. (Wikipedia)
1038///
1039/// Given a term-document matrix where rows are terms and columns are documents:
1040///
1041/// - **TF (Term Frequency)**: The raw count or frequency of term t in document d.
1042///   Here we use the matrix values directly as TF.
1043///
1044/// - **IDF (Inverse Document Frequency)**: A measure of how much information
1045///   the term provides, i.e., how common or rare it is across all documents.
1046///
1047///   IDF(t) = log(N / df(t))
1048///
1049///   where N = total number of documents, df(t) = number of documents containing term t.
1050///   We use log(N / (df(t) + 1)) to avoid division by zero.
1051///
1052/// - **TF-IDF(t, d) = TF(t, d) × IDF(t)**
1053///
1054/// Terms appearing in many documents get lower weight; terms specific to few
1055/// documents get higher weight.
1056impl<T> TfIdfOps for DMatrix<T>
1057where
1058    T: nalgebra::RealField,
1059{
1060    type Mat = Self;
1061
1062    fn tfidf(&self) -> Self::Mat {
1063        let (nrows, ncols) = self.shape();
1064        let n_docs = T::from_usize(ncols).unwrap();
1065        let mut result = self.clone();
1066
1067        for i in 0..nrows {
1068            // df(t) = document frequency = number of documents containing term t
1069            let df = (0..ncols).filter(|&j| self[(i, j)] != T::zero()).count();
1070            let df_t = T::from_usize(df).unwrap();
1071            // IDF(t) = log(N / (df(t) + 1))
1072            let idf = (n_docs.clone() / (df_t + T::one())).ln();
1073            // TF-IDF(t, d) = TF(t, d) × IDF(t)
1074            for j in 0..ncols {
1075                result[(i, j)] = result[(i, j)].clone() * idf.clone();
1076            }
1077        }
1078        result
1079    }
1080
1081    fn tfidf_normalize_columns(&self) -> Self::Mat {
1082        let mut result = self.tfidf();
1083        result.normalize_columns_inplace();
1084        result
1085    }
1086}
1087
1088////////////////////////////////////////
1089// Input and output in triplet format //
1090////////////////////////////////////////
1091
1092impl<T> MatTriplets for DMatrix<T>
1093where
1094    T: nalgebra::RealField + Float,
1095{
1096    type Mat = Self;
1097    type Scalar = T;
1098
1099    fn from_nonzero_triplets<I>(
1100        nrow: usize,
1101        ncol: usize,
1102        triplets: &[(I, I, T)],
1103    ) -> anyhow::Result<Self::Mat>
1104    where
1105        I: TryInto<usize> + Copy,
1106        <I as TryInto<usize>>::Error: std::fmt::Debug,
1107    {
1108        let mut data = vec![T::zero(); ncol * nrow];
1109        for &(ii, jj, x_ij) in triplets {
1110            let ii: usize = ii.try_into().expect("failed to convert index ii");
1111            let jj: usize = jj.try_into().expect("failed to convert index jj");
1112            data[ii * ncol + jj] = x_ij;
1113        }
1114        Ok(DMatrix::from_row_slice(nrow, ncol, &data))
1115    }
1116
1117    fn to_nonzero_triplets(&self) -> anyhow::Result<NRowNColTriplets<Self::Scalar>> {
1118        if let Some(eps) = T::from(1e-6) {
1119            let nrow = self.nrows();
1120            let ncol = self.ncols();
1121            let mut triplets = vec![];
1122            for j in 0..ncol {
1123                for i in 0..nrow {
1124                    let x_ij = &self[(i, j)];
1125                    if x_ij.abs() > eps {
1126                        triplets.push((i, j, *x_ij));
1127                    }
1128                }
1129            }
1130            Ok(NRowNColTriplets {
1131                nrow,
1132                ncol,
1133                triplets,
1134            })
1135        } else {
1136            anyhow::bail!("eps is not defined")
1137        }
1138    }
1139}
1140
1141impl<T> MatTriplets for CsrMatrix<T>
1142where
1143    T: nalgebra::RealField + Float,
1144{
1145    type Mat = Self;
1146    type Scalar = T;
1147
1148    fn from_nonzero_triplets<I>(
1149        nrow: usize,
1150        ncol: usize,
1151        triplets: &[(I, I, Self::Scalar)],
1152    ) -> anyhow::Result<Self::Mat>
1153    where
1154        I: TryInto<usize> + Copy,
1155        <I as TryInto<usize>>::Error: std::fmt::Debug,
1156    {
1157        let mut coo = CooMatrix::<T>::new(nrow, ncol);
1158        for &(ii, jj, x_ij) in triplets {
1159            let ii: usize = ii.try_into().expect("failed to convert index ii");
1160            let jj: usize = jj.try_into().expect("failed to convert index jj");
1161            coo.push(ii, jj, x_ij);
1162        }
1163        Ok(CsrMatrix::from(&coo))
1164    }
1165    fn to_nonzero_triplets(&self) -> anyhow::Result<NRowNColTriplets<Self::Scalar>> {
1166        if let Some(eps) = T::from(1e-6) {
1167            let nrow = self.nrows();
1168            let ncol = self.ncols();
1169
1170            let mut triplets = Vec::new();
1171
1172            for i in 0..nrow {
1173                if let Some(x_i) = self.get_row(i) {
1174                    let cols = x_i.col_indices();
1175                    let vals = x_i.values();
1176                    for k in 0..cols.len() {
1177                        let j = cols[k];
1178                        let x_ij = vals[k];
1179                        if x_ij > eps {
1180                            triplets.push((i, j, x_ij))
1181                        };
1182                    }
1183                }
1184            }
1185
1186            Ok(NRowNColTriplets {
1187                nrow,
1188                ncol,
1189                triplets,
1190            })
1191        } else {
1192            anyhow::bail!("eps is not defined")
1193        }
1194    }
1195}
1196
1197impl<T> MatTriplets for CscMatrix<T>
1198where
1199    T: nalgebra::RealField + Float,
1200{
1201    type Mat = Self;
1202    type Scalar = T;
1203
1204    fn from_nonzero_triplets<I>(
1205        nrow: usize,
1206        ncol: usize,
1207        triplets: &[(I, I, Self::Scalar)],
1208    ) -> anyhow::Result<Self::Mat>
1209    where
1210        I: TryInto<usize> + Copy,
1211        <I as TryInto<usize>>::Error: std::fmt::Debug,
1212    {
1213        let mut coo = CooMatrix::<T>::new(nrow, ncol);
1214        for &(ii, jj, x_ij) in triplets {
1215            let ii: usize = ii.try_into().expect("failed to convert index ii");
1216            let jj: usize = jj.try_into().expect("failed to convert index jj");
1217            coo.push(ii, jj, x_ij);
1218        }
1219        Ok(CscMatrix::from(&coo))
1220    }
1221
1222    fn to_nonzero_triplets(&self) -> anyhow::Result<NRowNColTriplets<Self::Scalar>> {
1223        if let Some(eps) = T::from(1e-6) {
1224            let nrow = self.nrows();
1225            let ncol = self.ncols();
1226
1227            let mut triplets = Vec::new();
1228            for j in 0..ncol {
1229                if let Some(x_j) = self.get_col(j) {
1230                    let rows = x_j.row_indices();
1231                    let vals = x_j.values();
1232                    for k in 0..rows.len() {
1233                        let i = rows[k];
1234                        let x_ij = vals[k];
1235                        if x_ij > eps {
1236                            triplets.push((i, j, x_ij))
1237                        }
1238                    }
1239                }
1240            }
1241
1242            Ok(NRowNColTriplets {
1243                nrow,
1244                ncol,
1245                triplets,
1246            })
1247        } else {
1248            anyhow::bail!("eps is not defined")
1249        }
1250    }
1251}
1252
1253impl<T> CandleDataLoaderOps for DMatrix<T>
1254where
1255    T: nalgebra::RealField + Copy + candle_core::WithDType,
1256{
1257    type Scalar = T;
1258    type Mat = Self;
1259
1260    // fn transpose(&self) -> Self::Mat {
1261    //     self.transpose()
1262    // }
1263
1264    fn rows_to_tensor_vec(&self) -> Vec<candle_core::Tensor> {
1265        let mut idx_data = self
1266            .row_iter()
1267            .enumerate()
1268            .par_bridge()
1269            .map(|(i, row)| {
1270                let mut v =
1271                    candle_core::Tensor::from_iter(row.iter().copied(), &candle_core::Device::Cpu)
1272                        .expect("failed to create tensor");
1273                v = v.reshape((1, row.len())).expect("failed to reshape");
1274                (i, v)
1275            })
1276            .collect::<Vec<_>>();
1277
1278        idx_data.sort_by_key(|(i, _)| *i);
1279        idx_data.into_iter().map(|(_, t)| t).collect()
1280    }
1281
1282    fn data_shape(&self) -> (usize, usize) {
1283        (self.nrows(), self.ncols())
1284    }
1285
1286    fn row_to_f32_vec(&self, i: usize) -> Vec<f32> {
1287        let row_data: Vec<T> = self.row(i).iter().copied().collect();
1288        let t = candle_core::Tensor::from_vec(row_data, self.ncols(), &candle_core::Device::Cpu)
1289            .expect("tensor from row");
1290        t.to_dtype(candle_core::DType::F32)
1291            .expect("to f32")
1292            .to_vec1::<f32>()
1293            .expect("to vec")
1294    }
1295}