Skip to content

Commit f122fb6

Browse files
authored
feat: Add Bunch–Kaufman LDL Decomposition (#1591)
1 parent c52ca1f commit f122fb6

7 files changed

Lines changed: 788 additions & 51 deletions

File tree

src/base/norm.rs

Lines changed: 101 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,44 @@ pub trait Norm<T: SimdComplexField> {
3939
ShapeConstraint: SameNumberOfRows<R1, R2> + SameNumberOfColumns<C1, C2>;
4040
}
4141

42-
/// Euclidean norm.
42+
/// Euclidean norm of a vector, or Frobenius norm of a matrix.
43+
///
44+
/// Computes sqrt(sum |a_ij|^2) over all elements.
45+
///
46+
/// <div class="warning">
47+
/// For matrices, this is the Frobenius norm, not the matrix 2-norm (spectral norm).
48+
/// </div>
4349
#[derive(Copy, Clone, Debug)]
4450
pub struct EuclideanNorm;
45-
/// Lp norm.
51+
52+
/// Entrywise Lp norm of a matrix or vector.
53+
///
54+
/// Computes (sum |a_ij|^p)^(1/p) over all elements.
55+
///
56+
/// <div class="warning">
57+
/// This does not match the standard mathematical definition of the matrix Lp norm.
58+
/// </div>
4659
#[derive(Copy, Clone, Debug)]
4760
pub struct LpNorm(pub i32);
48-
/// L-infinite norm aka. Chebytchev norm aka. uniform norm aka. suppremum norm.
61+
62+
/// Induced matrix 1-norm (maximum absolute column sum).
63+
///
64+
/// Computes max_j sum_i |a_ij|.
65+
///
66+
/// For a column vector, this is the L1 norm.
67+
/// For a row vector, this is the L-infinity norm.
68+
#[derive(Copy, Clone, Debug)]
69+
pub struct OneNorm;
70+
71+
/// Entrywise L-infinity norm of a matrix or vector.
72+
///
73+
/// Computes max |a_ij| over all elements.
74+
///
75+
/// For a vector this is the standard L-infinity norm.
76+
///
77+
/// <div class="warning">
78+
/// For matrices, this is the entrywise maximum, not the induced matrix infinity-norm.
79+
/// </div>
4980
#[derive(Copy, Clone, Debug)]
5081
pub struct UniformNorm;
5182

@@ -120,6 +151,45 @@ impl<T: SimdComplexField> Norm<T> for LpNorm {
120151
}
121152
}
122153

154+
impl<T: SimdComplexField> Norm<T> for OneNorm {
155+
#[inline]
156+
fn norm<R, C, S>(&self, m: &Matrix<T, R, C, S>) -> T::SimdRealField
157+
where
158+
R: Dim,
159+
C: Dim,
160+
S: Storage<T, R, C>,
161+
{
162+
m.column_iter()
163+
.map(|col| col.fold(T::SimdRealField::zero(), |a, b| a + b.simd_modulus()))
164+
.fold(T::SimdRealField::zero(), T::SimdRealField::simd_max)
165+
}
166+
167+
#[inline]
168+
fn metric_distance<R1, C1, S1, R2, C2, S2>(
169+
&self,
170+
m1: &Matrix<T, R1, C1, S1>,
171+
m2: &Matrix<T, R2, C2, S2>,
172+
) -> T::SimdRealField
173+
where
174+
R1: Dim,
175+
C1: Dim,
176+
S1: Storage<T, R1, C1>,
177+
R2: Dim,
178+
C2: Dim,
179+
S2: Storage<T, R2, C2>,
180+
ShapeConstraint: SameNumberOfRows<R1, R2> + SameNumberOfColumns<C1, C2>,
181+
{
182+
m1.column_iter()
183+
.zip(m2.column_iter())
184+
.map(|(c1, c2)| {
185+
c1.zip_fold(&c2, T::SimdRealField::zero(), |acc, a, b| {
186+
acc + (a - b).simd_modulus()
187+
})
188+
})
189+
.fold(T::SimdRealField::zero(), T::SimdRealField::simd_max)
190+
}
191+
}
192+
123193
impl<T: SimdComplexField> Norm<T> for UniformNorm {
124194
#[inline]
125195
fn norm<R, C, S>(&self, m: &Matrix<T, R, C, S>) -> T::SimdRealField
@@ -158,8 +228,11 @@ impl<T: SimdComplexField> Norm<T> for UniformNorm {
158228
}
159229

160230
/// # Magnitude and norms
231+
///
232+
/// Unless otherwise noted, the norm used throughout is the L2 norm for vectors and
233+
/// the Frobenius norm for matrices.
161234
impl<T: Scalar, R: Dim, C: Dim, S: Storage<T, R, C>> Matrix<T, R, C, S> {
162-
/// The squared L2 norm of this vector.
235+
/// Squared L2 norm of this vector, or squared Frobenius norm of this matrix.
163236
#[inline]
164237
#[must_use]
165238
pub fn norm_squared(&self) -> T::SimdRealField
@@ -176,7 +249,7 @@ impl<T: Scalar, R: Dim, C: Dim, S: Storage<T, R, C>> Matrix<T, R, C, S> {
176249
res
177250
}
178251

179-
/// The L2 norm of this matrix.
252+
/// L2 norm of this vector, or Frobenius norm of this matrix.
180253
///
181254
/// Use `.apply_norm` to apply a custom norm.
182255
#[inline]
@@ -188,7 +261,7 @@ impl<T: Scalar, R: Dim, C: Dim, S: Storage<T, R, C>> Matrix<T, R, C, S> {
188261
self.norm_squared().simd_sqrt()
189262
}
190263

191-
/// Compute the distance between `self` and `rhs` using the metric induced by the euclidean norm.
264+
/// Distance between `self` and `rhs` using the L2 norm for vectors, or the Frobenius for matrices.
192265
///
193266
/// Use `.apply_metric_distance` to apply a custom norm.
194267
#[inline]
@@ -306,7 +379,13 @@ impl<T: Scalar, R: Dim, C: Dim, S: Storage<T, R, C>> Matrix<T, R, C, S> {
306379
self.unscale(self.norm())
307380
}
308381

309-
/// The Lp norm of this matrix.
382+
/// Entrywise Lp norm of this matrix or vector.
383+
///
384+
/// Computes (sum |a_ij|^p)^(1/p) over all elements.
385+
///
386+
/// <div class="warning">
387+
/// For matrices, this does not match the standard mathematical definition of the matrix Lp norm.
388+
/// </div>
310389
#[inline]
311390
#[must_use]
312391
pub fn lp_norm(&self, p: i32) -> T::SimdRealField
@@ -316,6 +395,21 @@ impl<T: Scalar, R: Dim, C: Dim, S: Storage<T, R, C>> Matrix<T, R, C, S> {
316395
self.apply_norm(&LpNorm(p))
317396
}
318397

398+
/// Induced matrix 1-norm (maximum absolute column sum).
399+
///
400+
/// Computes max_j sum_i |a_ij|.
401+
///
402+
/// For a column vector, this is the L1 norm.
403+
/// For a row vector, this is the L-infinity norm.
404+
#[inline]
405+
#[must_use]
406+
pub fn one_norm(&self) -> T::SimdRealField
407+
where
408+
T: SimdComplexField,
409+
{
410+
self.apply_norm(&OneNorm)
411+
}
412+
319413
/// Attempts to normalize `self`.
320414
///
321415
/// The components of this matrix can be SIMD types.

src/linalg/decomposition.rs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
use crate::storage::Storage;
22
use crate::{
33
Allocator, Bidiagonal, Cholesky, ColPivQR, ComplexField, DefaultAllocator, Dim, DimDiff,
4-
DimMin, DimMinimum, DimSub, FullPivLU, Hessenberg, LU, Matrix, OMatrix, QR, RealField, SVD,
5-
Schur, SymmetricEigen, SymmetricTridiagonal, U1, UDU,
4+
DimMin, DimMinimum, DimSub, FullPivLU, Hessenberg, LBLT, LU, Matrix, OMatrix, QR, RealField,
5+
SVD, Schur, SymmetricEigen, SymmetricTridiagonal, U1, UDU,
66
};
77

88
/// # Rectangular matrix decomposition
@@ -244,6 +244,7 @@ impl<T: ComplexField, R: Dim, C: Dim, S: Storage<T, R, C>> Matrix<T, R, C, S> {
244244
/// | -------------------------|---------------------------|--------------|
245245
/// | Hessenberg | `Q * H * Qᵀ` | `Q` is a unitary matrix and `H` an upper-Hessenberg matrix. |
246246
/// | Cholesky | `L * Lᵀ` | `L` is a lower-triangular matrix. |
247+
/// | LBLT decomposition | `Pᵀ * L * B * Lᴴ * P` | `L` is unit lower-triangular, `B` is Hermitian block-diagonal, and `P` is a permutation matrix. |
247248
/// | UDU | `U * D * Uᵀ` | `U` is a upper-triangular matrix, and `D` a diagonal matrix. |
248249
/// | Schur decomposition | `Q * T * Qᵀ` | `Q` is an unitary matrix and `T` a quasi-upper-triangular matrix. |
249250
/// | Symmetric eigendecomposition | `Q ~ Λ ~ Qᵀ` | `Q` is an unitary matrix, and `Λ` is a real diagonal matrix. |
@@ -260,6 +261,18 @@ impl<T: ComplexField, D: Dim, S: Storage<T, D, D>> Matrix<T, D, D, S> {
260261
Cholesky::new(self.into_owned())
261262
}
262263

264+
/// Computes the LBLT decomposition of this matrix.
265+
/// The input matrix `self` is assumed to be Hermitian (symmetric) and the decomposition will
266+
/// only read the lower-triangular part of `self`.
267+
pub fn lblt(self) -> LBLT<T, D>
268+
where
269+
T: Copy,
270+
T::RealField: Copy,
271+
DefaultAllocator: Allocator<D> + Allocator<D, D>,
272+
{
273+
LBLT::new(self.into_owned())
274+
}
275+
263276
/// Attempts to compute the UDU decomposition of this matrix.
264277
///
265278
/// The input matrix `self` is assumed to be symmetric and this decomposition will only read

src/linalg/exp.rs

Lines changed: 45 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -156,31 +156,49 @@ where
156156
fn d4_tight(&mut self) -> T::RealField {
157157
if self.d4_exact.is_none() {
158158
self.calc_a4();
159-
self.d4_exact = Some(one_norm(self.a4.as_ref().unwrap()).powf(convert(0.25)));
159+
self.d4_exact = Some(self.a4.as_ref().unwrap().one_norm().powf(convert(0.25)));
160160
}
161161
self.d4_exact.clone().unwrap()
162162
}
163163

164164
fn d6_tight(&mut self) -> T::RealField {
165165
if self.d6_exact.is_none() {
166166
self.calc_a6();
167-
self.d6_exact = Some(one_norm(self.a6.as_ref().unwrap()).powf(convert(1.0 / 6.0)));
167+
self.d6_exact = Some(
168+
self.a6
169+
.as_ref()
170+
.unwrap()
171+
.one_norm()
172+
.powf(convert(1.0 / 6.0)),
173+
);
168174
}
169175
self.d6_exact.clone().unwrap()
170176
}
171177

172178
fn d8_tight(&mut self) -> T::RealField {
173179
if self.d8_exact.is_none() {
174180
self.calc_a8();
175-
self.d8_exact = Some(one_norm(self.a8.as_ref().unwrap()).powf(convert(1.0 / 8.0)));
181+
self.d8_exact = Some(
182+
self.a8
183+
.as_ref()
184+
.unwrap()
185+
.one_norm()
186+
.powf(convert(1.0 / 8.0)),
187+
);
176188
}
177189
self.d8_exact.clone().unwrap()
178190
}
179191

180192
fn d10_tight(&mut self) -> T::RealField {
181193
if self.d10_exact.is_none() {
182194
self.calc_a10();
183-
self.d10_exact = Some(one_norm(self.a10.as_ref().unwrap()).powf(convert(1.0 / 10.0)));
195+
self.d10_exact = Some(
196+
self.a10
197+
.as_ref()
198+
.unwrap()
199+
.one_norm()
200+
.powf(convert(1.0 / 10.0)),
201+
);
184202
}
185203
self.d10_exact.clone().unwrap()
186204
}
@@ -196,7 +214,7 @@ where
196214

197215
if self.d4_approx.is_none() {
198216
self.calc_a4();
199-
self.d4_approx = Some(one_norm(self.a4.as_ref().unwrap()).powf(convert(0.25)));
217+
self.d4_approx = Some(self.a4.as_ref().unwrap().one_norm().powf(convert(0.25)));
200218
}
201219

202220
self.d4_approx.clone().unwrap()
@@ -213,7 +231,13 @@ where
213231

214232
if self.d6_approx.is_none() {
215233
self.calc_a6();
216-
self.d6_approx = Some(one_norm(self.a6.as_ref().unwrap()).powf(convert(1.0 / 6.0)));
234+
self.d6_approx = Some(
235+
self.a6
236+
.as_ref()
237+
.unwrap()
238+
.one_norm()
239+
.powf(convert(1.0 / 6.0)),
240+
);
217241
}
218242

219243
self.d6_approx.clone().unwrap()
@@ -230,7 +254,13 @@ where
230254

231255
if self.d8_approx.is_none() {
232256
self.calc_a8();
233-
self.d8_approx = Some(one_norm(self.a8.as_ref().unwrap()).powf(convert(1.0 / 8.0)));
257+
self.d8_approx = Some(
258+
self.a8
259+
.as_ref()
260+
.unwrap()
261+
.one_norm()
262+
.powf(convert(1.0 / 8.0)),
263+
);
234264
}
235265

236266
self.d8_approx.clone().unwrap()
@@ -247,7 +277,13 @@ where
247277

248278
if self.d10_approx.is_none() {
249279
self.calc_a10();
250-
self.d10_approx = Some(one_norm(self.a10.as_ref().unwrap()).powf(convert(1.0 / 10.0)));
280+
self.d10_approx = Some(
281+
self.a10
282+
.as_ref()
283+
.unwrap()
284+
.one_norm()
285+
.powf(convert(1.0 / 10.0)),
286+
);
251287
}
252288

253289
self.d10_approx.clone().unwrap()
@@ -430,7 +466,7 @@ where
430466
let choose_2m_m = factorial(2 * m) / (m_factorial * m_factorial);
431467

432468
let abs_c_recip = choose_2m_m * factorial(2 * m + 1);
433-
let alpha = a_abs_onenorm / one_norm(a);
469+
let alpha = a_abs_onenorm / a.one_norm();
434470
let alpha: f64 = try_convert::<_, f64>(alpha).unwrap() / abs_c_recip as f64;
435471

436472
let u = 2_f64.powf(-53.0);
@@ -451,27 +487,6 @@ where
451487
q.lu().solve(&p).unwrap()
452488
}
453489

454-
fn one_norm<T, D>(m: &OMatrix<T, D, D>) -> T::RealField
455-
where
456-
T: ComplexField,
457-
D: Dim,
458-
DefaultAllocator: Allocator<D, D>,
459-
{
460-
let mut max = <T as ComplexField>::RealField::zero();
461-
462-
for i in 0..m.ncols() {
463-
let col = m.column(i);
464-
max = max.max(
465-
col.iter()
466-
.fold(<T as ComplexField>::RealField::zero(), |a, b| {
467-
a + b.clone().abs()
468-
}),
469-
);
470-
}
471-
472-
max
473-
}
474-
475490
impl<T: ComplexField, D> OMatrix<T, D, D>
476491
where
477492
D: DimMin<D, Output = D>,
@@ -539,15 +554,3 @@ where
539554
x
540555
}
541556
}
542-
543-
#[cfg(test)]
544-
mod tests {
545-
#[test]
546-
#[allow(clippy::float_cmp)]
547-
fn one_norm() {
548-
use crate::Matrix3;
549-
let m = Matrix3::new(-3.0, 5.0, 7.0, 2.0, 6.0, 4.0, 0.0, 2.0, 8.0);
550-
551-
assert_eq!(super::one_norm(&m), 19.0);
552-
}
553-
}

0 commit comments

Comments
 (0)