Skip to content

Commit 62aa1a9

Browse files
authored
Merge pull request #397 from NumSim-Stack/389-390-gram-and-sympart
#389 #390: derive symmetric(+PSD) for Gram forms and the symmetric part
2 parents 80bf145 + 2bfde27 commit 62aa1a9

2 files changed

Lines changed: 98 additions & 0 deletions

File tree

include/numsim_cas/tensor/tensor_operators.h

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,19 @@ inline expression_holder<tensor_expression> tag_invoke(add_fn, L &&lhs,
8383
result.data()->set_space({Skew{}, AnyTraceTag{}});
8484
return result;
8585
}
86+
// trans(A) + A and A + trans(A) are symmetric in any dimension: the
87+
// symmetric part 2*sym(A). Mirror of the skew branch above (and of the
88+
// sub operator's skew rule). The skew branch matched trans(A) + (-A)
89+
// above, so a match here means a genuine plus. Closes #390.
90+
if (is_trans_of(lhs, rhs) || is_trans_of(rhs, lhs)) {
91+
auto &_lhs{lhs.template get<tensor_visitable_t>()};
92+
simplifier::tensor_detail::add_base visitor(std::forward<L>(lhs),
93+
std::forward<R>(rhs));
94+
auto result = _lhs.accept(visitor);
95+
if (result.is_valid())
96+
result.data()->set_space({Symmetric{}, AnyTraceTag{}});
97+
return result;
98+
}
8699
}
87100

88101
auto &_lhs{lhs.template get<tensor_visitable_t>()};
@@ -166,6 +179,22 @@ tag_invoke(mul_fn, L &&lhs, [[maybe_unused]] R &&rhs) {
166179
return make_expression<identity_tensor>(lhs.get().dim(), std::size_t{2});
167180
if (is_trans_of(lhs, rhs) && is_orthogonal(rhs))
168181
return make_expression<identity_tensor>(rhs.get().dim(), std::size_t{2});
182+
// Gram form: trans(X)*X and X*trans(X) are symmetric and positive
183+
// semidefinite for any X (x . (F^T F) . x = ||F x||^2 >= 0). Orthogonal X
184+
// already returned I above; X invertibility is unknown, so PSD only (not
185+
// PD). Closes #389.
186+
if (is_trans_of(lhs, rhs) || is_trans_of(rhs, lhs)) {
187+
auto &_lhs{lhs.template get<tensor_visitable_t>()};
188+
tensor_detail::simplifier::mul_base visitor(std::forward<L>(lhs),
189+
std::forward<R>(rhs));
190+
auto result = _lhs.accept(visitor);
191+
if (result.is_valid()) {
192+
result.data()->set_space({Symmetric{}, AnyTraceTag{}});
193+
result.data()->tensor_algebra_assumptions().insert(
194+
positive_semidefinite{});
195+
}
196+
return result;
197+
}
169198
}
170199
auto &_lhs{lhs.template get<tensor_visitable_t>()};
171200
tensor_detail::simplifier::mul_base visitor(std::forward<L>(lhs),

tests/TensorSpacePropagationTest.h

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,75 @@ TEST_F(TensorSpacePropagationTest, DiffAddSymmetric) {
424424
EXPECT_PRINT(d, "2*P_sym{4}");
425425
}
426426

427+
// ═══════════════════════════════════════════════════════════════════════════════
428+
// Gram forms (#389): trans(X)*X and X*trans(X) are symmetric + PSD for any X
429+
// ═══════════════════════════════════════════════════════════════════════════════
430+
431+
TEST_F(TensorSpacePropagationTest, GramTransXtimesXIsSymmetricPSD) {
432+
// trans(X)*X = right Cauchy-Green; symmetric and PSD regardless of X.
433+
auto g = trans(X) * X;
434+
EXPECT_TRUE(is_symmetric(g)) << "trans(X)*X must be symmetric";
435+
EXPECT_TRUE(is_positive_semidefinite(g)) << "trans(X)*X must be PSD";
436+
EXPECT_FALSE(is_skew(g));
437+
EXPECT_FALSE(is_positive_definite(g)) << "X invertibility unknown → PSD only";
438+
}
439+
440+
TEST_F(TensorSpacePropagationTest, GramXtimesTransXIsSymmetricPSD) {
441+
// X*trans(X) = left Cauchy-Green / Finger tensor.
442+
auto g = X * trans(X);
443+
EXPECT_TRUE(is_symmetric(g));
444+
EXPECT_TRUE(is_positive_semidefinite(g));
445+
}
446+
447+
TEST_F(TensorSpacePropagationTest, GramNormalizesUnderSymProjector) {
448+
// The derived symmetric annotation must let sym() short-circuit.
449+
EXPECT_PRINT(sym(trans(X) * X), ::testcas::S(trans(X) * X));
450+
}
451+
452+
TEST_F(TensorSpacePropagationTest, OrthogonalTransXtimesXStillFoldsToIdentity) {
453+
// Safety: for orthogonal Q the trans(Q)*Q -> I fold must still win over the
454+
// Gram annotation branch (I is stronger than "some symmetric PSD tensor").
455+
auto Q = std::get<0>(make_tensor_variable(std::tuple{"Q", dim, 2}));
456+
assume_orthogonal(Q);
457+
EXPECT_TRUE(is_same<identity_tensor>(trans(Q) * Q));
458+
EXPECT_TRUE(is_same<identity_tensor>(Q * trans(Q)));
459+
}
460+
461+
TEST_F(TensorSpacePropagationTest, DistinctSymmetricProductStaysNonSymmetric) {
462+
// Safety: A*B of two DISTINCT symmetric tensors is symmetric only if they
463+
// commute — the Gram rule must not over-generalize to any product.
464+
auto B = std::get<0>(make_tensor_variable(std::tuple{"B", dim, 2}));
465+
assume_symmetric(B);
466+
EXPECT_FALSE(is_symmetric(C * B));
467+
EXPECT_FALSE(is_positive_semidefinite(C * B));
468+
}
469+
470+
// ═══════════════════════════════════════════════════════════════════════════════
471+
// Symmetric part (#390): trans(X)+X and X+trans(X) are symmetric for any X
472+
// ═══════════════════════════════════════════════════════════════════════════════
473+
474+
TEST_F(TensorSpacePropagationTest, SymmetricPartXPlusTransXIsSymmetric) {
475+
// X + trans(X) = 2*sym(X); symmetric in any dimension.
476+
EXPECT_TRUE(is_symmetric(X + trans(X)));
477+
EXPECT_TRUE(is_symmetric(trans(X) + X));
478+
EXPECT_FALSE(is_skew(X + trans(X)));
479+
}
480+
481+
TEST_F(TensorSpacePropagationTest, SymmetricPartDoesNotStealTheSkewSpelling) {
482+
// Safety: trans(X)-X and trans(X)+(-X) must remain skew — the skew branch
483+
// is checked before the symmetric-part branch.
484+
EXPECT_TRUE(is_skew(trans(X) - X));
485+
EXPECT_TRUE(is_skew(trans(X) + (-X)));
486+
EXPECT_TRUE(is_skew(X - trans(X)));
487+
}
488+
489+
TEST_F(TensorSpacePropagationTest, GeneralSumStaysUnannotated) {
490+
// Safety: a generic X+Y (Y not trans(X)) carries no space.
491+
auto Y = std::get<0>(make_tensor_variable(std::tuple{"Y", dim, 2}));
492+
EXPECT_FALSE(is_symmetric(X + Y));
493+
EXPECT_FALSE(is_skew(X + Y));
494+
}
495+
427496
} // namespace numsim::cas
428497

429498
#endif // TENSORSPACEPROPAGATIONTEST_H

0 commit comments

Comments
 (0)