use crate::error::LapackError;
use oxiblas_core::scalar::{Field, Real, Scalar};
use oxiblas_matrix::{Mat, MatRef};
#[derive(Debug, Clone)]
pub struct Ql<T: Scalar> {
factors: Mat<T>,
tau: Vec<T>,
m: usize,
n: usize,
}
impl<T: Field + Real + bytemuck::Zeroable> Ql<T> {
pub fn compute(a: MatRef<T>) -> Result<Self, LapackError> {
let m = a.nrows();
let n = a.ncols();
if m == 0 || n == 0 {
return Err(LapackError::new(
crate::error::ErrorCode::InvalidDimension {
argument: 1,
expected: 1,
actual: 0,
},
"QL factorization",
));
}
let k = m.min(n);
let mut factors = Mat::zeros(m, n);
for i in 0..m {
for j in 0..n {
factors[(i, j)] = a[(i, j)];
}
}
let mut tau = vec![T::zero(); k];
for i in 0..k {
let col = n - 1 - i;
let vec_len = m - i;
if vec_len == 0 {
continue;
}
let (tau_i, beta) = compute_householder_bottom(&mut factors, col, vec_len);
tau[i] = tau_i;
factors[(vec_len - 1, col)] = beta;
if tau_i != T::zero() && col > 0 {
apply_householder_bottom(&mut factors, col, vec_len, tau_i);
}
}
Ok(Self { factors, tau, m, n })
}
#[must_use]
pub fn l_factor(&self) -> Mat<T> {
let mut l = Mat::zeros(self.m, self.n);
for i in 0..self.m {
if self.m >= self.n {
if i < self.m - self.n {
} else {
for j in 0..self.n {
if j <= self.n + i - self.m {
l[(i, j)] = self.factors[(i, j)];
}
}
}
} else {
for j in 0..self.n {
if j <= self.n - self.m + i {
l[(i, j)] = self.factors[(i, j)];
}
}
}
}
l
}
#[must_use]
pub fn q_factor(&self) -> Mat<T> {
let k = self.m.min(self.n);
let mut q = Mat::zeros(self.m, self.m);
for i in 0..self.m {
q[(i, i)] = T::one();
}
for i in (0..k).rev() {
let col = self.n - 1 - i;
let vec_len = self.m - i;
if self.tau[i] == T::zero() {
continue;
}
for qcol in 0..self.m {
let mut w = q[(vec_len - 1, qcol)]; for row in 0..(vec_len - 1) {
w = w + self.factors[(row, col)] * q[(row, qcol)];
}
let tw = self.tau[i] * w;
q[(vec_len - 1, qcol)] = q[(vec_len - 1, qcol)] - tw;
for row in 0..(vec_len - 1) {
q[(row, qcol)] = q[(row, qcol)] - tw * self.factors[(row, col)];
}
}
}
q
}
#[must_use]
pub fn dims(&self) -> (usize, usize) {
(self.m, self.n)
}
}
fn compute_householder_bottom<T: Field + Real>(
factors: &mut Mat<T>,
col: usize,
len: usize,
) -> (T, T) {
if len == 0 {
return (T::zero(), T::zero());
}
let mut norm_sq = T::zero();
for i in 0..len {
norm_sq = norm_sq + factors[(i, col)] * factors[(i, col)];
}
let norm = Real::sqrt(norm_sq);
if norm == T::zero() {
return (T::zero(), T::zero());
}
let x_pivot = factors[(len - 1, col)];
let beta = if x_pivot >= T::zero() { -norm } else { norm };
let tau = (beta - x_pivot) / beta;
let denom = x_pivot - beta;
if Scalar::abs(denom) > <T as Scalar>::epsilon() {
let scale = T::one() / denom;
for i in 0..(len - 1) {
factors[(i, col)] = factors[(i, col)] * scale;
}
}
(tau, beta)
}
fn apply_householder_bottom<T: Field + Real>(factors: &mut Mat<T>, col: usize, len: usize, tau: T) {
if tau == T::zero() || len == 0 {
return;
}
for k in 0..col {
let mut w = factors[(len - 1, k)]; for i in 0..(len - 1) {
w = w + factors[(i, col)] * factors[(i, k)];
}
let tw = tau * w;
factors[(len - 1, k)] = factors[(len - 1, k)] - tw;
for i in 0..(len - 1) {
factors[(i, k)] = factors[(i, k)] - tw * factors[(i, col)];
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn approx_eq(a: f64, b: f64, tol: f64) -> bool {
(a - b).abs() < tol
}
#[test]
fn test_ql_square() {
let a = Mat::from_rows(&[&[1.0f64, 2.0], &[3.0, 4.0]]);
let ql = Ql::compute(a.as_ref()).unwrap();
let l = ql.l_factor();
let q = ql.q_factor();
assert!(
l[(0, 1)].abs() < 1e-10,
"L should be lower triangular, L[0,1] = {}",
l[(0, 1)]
);
for i in 0..2 {
for j in 0..2 {
let mut dot = 0.0;
for k in 0..2 {
dot += q[(k, i)] * q[(k, j)];
}
let expected = if i == j { 1.0 } else { 0.0 };
assert!(
approx_eq(dot, expected, 1e-10),
"Q^T*Q[{},{}] = {}, expected {}",
i,
j,
dot,
expected
);
}
}
for i in 0..2 {
for j in 0..2 {
let mut sum = 0.0;
for k in 0..2 {
sum += q[(i, k)] * l[(k, j)];
}
assert!(
approx_eq(sum, a[(i, j)], 1e-10),
"QL[{},{}] = {}, A = {}",
i,
j,
sum,
a[(i, j)]
);
}
}
}
#[test]
fn test_ql_tall() {
let a = Mat::from_rows(&[&[1.0f64, 2.0], &[3.0, 4.0], &[5.0, 6.0]]);
let ql = Ql::compute(a.as_ref()).unwrap();
let l = ql.l_factor();
let q = ql.q_factor();
assert_eq!(l.nrows(), 3);
assert_eq!(l.ncols(), 2);
assert_eq!(q.nrows(), 3);
assert_eq!(q.ncols(), 3);
for i in 0..3 {
for j in 0..3 {
let mut dot = 0.0;
for k in 0..3 {
dot += q[(k, i)] * q[(k, j)];
}
let expected = if i == j { 1.0 } else { 0.0 };
assert!(
approx_eq(dot, expected, 1e-10),
"Q^T*Q[{},{}] = {}, expected {}",
i,
j,
dot,
expected
);
}
}
for i in 0..3 {
for j in 0..2 {
let mut sum = 0.0;
for k in 0..3 {
sum += q[(i, k)] * l[(k, j)];
}
assert!(
approx_eq(sum, a[(i, j)], 1e-10),
"QL[{},{}] = {}, A = {}",
i,
j,
sum,
a[(i, j)]
);
}
}
}
#[test]
fn test_ql_wide() {
let a = Mat::from_rows(&[&[1.0f64, 2.0, 3.0], &[4.0, 5.0, 6.0]]);
let ql = Ql::compute(a.as_ref()).unwrap();
let l = ql.l_factor();
let q = ql.q_factor();
assert_eq!(l.nrows(), 2);
assert_eq!(l.ncols(), 3);
assert_eq!(q.nrows(), 2);
assert_eq!(q.ncols(), 2);
for i in 0..2 {
for j in 0..2 {
let mut dot = 0.0;
for k in 0..2 {
dot += q[(k, i)] * q[(k, j)];
}
let expected = if i == j { 1.0 } else { 0.0 };
assert!(
approx_eq(dot, expected, 1e-10),
"Q^T*Q[{},{}] = {}",
i,
j,
dot
);
}
}
for i in 0..2 {
for j in 0..3 {
let mut sum = 0.0;
for k in 0..2 {
sum += q[(i, k)] * l[(k, j)];
}
assert!(
approx_eq(sum, a[(i, j)], 1e-10),
"QL[{},{}] = {}, A = {}",
i,
j,
sum,
a[(i, j)]
);
}
}
}
#[test]
fn test_ql_identity() {
let a: Mat<f64> = Mat::eye(3);
let ql = Ql::compute(a.as_ref()).unwrap();
let l = ql.l_factor();
let q = ql.q_factor();
for i in 0..3 {
for j in 0..3 {
if i == j {
assert!(q[(i, j)].abs() > 0.99);
assert!(l[(i, j)].abs() > 0.99);
} else {
assert!(q[(i, j)].abs() < 1e-10);
assert!(l[(i, j)].abs() < 1e-10);
}
}
}
}
}