diff --git a/include/numsim-materials/umat/umat_interface.h b/include/numsim-materials/umat/umat_interface.h index b10ce03..3e1859c 100644 --- a/include/numsim-materials/umat/umat_interface.h +++ b/include/numsim-materials/umat/umat_interface.h @@ -207,10 +207,9 @@ class umat_registry { std::unique_ptr ctx; std::unique_ptr solid; std::unique_ptr 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 props; }; static std::unordered_mapsecond.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; } @@ -269,7 +277,7 @@ class umat_registry { thread_state ts; ts.ctx = std::make_unique(); 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"); diff --git a/tests/test_umat_interface.cpp b/tests/test_umat_interface.cpp index 72dafc2..5f7078c 100644 --- a/tests/test_umat_interface.cpp +++ b/tests/test_umat_interface.cpp @@ -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; @@ -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 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; }