Skip to content

Commit ce2e979

Browse files
committed
Fix #420: t2s function folds under the rule contract + close trans gaps
Extract the construction folds of the t2s functions (trace/det/norm/dot/ dot_product) from the tensor_to_scalar_functions.cpp if-chains into named, group-tagged try_* rules under the uniform contract `std::optional<result> try_<name>(tensor_holder const&)`, cataloged in tensor_to_scalar/simplifier/tensor_to_scalar_function_rules.h with bodies in the matching .cpp (operator ADL — the same .h/.cpp split as scalar_function_rules). The functions drive the rules in sequence at construction; det's terminal tensor_det node + PD/PSD annotation stays in det() (not an early-return fold). Extraction is behavior-identical — the existing Trace/Det/Norm simplification tests are unchanged. Two new always-on canonicalizers close transpose gaps (both transpose-invariant): trace(Aᵀ)→trace(A), ‖Aᵀ‖→‖A‖, matched on permute_indices_wrapper{2,1} like the existing det(trans A) fold. Reordering (trace(AB)=trace(BA)) is non-confluent and deferred to an opt-in pass. New tests: TraceNormTransGaps (end-to-end) and FunctionRulesUnitCoverage (each rule fires/declines; transpose rules guarded for dim≥2). Full suite 2298/2298; g++-14 -Wall -Wextra -Werror clean. Second domain of #417. Signed-off-by: Peter Lenz <peterlenz89.pl@gmail.com>
1 parent 62aa1a9 commit ce2e979

4 files changed

Lines changed: 351 additions & 107 deletions

File tree

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
#ifndef TENSOR_TO_SCALAR_FUNCTION_RULES_H
2+
#define TENSOR_TO_SCALAR_FUNCTION_RULES_H
3+
4+
#include <optional>
5+
6+
#include <numsim_cas/tensor/tensor_expression.h>
7+
#include <numsim_cas/tensor_to_scalar/tensor_to_scalar_expression.h>
8+
9+
// Construction-time fold rules for the t2s functions (#417 / #420).
10+
//
11+
// Rule contract: each rule is a named, group-tagged free function
12+
// std::optional<result> try_<name>(tensor_holder const& arg);
13+
// returning nullopt when it does not fire. The trace/det/norm/dot functions in
14+
// tensor_to_scalar_functions.cpp drive the applicable rules in sequence at
15+
// construction. Bodies live in the matching .cpp because they call the t2s /
16+
// tensor / scalar operators (operator*, /, pow, abs), which must be visible via
17+
// ADL there — the same .h-decl / .cpp-def split as scalar_function_rules.
18+
//
19+
// These rules are cross-domain: they take a tensor and yield a t2s scalar. The
20+
// catalog is the enumerable list a later registry would consume.
21+
namespace numsim::cas::t2s_rules {
22+
23+
using tensor_holder = expression_holder<tensor_expression>;
24+
using result = expression_holder<tensor_to_scalar_expression>;
25+
26+
// ── dot / dot_product [dot] ──────────────────────────────────────────
27+
std::optional<result> try_dot_product_zero(tensor_holder const &lhs,
28+
tensor_holder const &rhs);
29+
std::optional<result> try_dot_zero(tensor_holder const &e); // dot(0) → 0
30+
31+
// ── trace [trace] ────────────────────────────────────────────────────
32+
std::optional<result> try_trace_zero(tensor_holder const &e); // tr(0) → 0
33+
std::optional<result> try_trace_identity(tensor_holder const &e); // tr(I) → dim
34+
std::optional<result>
35+
try_trace_of_trans(tensor_holder const &e); // tr(Aᵀ) → tr(A)
36+
std::optional<result>
37+
try_trace_scalar_mul(tensor_holder const &e); // tr(s·A) → s·tr(A)
38+
std::optional<result>
39+
try_trace_add(tensor_holder const &e); // tr(A+B) → tr(A)+tr(B)
40+
41+
// ── norm [norm] ──────────────────────────────────────────────────────
42+
std::optional<result> try_norm_zero(tensor_holder const &e); // ‖0‖ → 0
43+
std::optional<result> try_norm_of_trans(tensor_holder const &e); // ‖Aᵀ‖ → ‖A‖
44+
std::optional<result>
45+
try_norm_scalar_mul(tensor_holder const &e); // ‖s·A‖ → |s|·‖A‖
46+
47+
// ── det [det] ────────────────────────────────────────────────────────
48+
std::optional<result> try_det_zero(tensor_holder const &e); // det(0) → 0
49+
std::optional<result> try_det_identity(tensor_holder const &e); // det(I) → 1
50+
std::optional<result>
51+
try_det_chirality(tensor_holder const &e); // proper→1, improper→-1
52+
std::optional<result>
53+
try_det_inv(tensor_holder const &e); // det(A⁻¹) → 1/det(A)
54+
std::optional<result> try_det_trans(tensor_holder const &e); // det(Aᵀ) → det(A)
55+
std::optional<result>
56+
try_det_outer_product(tensor_holder const &e); // det(u⊗v) → 0, dim≥2
57+
std::optional<result>
58+
try_det_scalar_mul(tensor_holder const &e); // det(s·A) → sᵈ·det(A)
59+
std::optional<result>
60+
try_det_mul(tensor_holder const &e); // det(∏Aᵢ) → ∏det(Aᵢ)
61+
62+
} // namespace numsim::cas::t2s_rules
63+
64+
#endif // TENSOR_TO_SCALAR_FUNCTION_RULES_H
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
#include <numsim_cas/tensor_to_scalar/simplifier/tensor_to_scalar_function_rules.h>
2+
3+
#include <numsim_cas/tensor_to_scalar/tensor_to_scalar_definitions.h>
4+
#include <numsim_cas/tensor_to_scalar/tensor_to_scalar_functions.h>
5+
#include <numsim_cas/tensor_to_scalar/tensor_to_scalar_operators.h>
6+
7+
#include <numsim_cas/basic_functions.h>
8+
#include <numsim_cas/scalar/scalar_std.h>
9+
#include <numsim_cas/tensor/tensor_assume.h>
10+
#include <numsim_cas/tensor/tensor_definitions.h>
11+
#include <numsim_cas/tensor/tensor_operators.h>
12+
13+
#include <ranges>
14+
15+
// Bodies mirror the previous inline folds in tensor_to_scalar_functions.cpp
16+
// exactly; #420 extracts them into named, testable rules. The two *_of_trans
17+
// rules are new (transpose-invariance gaps). The det terminal node + its PD/PSD
18+
// annotation stays in det() — it is not an early-return fold.
19+
20+
namespace numsim::cas::t2s_rules {
21+
22+
// ── dot / dot_product ────────────────────────────────────────────────
23+
std::optional<result> try_dot_product_zero(tensor_holder const &lhs,
24+
tensor_holder const &rhs) {
25+
if (is_same<tensor_zero>(lhs) || is_same<tensor_zero>(rhs))
26+
return make_expression<tensor_to_scalar_zero>();
27+
return {};
28+
}
29+
std::optional<result> try_dot_zero(tensor_holder const &e) {
30+
if (is_same<tensor_zero>(e))
31+
return make_expression<tensor_to_scalar_zero>();
32+
return {};
33+
}
34+
35+
// ── trace ────────────────────────────────────────────────────────────
36+
std::optional<result> try_trace_zero(tensor_holder const &e) {
37+
if (is_same<tensor_zero>(e))
38+
return make_expression<tensor_to_scalar_zero>();
39+
return {};
40+
}
41+
std::optional<result> try_trace_identity(tensor_holder const &e) {
42+
// tr(I) = dim. Rank-2 asserted by the caller, so any identity_tensor here
43+
// is the rank-2 Kronecker delta (#188 unified kronecker_delta).
44+
if (is_same<identity_tensor>(e)) {
45+
auto dim = e.get().dim();
46+
return make_expression<tensor_to_scalar_scalar_wrapper>(
47+
make_expression<scalar_constant>(static_cast<int>(dim)));
48+
}
49+
return {};
50+
}
51+
std::optional<result> try_trace_of_trans(tensor_holder const &e) {
52+
// tr(Aᵀ) = tr(A). trans() builds permute_indices_wrapper{2,1}; match the
53+
// index sequence so a non-transpose permutation is not mis-simplified.
54+
if (is_same<permute_indices_wrapper>(e)) {
55+
auto const &perm = e.get<permute_indices_wrapper>();
56+
if (perm.indices() == sequence{2, 1})
57+
return trace(perm.expr());
58+
}
59+
return {};
60+
}
61+
std::optional<result> try_trace_scalar_mul(tensor_holder const &e) {
62+
if (is_same<tensor_scalar_mul>(e)) {
63+
auto const &sm = e.get<tensor_scalar_mul>();
64+
return sm.expr_lhs() * trace(sm.expr_rhs());
65+
}
66+
return {};
67+
}
68+
std::optional<result> try_trace_add(tensor_holder const &e) {
69+
if (is_same<tensor_add>(e)) {
70+
auto const &add = e.get<tensor_add>();
71+
result r;
72+
if (add.coeff().is_valid())
73+
r = trace(add.coeff());
74+
for (auto const &child : add.symbol_map() | std::views::values) {
75+
if (r.is_valid())
76+
r = r + trace(child);
77+
else
78+
r = trace(child);
79+
}
80+
return r;
81+
}
82+
return {};
83+
}
84+
85+
// ── norm ─────────────────────────────────────────────────────────────
86+
std::optional<result> try_norm_zero(tensor_holder const &e) {
87+
if (is_same<tensor_zero>(e))
88+
return make_expression<tensor_to_scalar_zero>();
89+
return {};
90+
}
91+
std::optional<result> try_norm_of_trans(tensor_holder const &e) {
92+
// ‖Aᵀ‖ = ‖A‖ — the Frobenius norm is transpose-invariant.
93+
if (is_same<permute_indices_wrapper>(e)) {
94+
auto const &perm = e.get<permute_indices_wrapper>();
95+
if (perm.indices() == sequence{2, 1})
96+
return norm(perm.expr());
97+
}
98+
return {};
99+
}
100+
std::optional<result> try_norm_scalar_mul(tensor_holder const &e) {
101+
if (is_same<tensor_scalar_mul>(e)) {
102+
auto const &sm = e.get<tensor_scalar_mul>();
103+
return abs(sm.expr_lhs()) * norm(sm.expr_rhs());
104+
}
105+
return {};
106+
}
107+
108+
// ── det ──────────────────────────────────────────────────────────────
109+
std::optional<result> try_det_zero(tensor_holder const &e) {
110+
if (is_same<tensor_zero>(e))
111+
return make_expression<tensor_to_scalar_zero>();
112+
return {};
113+
}
114+
std::optional<result> try_det_identity(tensor_holder const &e) {
115+
if (is_same<identity_tensor>(e))
116+
return make_expression<tensor_to_scalar_one>();
117+
return {};
118+
}
119+
std::optional<result> try_det_chirality(tensor_holder const &e) {
120+
// det of an orthogonal tensor is ±1, resolved by chirality (#269):
121+
// proper → +1, improper → −1, bare orthogonal → NO fold (sign unknown).
122+
if (is_proper_rotation(e))
123+
return make_expression<tensor_to_scalar_one>();
124+
if (is_improper_rotation(e))
125+
return -make_expression<tensor_to_scalar_one>();
126+
return {};
127+
}
128+
std::optional<result> try_det_inv(tensor_holder const &e) {
129+
// det(A⁻¹) = 1/det(A), routed through t2s div → canonical pow(det(A),-1).
130+
if (is_same<tensor_inv>(e)) {
131+
auto const &inner = e.get<tensor_inv>().expr();
132+
return make_expression<tensor_to_scalar_one>() / det(inner);
133+
}
134+
return {};
135+
}
136+
std::optional<result> try_det_trans(tensor_holder const &e) {
137+
if (is_same<permute_indices_wrapper>(e)) {
138+
auto const &perm = e.get<permute_indices_wrapper>();
139+
if (perm.indices() == sequence{2, 1})
140+
return det(perm.expr());
141+
}
142+
return {};
143+
}
144+
std::optional<result> try_det_outer_product(tensor_holder const &e) {
145+
// det(u ⊗ v) = 0 for dim ≥ 2 (rank-1 matrix). dim = 1 is the 1×1 scalar.
146+
if (is_same<outer_product_wrapper>(e) && e.get().dim() >= 2)
147+
return make_expression<tensor_to_scalar_zero>();
148+
return {};
149+
}
150+
std::optional<result> try_det_scalar_mul(tensor_holder const &e) {
151+
if (is_same<tensor_scalar_mul>(e)) {
152+
auto const &sm = e.get<tensor_scalar_mul>();
153+
auto dim = static_cast<int>(sm.expr_rhs().get().dim());
154+
auto dim_expr = make_expression<scalar_constant>(dim);
155+
return pow(sm.expr_lhs(), std::move(dim_expr)) * det(sm.expr_rhs());
156+
}
157+
return {};
158+
}
159+
std::optional<result> try_det_mul(tensor_holder const &e) {
160+
if (is_same<tensor_mul>(e)) {
161+
auto const &mul = e.get<tensor_mul>();
162+
result r;
163+
if (mul.coeff().is_valid())
164+
r = det(mul.coeff());
165+
for (auto const &child : mul.data()) {
166+
if (r.is_valid())
167+
r = r * det(child);
168+
else
169+
r = det(child);
170+
}
171+
return r;
172+
}
173+
return {};
174+
}
175+
176+
} // namespace numsim::cas::t2s_rules

0 commit comments

Comments
 (0)