From ccf15f6d4941dada3de06b0108a48ad13ef3b3f6 Mon Sep 17 00:00:00 2001 From: petlenz Date: Sun, 23 Aug 2026 00:38:44 +0200 Subject: [PATCH 1/2] materials: register plasticity, hardening, damage and state functions Eight material classes were missing from register_default_materials(), so they could not be named in a JSON document at all. Two of them are small_strain_plasticity and linear_isotropic_hardening, which means a document could build elasticity and damage and NOT J2 plasticity -- the library's flagship model -- against a stated goal of being config driven. Registration is a hand-maintained list, and every test that uses these materials constructs them in C++ through ctx.create(), never through the factory. So the tests passed, the materials worked, and the only broken thing was the path the document layer depends on. small_strain_plasticity and rk_plasticity are templates over the yield-function TYPE, so the registrable names are the concrete aliases: j2_plasticity, drucker_prager_plasticity, j2_rk_plasticity. drucker_prager_plasticity is registered but NOT yet fully configurable from a document. Its yield function carries eta, beta and K_bulk and arrives as a C++ object through an undeclared "yield_function" parameter, which the JSON reader cannot convert. A document can name it; it cannot set those three. That is recorded in a test rather than left to be discovered. Three tests: the names resolve; a J2 model built entirely from a document yields (registration alone would pass the name check while still failing here if a parameter were not convertible); and Drucker-Prager's limitation is pinned, so the test fails if yield_function ever becomes expressible and the note goes stale. Closes #33. --- include/numsim-materials/default_materials.h | 33 +++++++++ tests/CMakeLists.txt | 1 + tests/test_material_registry.cpp | 77 ++++++++++++++++++++ 3 files changed, 111 insertions(+) create mode 100644 tests/test_material_registry.cpp diff --git a/include/numsim-materials/default_materials.h b/include/numsim-materials/default_materials.h index d433970..7b3ad10 100644 --- a/include/numsim-materials/default_materials.h +++ b/include/numsim-materials/default_materials.h @@ -8,6 +8,14 @@ #include "numsim-materials/materials/scalar_stepper.h" #include "numsim-materials/materials/constant_scalar.h" #include "numsim-materials/materials/props_scalar.h" +#include "numsim-materials/materials/small_strain_plasticity.h" +#include "numsim-materials/materials/rk_plasticity.h" +#include "numsim-materials/materials/linear_isotropic_hardening.h" +#include "numsim-materials/materials/exponential_isotropic_hardening.h" +#include "numsim-materials/materials/linear_damage_law.h" +#include "numsim-materials/materials/curing_rate.h" +#include "numsim-materials/materials/strain_energy_state_function.h" +#include "numsim-materials/materials/vector_strain_state_function.h" #include "numsim-materials/materials/isotropic_tangent.h" #include "numsim-materials/materials/linear_elasticity.h" #include "numsim-materials/materials/linear_stress.h" @@ -75,6 +83,31 @@ void register_default_materials() { factory.template register_type>("linear_elasticity"); factory.template register_type>("constant_scalar"); factory.template register_type>("props_scalar"); + + // Plasticity. small_strain_plasticity and rk_plasticity are templates over + // the yield-function TYPE, so the registrable names are the concrete + // aliases, not the templates. + // + // drucker_prager_plasticity is registered but is NOT yet fully configurable + // from a document: its yield function carries eta, beta and K_bulk, supplied + // through an undeclared "yield_function" parameter as a C++ object, and the + // JSON reader has no converter for one. A document can name it; it cannot + // yet set those three. j2_plasticity has no such state and is complete. + factory.template register_type>("j2_plasticity"); + factory.template register_type>( + "drucker_prager_plasticity"); + factory.template register_type>("j2_rk_plasticity"); + + factory.template register_type>( + "linear_isotropic_hardening"); + factory.template register_type>( + "exponential_isotropic_hardening"); + factory.template register_type>("linear_damage_law"); + factory.template register_type>("curing_rate"); + factory.template register_type>( + "strain_energy_state_function"); + factory.template register_type>( + "vector_strain_state_function"); factory.template register_type>("isotropic_tangent"); factory.template register_type>("linear_stress"); factory.template register_type>("autocatalytic_reaction"); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 4425a79..7f1d82b 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -26,6 +26,7 @@ add_numsim_test(test_umat_interface test_umat_interface.cpp) add_numsim_test(test_tangent_generator test_tangent_generator.cpp) add_numsim_test(test_json_model test_json_model.cpp) add_numsim_test(test_props_scalar test_props_scalar.cpp) +add_numsim_test(test_material_registry test_material_registry.cpp) target_link_libraries(test_umat_interface PRIVATE Threads::Threads) # Data dumper for plotting (not a test — standalone executable) diff --git a/tests/test_material_registry.cpp b/tests/test_material_registry.cpp new file mode 100644 index 0000000..399f470 --- /dev/null +++ b/tests/test_material_registry.cpp @@ -0,0 +1,77 @@ +#include +#include +#include +#include "numsim-materials/default_materials.h" +#include "numsim-materials/io/json_material_factory.h" + +namespace { + +namespace nm = numsim::materials; +using policy = nm::material_policy_default; +using T = policy::value_type; +using factory_type = nm::object_store::factory_type; + +struct Registration { + Registration() { nm::register_default_materials(); } +}; +const Registration registration_{}; + +/// A material that is not registered cannot be named in a document, whatever +/// else works about it. These were all absent, so a JSON model could build +/// elasticity and damage and essentially nothing else. +TEST(MaterialRegistry, PlasticityAndFriendsAreReachableFromJson) { + auto& f = factory_type::instance(); + for (const char* name : {"j2_plasticity", "drucker_prager_plasticity", + "j2_rk_plasticity", "linear_isotropic_hardening", + "exponential_isotropic_hardening", "linear_damage_law", + "curing_rate", "strain_energy_state_function", + "vector_strain_state_function"}) + EXPECT_TRUE(f.contains(name)) << name << " cannot be named in a document"; +} + +/// The one that matters: a J2 model built entirely from a document, driven to +/// yield. Registration alone would pass the check above while still failing +/// here if a parameter were not JSON-convertible. +TEST(MaterialRegistry, AJ2ModelRunsFromADocumentAlone) { + const char* doc = R"([ + {"type":"tensor_component_stepper_rank2","name":"stepper", + "increment":0.01,"indices":[0,0]}, + {"type":"linear_elasticity","name":"elastic", + "strain_producer_name":"stepper","K":166.67,"G":76.92}, + {"type":"backward_euler","name":"solver"}, + {"type":"linear_isotropic_hardening","name":"hardening", + "source":"j2","K":1000.0}, + {"type":"j2_plasticity","name":"j2","elastic_source":"elastic", + "hardening_source":"hardening","strain_source":"stepper", + "solver_source":"solver","G":76.92,"sigma_0":50.0} + ])"; + + nm::material_context ctx; + ASSERT_NO_THROW({ + for (const auto& m : nlohmann::json::parse(doc)) + nm::create_from_json(ctx, m); + ctx.finalize(); + }); + + for (int i = 0; i < 40; ++i) { ctx.update(); ctx.commit(); } + EXPECT_GT(ctx.get("j2", "equivalent_plastic_strain"), 1e-6) + << "the document built, but the model never yielded"; +} + +/// drucker_prager_plasticity is reachable by name, but its yield function +/// carries eta, beta and K_bulk and arrives as a C++ object through an +/// undeclared "yield_function" parameter. A document can name the material and +/// cannot yet configure it — asserted so the limitation is recorded rather +/// than discovered. +TEST(MaterialRegistry, DruckerPragerIsNamedButNotYetConfigurableFromJson) { + auto& f = factory_type::instance(); + ASSERT_TRUE(f.contains("drucker_prager_plasticity")); + const auto schema = f.schema("drucker_prager_plasticity"); + bool has_yf = false; + for (const auto& [key, param] : schema) has_yf |= (key == "yield_function"); + EXPECT_FALSE(has_yf) + << "yield_function is now in the schema — if it became JSON-convertible, " + "this test and the note in default_materials.h should go"; +} + +} // namespace From 8e3aeb13aca76f47c10cf17c51f4be36adf68e3e Mon Sep 17 00:00:00 2001 From: petlenz Date: Sun, 23 Aug 2026 09:15:12 +0200 Subject: [PATCH 2/2] materials: do not register drucker_prager_plasticity while it cannot be configured Registering it made a document able to NAME it, and the yield function -- which carries eta, beta and K_bulk -- cannot be expressed in JSON. A document naming it therefore got a DEFAULT-constructed one: eta = beta = k = 0. Probed: it builds without error, runs 20 steps, never yields, and is indistinguishable from elasticity. That is worse than not registering it. Unregistered, a document naming it fails with an unknown material type, which says exactly what is wrong. Registered, it silently returns elastic results from a model the user believes is Drucker-Prager. The test now pins the absence and its reason, so registering it again without making yield_function expressible fails. --- include/numsim-materials/default_materials.h | 16 +++++++------ tests/test_material_registry.cpp | 25 +++++++++----------- 2 files changed, 20 insertions(+), 21 deletions(-) diff --git a/include/numsim-materials/default_materials.h b/include/numsim-materials/default_materials.h index 7b3ad10..0d0045b 100644 --- a/include/numsim-materials/default_materials.h +++ b/include/numsim-materials/default_materials.h @@ -88,16 +88,18 @@ void register_default_materials() { // the yield-function TYPE, so the registrable names are the concrete // aliases, not the templates. // - // drucker_prager_plasticity is registered but is NOT yet fully configurable - // from a document: its yield function carries eta, beta and K_bulk, supplied - // through an undeclared "yield_function" parameter as a C++ object, and the - // JSON reader has no converter for one. A document can name it; it cannot - // yet set those three. j2_plasticity has no such state and is complete. factory.template register_type>("j2_plasticity"); - factory.template register_type>( - "drucker_prager_plasticity"); factory.template register_type>("j2_rk_plasticity"); + // drucker_prager_plasticity is deliberately NOT registered. Its yield + // function carries eta, beta and K_bulk and arrives as a C++ object through + // an undeclared "yield_function" parameter that the JSON reader cannot + // convert. Registered, a document could name it and would silently get a + // DEFAULT-constructed yield function -- eta = beta = k = 0 -- which builds, + // runs, never yields, and is indistinguishable from elasticity. An unknown + // material type is a loud error naming the type; a silently elastic + // Drucker-Prager is not. Register it once the yield function is expressible. + factory.template register_type>( "linear_isotropic_hardening"); factory.template register_type>( diff --git a/tests/test_material_registry.cpp b/tests/test_material_registry.cpp index 399f470..306c790 100644 --- a/tests/test_material_registry.cpp +++ b/tests/test_material_registry.cpp @@ -21,7 +21,7 @@ const Registration registration_{}; /// elasticity and damage and essentially nothing else. TEST(MaterialRegistry, PlasticityAndFriendsAreReachableFromJson) { auto& f = factory_type::instance(); - for (const char* name : {"j2_plasticity", "drucker_prager_plasticity", + for (const char* name : {"j2_plasticity", "j2_rk_plasticity", "linear_isotropic_hardening", "exponential_isotropic_hardening", "linear_damage_law", "curing_rate", "strain_energy_state_function", @@ -58,20 +58,17 @@ TEST(MaterialRegistry, AJ2ModelRunsFromADocumentAlone) { << "the document built, but the model never yielded"; } -/// drucker_prager_plasticity is reachable by name, but its yield function -/// carries eta, beta and K_bulk and arrives as a C++ object through an -/// undeclared "yield_function" parameter. A document can name the material and -/// cannot yet configure it — asserted so the limitation is recorded rather -/// than discovered. -TEST(MaterialRegistry, DruckerPragerIsNamedButNotYetConfigurableFromJson) { +/// drucker_prager_plasticity is deliberately NOT registered. Its yield function +/// carries eta, beta and K_bulk and arrives as a C++ object the JSON reader +/// cannot convert, so a document naming it would get a default-constructed one: +/// eta = beta = k = 0, which builds, runs, never yields, and is +/// indistinguishable from elasticity. An unknown type is a loud error; a +/// silently elastic Drucker-Prager is not. +TEST(MaterialRegistry, DruckerPragerStaysUnregisteredWhileUnconfigurable) { auto& f = factory_type::instance(); - ASSERT_TRUE(f.contains("drucker_prager_plasticity")); - const auto schema = f.schema("drucker_prager_plasticity"); - bool has_yf = false; - for (const auto& [key, param] : schema) has_yf |= (key == "yield_function"); - EXPECT_FALSE(has_yf) - << "yield_function is now in the schema — if it became JSON-convertible, " - "this test and the note in default_materials.h should go"; + EXPECT_FALSE(f.contains("drucker_prager_plasticity")) + << "registered, a document could name it and silently get eta=beta=k=0; " + "register it once yield_function is expressible in JSON"; } } // namespace