Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion src/linalg/decomposition.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use crate::storage::Storage;
use crate::{
Allocator, Bidiagonal, Cholesky, ColPivQR, ComplexField, DefaultAllocator, Dim, DimDiff,
DimMin, DimMinimum, DimSub, FullPivLU, Hessenberg, Matrix, OMatrix, RealField, Schur,
SymmetricEigen, SymmetricTridiagonal, LU, QR, SVD, U1, UDU,
SymmetricEigen, SymmetricTridiagonal, LDL, LU, QR, SVD, U1, UDU,
};

/// # Rectangular matrix decomposition
Expand Down Expand Up @@ -281,6 +281,18 @@ impl<T: ComplexField, D: Dim, S: Storage<T, D, D>> Matrix<T, D, D, S> {
Hessenberg::new(self.into_owned())
}

/// Attempts to compute the LDL decomposition of this matrix.
///
/// The input matrix `self` is assumed to be symmetric and this decomposition will only read
/// the lower-triangular part of `self`.
pub fn ldl(self) -> Option<LDL<T, D>>
where
T: ComplexField,
DefaultAllocator: Allocator<D> + Allocator<D, D>,
{
LDL::new(self.into_owned())
}

/// Computes the Schur decomposition of a square matrix.
pub fn schur(self) -> Schur<T, D>
where
Expand Down
121 changes: 121 additions & 0 deletions src/linalg/ldl.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
#[cfg(feature = "serde-serialize-no-std")]
use serde::{Deserialize, Serialize};

use crate::allocator::Allocator;
use crate::base::{Const, DefaultAllocator, OMatrix, OVector};
use crate::dimension::Dim;
use simba::scalar::ComplexField;

/// The LDL / LDL^T factorization of a Hermitian matrix A = LDL^T where L is a lower unit-triangular matrix and D is diagonal matrix.
#[cfg_attr(feature = "serde-serialize-no-std", derive(Serialize, Deserialize))]
#[cfg_attr(
feature = "serde-serialize-no-std",
serde(bound(serialize = "OMatrix<T, D, D>: Serialize"))
)]
#[cfg_attr(
feature = "serde-serialize-no-std",
serde(bound(deserialize = "OMatrix<T, D, D>: Deserialize<'de>"))
)]
#[derive(Clone, Debug)]
pub struct LDL<T: ComplexField, D: Dim>(OMatrix<T, D, D>)
where
DefaultAllocator: Allocator<D> + Allocator<D, D>;

impl<T: ComplexField, D: Dim> Copy for LDL<T, D>
where
DefaultAllocator: Allocator<D> + Allocator<D, D>,
OMatrix<T, D, D>: Copy,
{
}

impl<T: ComplexField, D: Dim> LDL<T, D>
where
DefaultAllocator: Allocator<D> + Allocator<D, D>,
{
/// Returns the diagonal elements as a vector.
#[must_use]
pub fn d(&self) -> OVector<T, D> {
self.0.diagonal()
}

/// Returns the diagonal elements as a matrix.
#[must_use]
pub fn d_matrix(&self) -> OMatrix<T, D, D> {
OMatrix::from_diagonal(&self.0.diagonal())
}

/// Returns the lower triangular matrix.
#[must_use]
pub fn l_matrix(&self) -> OMatrix<T, D, D> {
let mut l = self.0.clone();

l.column_iter_mut()
.enumerate()
.for_each(|(idx, mut column)| {
column[idx] = T::one();
});

l
}

/// Returns the matrix L * sqrt(D).
/// This function returns `None` if the square root of any of the values in the diagonal matrix D is not finite.
///
/// This function can be used to generate a lower triangular matrix as if it were generated by the Cholesky decomposition, without the requirement of positive definiteness.
pub fn lsqrtd(&self) -> Option<OMatrix<T, D, D>> {
let n_dim = self.0.shape_generic().1;

let lsqrtd: crate::Matrix<T, D, D, <DefaultAllocator as Allocator<D, D>>::Buffer<T>> = &self
.l_matrix()
* OMatrix::from_diagonal(&OVector::from_iterator_generic(
n_dim,
Const::<1>,
self.d().iter().map(|value| value.clone().sqrt()),
));

// Check for any non-finite numbers in lsqrtd and return None if necessary.
if !lsqrtd.iter().fold(true, |acc, next| acc & next.is_finite()) {
None
} else {
Some(lsqrtd)
}
}

/// Computes the LDL / LDL^T factorization.
pub fn new(mut matrix: OMatrix<T, D, D>) -> Option<Self> {
for j in 0..matrix.ncols() {
let mut d_j: T = matrix[(j, j)].clone();

if j > 0 {
for k in 0..j {
d_j -= matrix[(j, k)].clone()
* matrix[(j, k)].clone().conjugate()
* matrix[(k, k)].clone();
}
}

matrix[(j, j)] = d_j;

for i in (j + 1)..matrix.ncols() {
let mut l_ij = matrix[(i, j)].clone();

for k in 0..j {
l_ij -= matrix[(j, k)].clone().conjugate()
* matrix[(i, k)].clone()
* matrix[(k, k)].clone();
}

if matrix[(j, j)] == T::zero() {
matrix[(i, j)] = T::zero();
} else {
matrix[(i, j)] = l_ij / matrix[(j, j)].clone();
}

// Zero out the upper triangular part.
matrix[(j, i)] = T::zero();
}
}

Some(Self(matrix))
}
}
2 changes: 2 additions & 0 deletions src/linalg/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ pub mod givens;
mod hessenberg;
pub mod householder;
mod inverse;
mod ldl;
mod lu;
mod permutation_sequence;
mod pow;
Expand All @@ -39,6 +40,7 @@ pub use self::cholesky::*;
pub use self::col_piv_qr::*;
pub use self::full_piv_lu::*;
pub use self::hessenberg::*;
pub use self::ldl::*;
pub use self::lu::*;
pub use self::permutation_sequence::*;
pub use self::qr::*;
Expand Down
115 changes: 115 additions & 0 deletions tests/linalg/ldl.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
use na::{Complex, Matrix3};
use num::Zero;

#[test]
#[rustfmt::skip]
fn ldl_simple() {
let m = Matrix3::new(
Complex::new(2.0, 0.0), Complex::new(-1.0, 0.5), Complex::zero(),
Complex::new(-1.0, -0.5), Complex::new(2.0, 0.0), Complex::new(-1.0, 0.0),
Complex::zero(), Complex::new(-1.0, 0.0), Complex::new(2.0, 0.0));

let ldl = m.ldl().unwrap();

println!("{:}", &m);
println!("{:}", ldl.l_matrix());
println!("{:}", ldl.d());

// Rebuild
let p = ldl.l_matrix() * ldl.d_matrix() * ldl.l_matrix().adjoint();


println!("{:}", &p);

assert!(relative_eq!(m, p, epsilon = 3.0e-12));
}

#[test]
#[rustfmt::skip]
fn ldl_partial() {
let m = Matrix3::new(
Complex::new(2.0, 0.0), Complex::zero(), Complex::zero(),
Complex::zero(), Complex::zero(), Complex::zero(),
Complex::zero(), Complex::zero(), Complex::new(2.0, 0.0));

let ldl = m.lower_triangle().ldl().unwrap();

// Rebuild
let p = ldl.l_matrix() * ldl.d_matrix() * ldl.l_matrix().adjoint();

assert!(relative_eq!(m, p, epsilon = 3.0e-12));
}

#[test]
#[rustfmt::skip]
fn ldl_lsqrtd() {
let m = Matrix3::new(
Complex::new(2.0, 0.0), Complex::new(-1.0, 0.5), Complex::zero(),
Complex::new(-1.0, -0.5), Complex::new(2.0, 0.0), Complex::new(-1.0, 0.0),
Complex::zero(), Complex::new(-1.0, 0.0), Complex::new(2.0, 0.0));

let chol= m.cholesky().unwrap();
let ldl = m.ldl().unwrap();

assert!(relative_eq!(ldl.lsqrtd().unwrap(), chol.l(), epsilon = 3.0e-16));
}

#[test]
#[should_panic]
#[rustfmt::skip]
fn ldl_non_sym_panic() {
let m = Matrix3::new(
2.0, -1.0, 0.0,
1.0, -2.0, 3.0,
-2.0, 1.0, 0.3);

let ldl = m.ldl().unwrap();

// Rebuild
let p = ldl.l_matrix() * ldl.d_matrix() * ldl.l_matrix().transpose();

assert!(relative_eq!(m, p, epsilon = 3.0e-16));
}

#[cfg(feature = "proptest-support")]
mod proptest_tests {
#[allow(unused_imports)]
use crate::core::helper::{RandComplex, RandScalar};

macro_rules! gen_tests(
($module: ident, $scalar: expr) => {
mod $module {
#[allow(unused_imports)]
use crate::core::helper::{RandScalar, RandComplex};
use crate::proptest::*;
use proptest::{prop_assert, proptest};

proptest! {
#[test]
fn ldl(m in dmatrix_($scalar)) {
let m = &m * m.adjoint();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understood correctly what you said in the comment, then this implementation of the LDL^T decomposition requires the matrix to be symmetric positive definite (not semi definite), right? However, using M M^T will only give us pos. semidefinite, so we should expect some spurious failures here. I've actually run into the same problem in nalgebra-lapack and you can see my solution here. I've added a proptest function that gives a spd matrix in a bit of a hacky, but sound way: The idea is to use M' = M M^T + alpha * Id, where alpha is chosen suitably large to make we don't get into numerical trouble. You should be able to easily adapt the functions to return complex matrices.

Copy link
Collaborator

@alexhroom alexhroom Nov 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just wanted to weigh in - i'm a little confused actually... the 'point' of LDL is that it's a generalisation of the Cholesky algorithm to all symmetric/Hermitian matrices, with no requirements on positive definiteness/semi-definiteness. If it only works for definite/semidefinite matrices then there's no benefit to using LDL over Cholesky, which is a simpler decomposition and a more efficient algorithm.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry if the above text is slightly confusing. But no, the LDL/LDL^T decomposition only requires the matrix to be symmetric. It does NOT require it to be positive definite. This is stated in both textbooks referenced here.

The problem is that, in both cases, the algorithms that are shown DO require the matrix to be positive OR negative definite. I'd have to look deeper into this, but I think this is because in the semi-positive/negative definite cases the solution is not unique. I think there is a choice for the particular column where the corresponding value in the D matrix is zero.


if let Some(ldl) = m.clone().ldl() {
let p = &ldl.l * &ldl.d_matrix() * &ldl.l.transpose();
println!("m: {}, p: {}", m, p);

prop_assert!(relative_eq!(m, p, epsilon = 1.0e-7));
}
}

#[test]
fn ldl_static(m in matrix4_($scalar)) {
let m = m.hermitian_part();

if let Some(ldl) = m.ldl() {
let p = ldl.l * ldl.d_matrix() * ldl.l.transpose();
prop_assert!(relative_eq!(m, p, epsilon = 1.0e-7));
}
}
}
}
}
);

gen_tests!(f64, PROPTEST_F64);
}
1 change: 1 addition & 0 deletions tests/linalg/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ mod exp;
mod full_piv_lu;
mod hessenberg;
mod inverse;
mod ldl;
mod lu;
mod pow;
mod qr;
Expand Down