rstats/
triangmat.rs

1use crate::*; // MStats, MinMax, MutVecg, Stats, VecVec };
2pub use indxvec::{Indices, Printing, Vecops};
3
4/// Meanings of 'kind' field. Note that 'Upper Symmetric' would represent the same full matrix as
5/// 'Lower Symmetric', so it is not used (lower symmetric matrix is never transposed)
6const KINDS: [&str; 5] = [
7    "Lower",
8    "Lower antisymmetric",
9    "Lower symmetric",
10    "Upper",
11    "Upper antisymmetric",
12];
13
14/// Translates single subscript to .data to a pair of  
15/// (row,column) coordinates within a lower/upper triangular matrix.
16/// Enables memory efficient representation of triangular matrices as one flat vector.
17fn rowcol(s: usize) -> (usize, usize) {
18        let row = ((((8 * s + 1) as f64).sqrt() - 1.) / 2.) as usize; // cast truncates like .floor()
19        let column = s - row * (row + 1) / 2; // subtracting the last triangular number (of whole rows)
20        (row, column)
21}
22
23/// Display implementation for TriangMat
24impl std::fmt::Display for TriangMat {
25    fn fmt<'a>(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
26        let n = Self::dim(self);  
27        write!(
28            f,
29            "{} ({n}x{n}) triangular matrix\n{}",
30            KINDS[self.kind],
31            (0..n).map(|r| self.row(r)).collect::<Vec<Vec<f64>>>().to_str()
32        )
33    }
34}
35
36/// Implementation of associated functions for struct TriangleMat.
37/// End type is f64, as triangular matrices will be mostly computed
38impl TriangMat {
39    /// Length of the data vec
40    pub fn len(&self) -> usize {
41        self.data.len()
42    }
43    /// Dimension of the implied full (square) matrix
44    /// from the quadratic equation: `n^2 + n - 2l = 0`
45    pub fn dim(&self) -> usize {
46        ((((8 * self.data.len() + 1) as f64).sqrt() - 1.) / 2.) as usize
47    }
48    /// Empty TriangMat test
49    pub fn is_empty(&self) -> bool {
50        self.data.is_empty()
51    }
52    /// Squared euclidian vector magnitude (norm) of the data vector
53    pub fn magsq(&self) -> f64 {
54        self.data.vmagsq()
55    }
56    /// Sum of the elements:  
57    /// when applied to the wedge product **a∧b**, returns det(**a,b**)
58    pub fn sum(&self) -> f64 {
59        self.data.iter().sum()
60    }
61    /// Diagonal elements
62    pub fn diagonal(&self) -> Vec<f64> {
63        let mut next = 0_usize;
64        let mut skip = 1;
65        let dat = &self.data;
66        let mut diagonal = Vec::with_capacity(self.dim());
67        while next < dat.len() {
68            diagonal.push(dat[next]);
69            skip += 1;
70            next += skip;
71        }
72        diagonal
73    }
74    /// Determinant of C = LL' is the square of the product of the diagonal elements of L
75    pub fn determinant(&self) -> f64 {
76        let product = self.diagonal().iter().product::<f64>();
77        product * product
78    }
79    /// New unit (symmetric) TriangMat matrix (data size `n*(n+1)/2`)
80    pub fn unit(n: usize) -> Self {
81        let mut data = Vec::new();
82        for i in 0..n {
83            // fill with zeros before the diagonal
84            for _ in 0..i {
85                data.push(0_f64)
86            }
87            data.push(1_f64);
88        }
89        TriangMat { kind: 2, data }
90    }
91
92    /// Projects to a smaller TriangMat of the same kind,
93    /// in a subspace given by a subspace index. 
94    /// Deletes all the rows and columns of the other dimensions.
95    /// The kept ones retain their original order.
96    pub fn project(&self, index: &[usize]) -> Self {
97        let mut res = Vec::with_capacity(sumn(index.len()));
98        for &rownum in index { 
99            let row = self.row(rownum);
100            for &colnum in index {
101                if colnum > rownum { break; };
102                res.push(row[colnum]);
103            };
104        }; 
105        TriangMat {
106            kind: self.kind,
107            data: res,
108        }
109    }
110
111    /// Copy one raw data row from TriangMat
112    /// To interpret the kind (plain, symmetric, assymetric, transposed),
113    /// use `realrow,realcolumn,to_full`
114    pub fn row(&self, r: usize) -> Vec<f64> {
115        let idx = sumn(r);
116        let Some(slice) = self.data.get(idx..idx + r + 1) else {
117            eprintln!("row called with invalid {r}, returned empty Vec");
118            return Vec::new();
119        };
120        slice.to_vec()
121    }    
122    /// Trivial implicit transposition of a mutable TriangMat.
123    /// The untransposed matrix is gone.
124    /// To keep the original, use `clone_transpose` below
125    pub fn transpose(&mut self) {
126        if self.kind != 2 {
127            self.kind += 3;
128            self.kind %= 6;
129        }
130    }
131    /// Implicit transposition of a cloned TriangMat. 
132    pub fn clone_transpose(&self) -> TriangMat { 
133        TriangMat { 
134            kind: if self.kind == 2 { self.kind } else { (self.kind + 3) % 6 }, 
135            data:self.data.clone()
136        } 
137    } 
138    /// One (short) row of a triangular matrix,  
139    /// assumed to be zero filled at the end.
140    /// When the matrix is transposed (kind>2),  
141    /// this will be a (short) column,
142    /// assumed to be zero filled upfront.
143    pub fn realrow(&self, r: usize) -> Vec<f64> {
144        let idx = sumn(r);
145        let Some(todiag) = self.data.get(idx..idx + r + 1) else {
146            eprintln!("fullrow called with invalid {r}, returned empty Vec");
147            return Vec::new();
148        };
149        let mut rowvec = todiag.to_vec();
150        // continue down from the diagonal along its column
151        match self.kind % 3 {
152            // symmetric
153            2 => {
154                for row in r + 1..self.dim() {
155                    rowvec.push(self.data[sumn(row) + r]);
156                }
157            }
158            // antisymmetric
159            1 => {
160                for row in r + 1..self.dim() {
161                    rowvec.push(-self.data[sumn(row) + r]);
162                }
163            }
164            // neither = plain
165            _ => (), // rowvec.resize(self.dim(), 0_f64),
166        };
167        rowvec
168    }
169    /// One (short) column of a triangular matrix,
170    /// assumed to be zero filled upfront.
171    /// When the matrix is transposed (kind>2),  
172    /// this will be a (short) row,
173    /// assumed to be zero filled at the end.
174    pub fn realcolumn(&self, r: usize) -> Vec<f64> {
175        let idx = sumn(r);
176        // reflect the corresponding row up to diagonal
177        let mut columnvec = match self.kind % 3 {
178            // symmetric
179            2 => self
180                .data
181                .iter()
182                .skip(idx)
183                .take(r)
184                .copied()
185                .collect::<Vec<f64>>(),
186            // antisymmetric
187            1 => self
188                .data
189                .iter()
190                .skip(idx)
191                .take(r)
192                .map(|&dataitem| -dataitem)
193                .collect::<Vec<f64>>(),
194            // neither = plain, fill with zeroes
195            _ => Vec::with_capacity(self.dim()), // vec![0_f64; r]
196        };
197        // now add the column starting below the diagonal
198        for row in r..self.dim() {
199            columnvec.push(self.data[sumn(row) + r]);
200        }
201        columnvec
202    }
203    /// Unpacks all kinds of TriangMat to equivalent full matrix form
204    /// For multiplications, use `rmultv,lmultv,mult` instead, to save this unpacking.
205    pub fn to_full(&self) -> Vec<Vec<f64>> {
206        let n = self.dim();
207        if self.kind > 2 {
208            // transpose
209            (0..self.dim())
210                .map(|rownum| {
211                    let mut column = vec![0_f64; rownum]; // fill zeroes
212                    column.append(&mut self.realcolumn(rownum));
213                    column
214                })
215                .collect::<Vec<Vec<f64>>>()
216        } else {
217            (0..self.dim())
218                .map(|rownum| {
219                    let mut shortrow = self.realrow(rownum);
220                    shortrow.resize(n, 0_f64); // fill zeroes
221                    shortrow
222                })
223                .collect::<Vec<Vec<f64>>>()
224        }
225    }
226    /// Postmultiply row vector v by triangular matrix `self`.
227    /// When a column of self is shorter, it is as if padded with zeroes upfront.
228    /// When v is shorter, it is as if padded with zeroes at the end.
229    pub fn rmultv<U>(&self, v: &[U]) -> Vec<f64>
230    where
231        U: Copy + PartialOrd + std::fmt::Display,
232        f64: From<U>,
233    {
234        if self.kind > 2 {
235            // transpose
236            (0..self.dim())
237                .map(|rownum| self.realrow(rownum).dotp(v))
238                .collect::<Vec<f64>>()
239        } else {
240            (0..self.dim())
241                .map(|rownum| v.dotp(&self.realcolumn(rownum)))
242                .collect::<Vec<f64>>()
243        }
244    }
245    /// Premultiply column vector v by triangular matrix `self`.
246    /// When a row of self is shorter, it is as if padded with zeroes at the end.
247    /// When v is shorter, it is as if padded with zeroes upfront.
248    /// The output is (assumed to be) a column.
249    pub fn lmultv<U>(&self, v: &[U]) -> Vec<f64>
250    where
251        U: Copy + PartialOrd + std::fmt::Display,
252        f64: From<U>,
253    {
254        if self.kind > 2 {
255            // transpose
256            (0..self.dim())
257                .map(|rownum| v.dotp(&self.realcolumn(rownum)))
258                .collect::<Vec<f64>>()
259        } else {
260            (0..self.dim())
261                .map(|rownum| self.realrow(rownum).dotp(v))
262                .collect::<Vec<f64>>()
263        }
264    }
265    /// One element of a product matrix, used by `mult`
266    /// given its precomputed (short) row/column vectors
267    /// self is used here only to test its `kind`
268    fn dotmult(&self, selfvec: &[f64], otvec: &[f64], otherkind: usize) -> f64 {
269        if self.kind > 2 {
270            if otherkind > 2 {
271                otvec.dotp(selfvec)
272            } else if selfvec.len() > otvec.len() {
273                selfvec.dotp(otvec)
274            } else {
275                otvec.dotp(selfvec)
276            }
277        } else if otherkind > 2 {
278            if selfvec.len() > otvec.len() {
279                otvec.dotp(selfvec)
280            } else {
281                selfvec.dotp(otvec)
282            }
283        } else {
284            selfvec.dotp(otvec)
285        }
286    }
287    /// General multiplication of two triangular matrices (of any kind).
288    /// The triangular matrices are not expanded and
289    /// incomplete rows/columns are not even padded (very effient).
290    pub fn mult(&self, other: &Self) -> Vec<Vec<f64>> {
291        (0..self.dim())
292            .map(|rownum| {
293                let selfvec = if self.kind > 2 {
294                    self.realcolumn(rownum)
295                } else {
296                    self.realrow(rownum)
297                };
298                (0..other.dim())
299                    .map(|colnum| {
300                        let otvec = if other.kind > 2 {
301                            other.realrow(colnum)
302                        } else {
303                            other.realcolumn(colnum)
304                        };
305                        self.dotmult(&selfvec, &otvec, other.kind)
306                    })
307                    .collect::<Vec<f64>>()
308            })
309            .collect::<Vec<Vec<f64>>>()
310    }
311    /// Efficient Cholesky-Banachiewicz matrix decomposition into `LL'`,
312    /// where L is the returned lower triangular matrix and L' its upper triangular transpose.
313    /// Takes a positive definite TriangMat matrix,
314    /// such as a covariance matrix produced by `covar`.
315    /// The computations are all done in the compact form,
316    /// making this implementation memory efficient for large (symmetric) matrices.
317    /// Reports errors if the input expectations are not satisfied.
318    pub fn cholesky(&self) -> Result<Self, RE> {
319        let sl = self.data.len();
320        // input not long enough to compute anything
321        if sl < 3 {
322            return nodata_error("cholesky needs at least 3x3 TriangMat");
323        };
324        // n is the dimension of the implied square matrix.
325        // It is obtained by solving a quadratic equation in rowcol()
326        let (n, c) = rowcol(sl);
327        // if the input is not a triangular number, then it is of the wrong size
328        if c != 0 {
329            return data_error("cholesky needs a triangular matrix");
330        };
331        let mut res = vec![0.0; sl]; // result L is of the same size as the input
332        for i in 0..n {
333            let isub = i * (i + 1) / 2; // matrix row index to the compact vector index
334            for j in 0..(i + 1) {
335                // i+1 to include the diagonal
336                let jsub = j * (j + 1) / 2; // matrix column index to the compact vector index
337                let mut sum = 0.0;
338                for k in 0..j {
339                    sum += res[isub + k] * res[jsub + k];
340                }
341                let dif = self.data[isub + j] - sum;
342                res[isub + j] = if i == j {
343                    // diagonal elements
344                    // dif <= 0 means that the input matrix is not positive definite,
345                    // or is ill-conditioned, so we return ArithError
346                    if dif <= 0_f64 {
347                        return arith_error("cholesky matrix is not positive definite");
348                    };
349                    dif.sqrt()
350                }
351                // passed, so enter real non-zero square root
352                else {
353                    dif / res[jsub + j]
354                };
355            }
356        }
357        Ok(TriangMat { kind: 0, data: res })
358    }
359
360    /// Mahalanobis scaled magnitude m(d) of a (column) vector d.
361    /// Self is the decomposed lower triangular matrix L, as returned by `cholesky`
362    /// decomposition of covariance/comediance positive definite matrix: C = LL',
363    /// where ' denotes transpose. Mahalanobis distance is defined as:    
364    /// `m(d) = sqrt(d'inv(C)d) = sqrt(d'inv(LL')d) = sqrt(d'inv(L')inv(L)d)`,
365    /// where `inv()` denotes matrix inverse, which is never explicitly computed.  
366    /// Let  `x = inv(L)d` ( and therefore also  `x' = d'inv(L')` ).
367    /// Substituting x into the above definition: `m(d) = sqrt(x'x) = |x|.  
368    /// We obtain x by setting Lx = d and solving by forward substitution.
369    /// All the calculations are done in the compact triangular form.
370    pub fn mahalanobis<U>(&self, d: &[U]) -> Result<f64, RE>
371    where
372        U: Copy + PartialOrd + std::fmt::Display,
373        f64: From<U>,
374    {
375        Ok(self.forward_substitute(d)?.vmag())
376    }
377
378    /// Solves for x the system of linear equations Lx = b,
379    /// where L (self) is a lower triangular matrix.   
380    pub fn forward_substitute<U>(&self, b: &[U]) -> Result<Vec<f64>, RE>
381    where
382        U: Copy + PartialOrd + std::fmt::Display,
383        f64: From<U>
384    {
385        if self.kind != 0 { return data_error("forward-substitute expects plain lower kind"); };
386        let data = &self.data;
387        if data.len() < 3 {
388            return nodata_error("forward-substitute needs at least three items");
389        };
390        // 2d matrix dimension
391        let n = self.dim();
392        // dimensions/lengths mismatch
393        if n != b.len() {
394            return data_error("forward_substitute mismatch of self and b dimension");
395        };
396        let mut res: Vec<f64> = Vec::with_capacity(n); // result of the same size as b
397        if self.data[0].is_normal() { res.push( f64::from(b[0])/self.data[0] ) } 
398        else { return arith_error("forward-substitute given underconstrained system"); };
399        for (row, &b_component) in b.iter().enumerate().take(n).skip(1) {
400            let rowoffset = sumn(row);
401            let sumtodiag = res.iter().enumerate().map(|(column, res_component)|
402                self.data[rowoffset + column] * res_component).sum::<f64>();            
403            if self.data[rowoffset + row].is_normal() { 
404                res.push((f64::from(b_component) - sumtodiag) / self.data[rowoffset + row]); }
405                else { return arith_error("forward-substitute given underconstrained system"); };            
406        }
407        // println!("Forward substitution: {}",res.gr());
408        Ok(res)
409    }
410
411    /// Householder's Q*M matrix product without explicitly computing Q
412    pub fn house_uapply<T>(&self, m: &[Vec<T>]) -> Vec<Vec<f64>>
413    where
414        T: Copy + PartialOrd + std::fmt::Display,
415        f64: From<T>,
416    {
417        let u = self.to_full();
418        let mut qm = m.iter().map(|mvec| mvec.tof64()).collect::<Vec<Vec<f64>>>();
419        for uvec in u.iter().take(self.dim()) {
420            qm.iter_mut()
421                .for_each(|qvec| *qvec = uvec.house_reflect::<f64>(qvec))
422        }
423        qm
424    }
425}