Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions include/numsim_cas/core/symbol_base.h
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ bool operator<(symbol_base<BaseExprT> const &lhs,
if (lhs.hash_value() != rhs.hash_value())
return lhs.hash_value() < rhs.hash_value();
// The hash covers only the name, so colliding names need a real tiebreak.
// A symbol with more identity state defines its own comparison.
return lhs.name() < rhs.name();
}

Expand Down
26 changes: 26 additions & 0 deletions include/numsim_cas/tensor/tensor.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include <numsim_cas/core/symbol_base.h>
#include <numsim_cas/tensor/tensor_expression.h>
#include <ostream>
#include <utility>

namespace numsim::cas {

Expand All @@ -30,6 +31,31 @@ class tensor final : public symbol_base<tensor_node_base_t<tensor>> {
// re-override here; symbol_base is the single source of truth for the
// Symbol classification.

// Shape is part of the identity: the same name at another dim or rank is
// a different tensor. The hash stays name-only, as a fast reject.
friend bool operator==(tensor const &lhs, tensor const &rhs) {
return static_cast<base const &>(lhs) == static_cast<base const &>(rhs) &&
lhs.dim() == rhs.dim() && lhs.rank() == rhs.rank();
}

friend bool operator!=(tensor const &lhs, tensor const &rhs) {
return !(lhs == rhs);
}

friend bool operator<(tensor const &lhs, tensor const &rhs) {
auto const &l = static_cast<base const &>(lhs);
auto const &r = static_cast<base const &>(rhs);
if (l < r)
return true;
if (r < l)
return false;
return std::pair{lhs.dim(), lhs.rank()} < std::pair{rhs.dim(), rhs.rank()};
}

friend bool operator>(tensor const &lhs, tensor const &rhs) {
return rhs < lhs;
}

// const tensor &operator=(expression_holder<tensor_expression> &&data) {
// this->m_expr = std::move(data);
// return *this;
Expand Down
70 changes: 70 additions & 0 deletions tests/CoreBugFixTest.h
Original file line number Diff line number Diff line change
Expand Up @@ -1034,6 +1034,76 @@ TEST(SymbolIdentity, EvaluatorKeepsBothDomainBindings) {
3.0);
}

// A tensor symbol's shape is part of its identity: the same name at a
// different dim or rank denotes a different tensor.
// Mixed-rank factors sharing a name must not merge into a power: tensor
// multiplication validates dim only, so this reaches the like-term path.
TEST(SymbolIdentity, MixedRankFactorsDoNotMerge) {
auto A32 = make_expression<tensor>("A", 3, 2);
auto A34 = make_expression<tensor>("A", 3, 4);
auto product = A32 * A34;
EXPECT_FALSE(is_same<tensor_pow>(product)) << to_string(product);
EXPECT_EQ(to_string(product), to_string(A32 * A34));
}

TEST(SymbolIdentity, TensorShapeDistinguishesSameName) {
auto A32 = make_expression<tensor>("A", 3, 2);
auto A22 = make_expression<tensor>("A", 2, 2);
auto A34 = make_expression<tensor>("A", 3, 4);
auto B32 = make_expression<tensor>("B", 3, 2);
auto A32b = make_expression<tensor>("A", 3, 2);

EXPECT_FALSE(*A32 == *A22) << "differing dim";
EXPECT_FALSE(*A32 == *A34) << "differing rank";
EXPECT_TRUE(*A32 == *A32b) << "same name and shape";
EXPECT_FALSE(*A32 == *B32) << "differing name";

// Distinct shapes need a total order, equal ones must stay incomparable.
EXPECT_TRUE((*A32 < *A22) != (*A22 < *A32));
EXPECT_TRUE((*A32 < *A34) != (*A34 < *A32));
EXPECT_FALSE(*A32 < *A32b);
EXPECT_FALSE(*A32b < *A32);

std::map<expression_holder<tensor_expression>, int> keys;
keys[A32] = 1;
keys[A22] = 2;
keys[A34] = 3;
EXPECT_EQ(keys.size(), 3u);
EXPECT_EQ(keys.at(A32), 1);
EXPECT_EQ(keys.at(A22), 2);
EXPECT_EQ(keys.at(A34), 3);
}

// Each shape carries its own binding; the evaluator must not serve one
// symbol's data for another.
TEST(SymbolIdentity, EvaluatorKeepsBindingsPerTensorShape) {
auto A32 = make_expression<tensor>("A", 3, 2);
auto A22 = make_expression<tensor>("A", 2, 2);

auto d32 = std::make_shared<tensor_data<double, 3, 2>>();
d32->data() = tmech::eye<double, 3, 2>();
auto d22 = std::make_shared<tensor_data<double, 2, 2>>();
d22->data() = 7.0 * tmech::eye<double, 2, 2>();

tensor_evaluator<double> ev;
ev.set(A32, d32);
ev.set(A22, d22);

auto r32 = ev.apply(A32);
ASSERT_NE(r32, nullptr);
EXPECT_EQ(r32->dim(), 3u);
EXPECT_DOUBLE_EQ(r32->raw_data()[0], 1.0);

auto r22 = ev.apply(A22);
ASSERT_NE(r22, nullptr);
EXPECT_EQ(r22->dim(), 2u);
EXPECT_DOUBLE_EQ(r22->raw_data()[0], 7.0);

tensor_to_scalar_evaluator<double> t2s;
t2s.set(A22, d22);
EXPECT_DOUBLE_EQ(t2s.apply(trace(A22)), 14.0);
}

// #93 — a tensor_mul's space() must survive copy reconstruction
// (tensor_add did this; mul dropped it).
TEST(CoreBugFix, TensorMulCopyPreservesSpaceAnnotation) {
Expand Down
Loading