iterative_solvers/utils/dense.rs
1//! Utility functions for creating dense matrices.
2
3use crate::utils::get_mut_unchecked;
4#[allow(unused_imports)]
5use crate::{
6    IterSolverError, IterSolverResult,
7    ops::{Matrix, Vector},
8    utils::from_diagonal,
9};
10
11/// Creates a diagonal matrix with the given data placed on a specified diagonal.
12///
13/// This function constructs a matrix where the provided data is placed on a diagonal
14/// that can be offset from the main diagonal. The resulting matrix size is determined
15/// by the length of the data and the absolute value of the offset.
16///
17/// # Arguments
18///
19/// * `data` - A slice of f64 values to be placed on the diagonal
20/// * `offset` - The diagonal offset:
21///   - `0`: Main diagonal
22///   - Positive: above the main diagonal (upper-diagonal)
23///   - Negative: below the main diagonal (lower-diagonal)
24///
25/// # Examples
26///
27/// ```rust
28/// use iterative_solvers::utils::dense::diagm;
29///
30/// // Main diagonal
31/// let data = vec![1.0, 2.0, 3.0];
32/// let mat = diagm(&data, 0);
33/// // Creates:
34/// // [1.0, 0.0, 0.0]
35/// // [0.0, 2.0, 0.0]
36/// // [0.0, 0.0, 3.0]
37///
38/// // Upper-diagonal (offset = 1)
39/// let mat = diagm(&data, 1);
40/// // Creates:
41/// // [0.0, 1.0, 0.0, 0.0]
42/// // [0.0, 0.0, 2.0, 0.0]
43/// // [0.0, 0.0, 0.0, 3.0]
44/// // [0.0, 0.0, 0.0, 0.0]
45/// ```
46pub fn diagm(data: &[f64], offset: isize) -> Matrix<f64> {
47    if data.is_empty() {
48        #[cfg(feature = "ndarray")]
49        {
50            return Matrix::zeros((0, 0));
51        }
52        #[cfg(not(feature = "ndarray"))]
53        {
54            return Matrix::zeros(0, 0);
55        }
56    }
57    match offset {
58        0 => from_diagonal(data),
59        offset => {
60            let offset_usize = offset.unsigned_abs();
61            let n = data.len() + offset_usize;
62            let mut mat = {
63                #[cfg(feature = "ndarray")]
64                {
65                    Matrix::zeros((n, n))
66                }
67                #[cfg(not(feature = "ndarray"))]
68                {
69                    Matrix::zeros(n, n)
70                }
71            };
72
73            unsafe {
74                if offset > 0 {
75                    data.iter().enumerate().for_each(|(idx, &val)| {
76                        *get_mut_unchecked(&mut mat, idx, idx + offset_usize) = val
77                    });
78                } else {
79                    data.iter().enumerate().for_each(|(idx, &val)| {
80                        *get_mut_unchecked(&mut mat, idx + offset_usize, idx) = val
81                    });
82                }
83            }
84            mat
85        }
86    }
87}
88
89/// Creates a tridiagonal matrix from diagonal, lower diagonal, and upper diagonal vectors.
90///
91/// This function constructs a tridiagonal matrix where:
92/// - The main diagonal contains elements from the `diagonal` vector
93/// - The lower-diagonal (below main) contains elements from the `lower` vector
94/// - The upper-diagonal (above main) contains elements from the `upper` vector
95///
96/// # Arguments
97///
98/// * `diagonal` - A slice containing the main diagonal elements
99/// * `lower` - A slice containing the lower diagonal elements (lower-diagonal)
100/// * `upper` - A slice containing the upper diagonal elements (upper-diagonal)
101///
102/// # Dimension Requirements
103///
104/// For a valid tridiagonal matrix:
105/// - `diagonal.len()` must equal `lower.len() + 1`
106/// - `lower.len()` must equal `upper.len()`
107///
108/// This is because an n×n tridiagonal matrix has:
109/// - n diagonal elements
110/// - (n-1) lower-diagonal elements
111/// - (n-1) upper-diagonal elements
112///
113/// # Examples
114///
115/// ```rust
116/// use iterative_solvers::utils::dense::tridiagonal;
117///
118/// let diagonal = vec![2.0, 3.0, 4.0];
119/// let lower = vec![1.0, 1.0];
120/// let upper = vec![1.0, 1.0];
121///
122/// let result = tridiagonal(&diagonal, &lower, &upper).unwrap();
123/// // Creates:
124/// // [2.0, 1.0, 0.0]
125/// // [1.0, 3.0, 1.0]
126/// // [0.0, 1.0, 4.0]
127/// ```
128///
129/// # Errors
130///
131/// Returns `IterSolverError::DimensionError` if the input vectors have incompatible dimensions.
132pub fn tridiagonal(
133    diagonal: &[f64],
134    lower: &[f64],
135    upper: &[f64],
136) -> IterSolverResult<Matrix<f64>> {
137    if diagonal.len() != lower.len() + 1 || lower.len() != upper.len() {
138        return Err(IterSolverError::DimensionError(format!(
139            "For tridiagonal matrix, the length of `diagonal` {}, the length of `lower` {} and `upper` {} do not match",
140            diagonal.len(),
141            lower.len(),
142            upper.len()
143        )));
144    }
145    Ok(diagm(diagonal, 0) + diagm(lower, -1) + diagm(upper, 1))
146}
147
148/// Creates a symmetric tridiagonal matrix from diagonal and sub-diagonal vectors.
149///
150/// This function constructs a symmetric tridiagonal matrix where the lower-diagonal
151/// and upper-diagonal elements are identical. This is a common structure in numerical
152/// methods, particularly for solving differential equations and eigenvalue problems.
153///
154/// # Arguments
155///
156/// * `diagonal` - A slice containing the main diagonal elements
157/// * `sub_diagonal` - A slice containing the lower-diagonal elements, which will be
158///   mirrored to create the upper-diagonal
159///
160/// # Dimension Requirements
161///
162/// For a valid symmetric tridiagonal matrix:
163/// - `diagonal.len()` must equal `sub_diagonal.len() + 1`
164///
165/// # Examples
166///
167/// ```rust
168/// use iterative_solvers::utils::dense::symmetric_tridiagonal;
169///
170/// let diagonal = vec![2.0, 3.0, 4.0];
171/// let sub_diagonal = vec![1.0, 1.5];
172///
173/// let result = symmetric_tridiagonal(&diagonal, &sub_diagonal).unwrap();
174/// // Creates:
175/// // [2.0, 1.0, 0.0]
176/// // [1.0, 3.0, 1.5]
177/// // [0.0, 1.5, 4.0]
178/// ```
179///
180/// # Errors
181///
182/// Returns `IterSolverError::DimensionError` if the input vectors have incompatible dimensions.
183///
184/// # Note
185///
186/// This function internally calls `tridiagonal(diagonal, sub_diagonal, sub_diagonal)`,
187/// ensuring that the lower and upper diagonals are identical.
188pub fn symmetric_tridiagonal(
189    diagonal: &[f64],
190    sub_diagonal: &[f64],
191) -> IterSolverResult<Matrix<f64>> {
192    tridiagonal(diagonal, sub_diagonal, sub_diagonal)
193}
194
195/// Creates a diagonal matrix from a list of diagonals and their offsets.
196///
197/// # Arguments
198///
199/// * `diagonals` - A vector of vectors, where each inner vector contains the diagonal elements
200/// * `offsets` - A vector of offsets for each diagonal
201///
202/// # Examples
203///
204/// ```rust
205/// use iterative_solvers::utils::dense::diags;
206///
207/// let diagonals = vec![vec![1.0, 2.0, 3.0], vec![4.0, 5.0], vec![6.0]];
208/// let offsets = vec![0, 1, 2];
209///
210/// let result = diags(diagonals, offsets);
211/// // Creates:
212/// // [1.0, 4.0, 6.0]
213/// // [0.0, 2.0, 5.0]
214/// // [0.0, 0.0, 3.0]
215/// ```
216pub fn diags(diagonals: Vec<Vec<f64>>, offsets: Vec<isize>) -> IterSolverResult<Matrix<f64>> {
217    if diagonals.len() != offsets.len() {
218        return Err(IterSolverError::DimensionError(format!(
219            "The length of `diagonals` {} and `offsets` {} do not match",
220            diagonals.len(),
221            offsets.len()
222        )));
223    }
224    if diagonals.is_empty() {
225        #[cfg(feature = "ndarray")]
226        {
227            return Ok(Matrix::zeros((0, 0)));
228        }
229        #[cfg(not(feature = "ndarray"))]
230        {
231            return Ok(Matrix::zeros(0, 0));
232        }
233    }
234
235    // Check for duplicate offsets
236    let n = diagonals[0].len() + offsets[0].unsigned_abs();
237    let mut unique_offsets = std::collections::HashSet::new();
238    for (index, (diag, offset)) in diagonals.iter().zip(offsets.iter()).enumerate() {
239        if !unique_offsets.insert(*offset) {
240            return Err(IterSolverError::InvalidInput(
241                "Duplicate offsets are not allowed".to_string(),
242            ));
243        }
244        if diag.len() + offset.unsigned_abs() != n {
245            return Err(IterSolverError::DimensionError(format!(
246                "The {}th diagonal's length {} and its offset {} do not match",
247                index,
248                diag.len(),
249                offset
250            )));
251        }
252    }
253
254    let mut res = diagm(&diagonals[0], offsets[0]);
255
256    for (diag, offset) in diagonals.iter().zip(offsets.iter()) {
257        res += &diagm(diag, *offset);
258    }
259    Ok(res)
260}
261
262#[cfg(test)]
263mod tests {
264    use super::*;
265
266    #[test]
267    fn test_diag() {
268        let data = vec![1.0, 2.0, 3.0];
269        let mat = diagm(&data, 0);
270        println!("{mat:?}");
271        let mat = diagm(&data, 1);
272        println!("{mat:?}");
273        let mat = diagm(&data, -1);
274        println!("{mat:?}");
275        let mat = diagm(&data, 2);
276        println!("{mat:?}");
277        let mat = diagm(&data, -2);
278        println!("{mat:?}");
279    }
280}