@@ -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