Skip to content
Open
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
38 changes: 23 additions & 15 deletions include/numsim-materials/umat/umat_interface.h
Original file line number Diff line number Diff line change
Expand Up @@ -207,10 +207,9 @@ class umat_registry {
std::unique_ptr<context_type> ctx;
std::unique_ptr<evaluator_type> solid;
std::unique_ptr<ps_evaluator_type> ps;
/// How many constants the context was built from. The graph is built once
/// and reused, so a later call arriving with a different count would mean
/// the cached parameters no longer describe this material.
std::size_t nprops{0};
/// What the context was built from; the graph is reused, so different
/// constants on a later call would no longer describe this material.
std::vector<double> props;
};

static std::unordered_map<std::string, thread_state, transparent_string_hash,
Expand All @@ -229,17 +228,26 @@ class umat_registry {
const auto key = normalise_cmname(cmname.data(), cmname.size(), buf);
auto& cache = thread_cache();
if (auto it = cache.find(key); it != cache.end()) {
// PROPS cannot vary for a given material name — two *MATERIAL blocks must
// have distinct names — so a changed count means the deck contradicts the
// cached graph. Checking the size is one comparison; checking the values
// is not worth it per integration point.
if (it->second.nprops != props.size())
// PROPS cannot vary for one material name, so anything different
// contradicts the graph the constants were baked into. Values, not just
// the count: same length with different numbers is the case that reaches
// a material. Two faults, two messages — the check only ever explains.
if (it->second.props.size() != props.size())
throw fatal_error(
"numsim UMAT: material '" + std::string(key) +
"' was built from " + std::to_string(it->second.nprops) +
" constants but this call supplies " +
std::to_string(props.size()) +
" — PROPS must be constant for a given material name");
"numsim UMAT: material '" + std::string(key) + "' was built from " +
std::to_string(it->second.props.size()) +
" constants but this call supplies " + std::to_string(props.size()) +
" — NPROPS cannot vary for a given material name");

for (std::size_t i = 0; i < props.size(); ++i)
if (it->second.props[i] != props[i])
throw fatal_error(
"numsim UMAT: material '" + std::string(key) + "' constant " +
std::to_string(i + 1) + " was baked into the graph as " +
std::to_string(it->second.props[i]) + " but this call supplies " +
std::to_string(props[i]) +
" — PROPS must be constant for a given material name; use "
"distinct *MATERIAL names for distinct constants");
return it->second;
}

Expand Down Expand Up @@ -269,7 +277,7 @@ class umat_registry {
thread_state ts;
ts.ctx = std::make_unique<context_type>();
m.build(*ts.ctx, props);
ts.nprops = props.size();
ts.props.assign(props.begin(), props.end());
if (!ts.ctx->is_finalized())
throw fatal_error(
"the builder returned without calling finalize() on the context");
Expand Down
41 changes: 40 additions & 1 deletion tests/test_umat_interface.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,9 @@ struct Registration {
// against an already-built name the NPROPS-consistency check fires first
// and require_props is never reached.
registry::instance().register_model("COLDNAME", build_deck_elastic, de);
// One test only: it warms the cache itself, so a shared name would let
// test order decide the outcome.
registry::instance().register_model("VALUEPROBE", build_deck_elastic, de);

// Deliberately lower-case, to prove the registry folds case on both sides.
registry::config lc;
Expand Down Expand Up @@ -765,7 +768,43 @@ TEST(UmatInterface, ChangingNpropsForTheSameNameIsFatal) {
call_umat("STIFF", stress, statev.data(), ddsdde, stran, dstran, 0.0, 0.1, 3,
3, 6, 0, &pnewdt, nullptr, nullptr, nullptr, nullptr, three, 3);
EXPECT_EQ(FatalProbe::count, 1);
EXPECT_NE(FatalProbe::last.find("constant"), std::string::npos)
// Both counts, so NPROPS=3 is not left to be matched against one number.
EXPECT_NE(FatalProbe::last.find("2 constants"), std::string::npos)
<< FatalProbe::last;
EXPECT_NE(FatalProbe::last.find("supplies 3"), std::string::npos)
<< FatalProbe::last;
}

/// Same count, different numbers — the case that reaches a material. A count
/// check accepts it and serves the first call's stiffness forever: a converged
/// analysis with the wrong moduli.
TEST(UmatInterface, ChangingPropsValuesForTheSameNameIsFatal) {
FatalProbe probe;
std::vector<T> statev(1, 0.0);
const T stran[6] = {0, 0, 0, 0, 0, 0};
const T dstran[6] = {0.001, 0, 0, 0, 0, 0};
T stress[6] = {0}, ddsdde[36] = {0}, pnewdt = 1.0;
const T soft[2] = {100.0, 40.0};
const T stiff[2] = {300.0, 140.0};

// Builds the context, and shows which constants it was built from.
call_umat("VALUEPROBE", stress, statev.data(), ddsdde, stran, dstran, 0.0, 0.1,
3, 3, 6, 0, &pnewdt, nullptr, nullptr, nullptr, nullptr, soft, 2);
ASSERT_EQ(FatalProbe::count, 0);
ASSERT_NEAR(ddsdde[0], 100.0 + 4.0 * 40.0 / 3.0, 1e-9);

// Same name, same count, different values.
call_umat("VALUEPROBE", stress, statev.data(), ddsdde, stran, dstran, 0.0, 0.1,
3, 3, 6, 0, &pnewdt, nullptr, nullptr, nullptr, nullptr, stiff, 2);
EXPECT_EQ(FatalProbe::count, 1)
<< "a same-length PROPS with different numbers must be reported, not "
"silently served from the cached graph";
// Must name which constant disagrees, and both values.
EXPECT_NE(FatalProbe::last.find("constant 1"), std::string::npos)
<< FatalProbe::last;
EXPECT_NE(FatalProbe::last.find("100.0"), std::string::npos)
<< FatalProbe::last;
EXPECT_NE(FatalProbe::last.find("300.0"), std::string::npos)
<< FatalProbe::last;
}

Expand Down
Loading