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}