Skip to content

Commit eb5796f

Browse files
authored
Merge pull request #4346 from jwpeterson/is_hpd
Add TypeTensor check for Hermitian positive-definiteness
2 parents aa74eb6 + 3af4373 commit eb5796f

3 files changed

Lines changed: 144 additions & 0 deletions

File tree

include/numerics/type_tensor.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,13 @@ class TypeTensor
436436
void write_unformatted (std::ostream & out_stream,
437437
const bool newline = true) const;
438438

439+
/**
440+
* Returns true if the TypeTensor is Hermitian (or symmetric, when
441+
* T==Real) and positive-definite to within the provided relative
442+
* tolerance, false otherwise.
443+
*/
444+
bool is_hpd(Real rel_tol = TOLERANCE*TOLERANCE) const;
445+
439446
protected:
440447

441448
/**

src/numerics/type_tensor.C

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,61 @@ void TypeTensor<T>::write_unformatted (std::ostream & out_stream,
6363
out_stream << '\n';
6464
}
6565

66+
template <typename T>
67+
bool TypeTensor<T>::is_hpd(Real rel_tol) const
68+
{
69+
// Convenient reference to this object, to be used for matrix
70+
// operations.
71+
const auto & A = *this;
72+
73+
// The norm of this matrix, needed for relative tolerance checks.
74+
auto A_norm = A.norm();
75+
76+
// The zero matrix is only positive semi-definite
77+
if (A_norm == 0)
78+
return false;
79+
80+
// An absolute tolerance (based on the user's provided relative
81+
// tolerance) to be used in the floating point comparisons below.
82+
auto abs_tol = rel_tol * A_norm;
83+
84+
// Form the complex conjugate transpose of A
85+
TypeTensor<T> A_conjugate_transpose;
86+
for (unsigned int i=0; i<LIBMESH_DIM; i++)
87+
for (unsigned int j=0; j<LIBMESH_DIM; j++)
88+
A_conjugate_transpose(i,j) = libmesh_conj(A(j,i));
89+
90+
// Check if Hermitian
91+
if ((A - A_conjugate_transpose).norm() > abs_tol)
92+
return false;
93+
94+
// If we made it here, then we are Hermitian, so now we just need to
95+
// check if we are positive-definite by checking that all principal
96+
// minors are positive. Note: the determinant of a Hermitian matrix
97+
// and all principal minors are real-valued. Since we already
98+
// checked that the matrix is Hermitian above, we don't bother to
99+
// check the complex parts are also zero now.
100+
101+
// For 3x3 and 2x2, check the 1x1 determinant
102+
#if LIBMESH_DIM > 1
103+
if (std::real(A(0,0)) < -abs_tol)
104+
return false;
105+
#endif
106+
107+
// For 3x3, check the upper 2x2 determinant
108+
#if LIBMESH_DIM > 2
109+
if (std::real(A(0,0)*A(1,1) - A(0,1)*A(1,0)) < -abs_tol)
110+
return false;
111+
#endif
112+
113+
// Finally, check the full determinant
114+
if (std::real(A.det()) < -abs_tol)
115+
return false;
116+
117+
// If we made it here, then the matrix is Hermitian
118+
// positive-definite.
119+
return true;
120+
}
66121

67122

68123
template <>

tests/numerics/type_tensor_test.C

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ public:
2424
#endif
2525
CPPUNIT_TEST(testRowCol);
2626
CPPUNIT_TEST(testIsZero);
27+
CPPUNIT_TEST(testIsHPD);
2728
#ifdef LIBMESH_HAVE_METAPHYSICL
2829
CPPUNIT_TEST(testReplaceAlgebraicType);
2930
#endif
@@ -104,6 +105,87 @@ private:
104105
}
105106
}
106107

108+
void testIsHPD()
109+
{
110+
LOG_UNIT_TEST;
111+
112+
{
113+
#if LIBMESH_DIM == 3
114+
{
115+
TensorValue<double> tensor(2., 1., 0.,
116+
1., 2., 1.,
117+
0., 1., 2.);
118+
119+
CPPUNIT_ASSERT(tensor.is_hpd(/*rel_tol=*/0.));
120+
}
121+
{
122+
// Symmetric but not positive-definite, because the upper 2x2
123+
// principal minor is zero.
124+
TensorValue<double> tensor(1, 0., 0.,
125+
0., 0., 1.,
126+
0., 1., 0.);
127+
128+
CPPUNIT_ASSERT(!tensor.is_hpd());
129+
}
130+
{
131+
// Random matrix that is SPD by construction
132+
133+
auto get_random = [&]()
134+
{
135+
return static_cast<Real>(std::rand()) / RAND_MAX; // in range [0,1]
136+
};
137+
138+
TensorValue<double> tensor(get_random(), get_random(), get_random(),
139+
get_random(), get_random(), get_random(),
140+
get_random(), get_random(), get_random());
141+
142+
// Make symmetric
143+
tensor = 0.5*(tensor + tensor.transpose());
144+
145+
// Make positive definite
146+
for (auto i : make_range(LIBMESH_DIM))
147+
tensor(i,i) += 3.;
148+
149+
CPPUNIT_ASSERT(tensor.is_hpd());
150+
}
151+
152+
#ifdef LIBMESH_USE_COMPLEX_NUMBERS
153+
154+
auto i = Complex(0, 1);
155+
auto one = Complex(1, 0);
156+
{
157+
// Symmetric but not Hermitian, because Hermitian matrices must
158+
// have real-valued entries on the diagonal.
159+
TensorValue<Complex> tensor(i, 0., 0.,
160+
0., 1., 0.,
161+
0., 0., 1.);
162+
163+
CPPUNIT_ASSERT(!tensor.is_hpd());
164+
}
165+
{
166+
// Symmetric but not Hermitian, because the off-diagonal
167+
// entries must be complex conjugates of one another.
168+
TensorValue<Complex> tensor(2, i, 0.,
169+
i, 2., i,
170+
0., i, 2.);
171+
172+
CPPUNIT_ASSERT(!tensor.is_hpd());
173+
}
174+
{
175+
// Both Hermitian and positive-definite
176+
TensorValue<Complex> tensor(2., one+i, 0.,
177+
one-i, 2., one+i,
178+
0., one-i, 2.);
179+
180+
CPPUNIT_ASSERT(tensor.is_hpd());
181+
}
182+
183+
#endif
184+
185+
#endif // LIBMESH_DIM == 3
186+
}
187+
}
188+
107189
void testRotation()
108190
{
109191
LOG_UNIT_TEST;

0 commit comments

Comments
 (0)