diff --git a/include/numsim-materials/default_materials.h b/include/numsim-materials/default_materials.h index 239f73c..54be2f4 100644 --- a/include/numsim-materials/default_materials.h +++ b/include/numsim-materials/default_materials.h @@ -4,10 +4,20 @@ #include #include "numsim-materials/core/material_base.h" #include "numsim-materials/solvers/backward_euler.h" +#include "numsim-materials/solvers/local_newton.h" #include "numsim-materials/solvers/vector_newton.h" #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/j2_plasticity.h" +#include "numsim-materials/materials/j2_rk_plasticity.h" +#include "numsim-materials/materials/drucker_prager_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,10 +85,42 @@ void register_default_materials() { factory.template register_type>("linear_elasticity"); factory.template register_type>("constant_scalar"); factory.template register_type>("props_scalar"); + + // Plasticity. Each model is its own class -- j2_plasticity, + // j2_rk_plasticity and drucker_prager_plasticity -- rather than a template + // over the yield-function type, so the registrable names are the classes. + factory.template register_type>("j2_plasticity"); + factory.template register_type>("j2_rk_plasticity"); + + // Drucker-Prager was held back while its cone parameters arrived inside a + // C++ "yield_function" object the JSON reader could not convert: a document + // naming it would have got a DEFAULT-constructed one -- eta = beta = k = 0 -- + // which builds, runs, never yields, and is indistinguishable from + // elasticity. #43 made eta, beta and K_bulk plain required scalars, so a + // deck now either supplies them or gets a named missing-parameter error. + // That was the stated condition for registering it (closes #33). + factory.template register_type>( + "drucker_prager_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"); factory.template register_type>("backward_euler"); + // local_newton is the solver every return map names through "solver_source". + // Registering the plasticity classes without it leaves them unreachable from + // a document anyway: the deck can name j2_plasticity but not the solver it + // requires. + factory.template register_type>("local_newton"); factory.template register_type>("tensor_component_stepper_rank1"); factory.template register_type>("tensor_component_stepper_rank2"); factory.template register_type>("scalar_identity_weight"); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 2460296..946e860 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) add_numsim_test(test_uncovered_materials test_uncovered_materials.cpp) add_numsim_test(test_weighted_sum test_weighted_sum.cpp) target_link_libraries(test_umat_interface PRIVATE Threads::Threads) diff --git a/tests/test_material_registry.cpp b/tests/test_material_registry.cpp new file mode 100644 index 0000000..d54f67b --- /dev/null +++ b/tests/test_material_registry.cpp @@ -0,0 +1,131 @@ +#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", "local_newton", + "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":"local_newton","name":"solver"}, + {"type":"linear_isotropic_hardening","name":"hardening", + "source":"j2","K":1000.0}, + {"type":"j2_plasticity","name":"j2", + "hardening_source":"hardening","strain_source":"stepper", + "solver_source":"solver","K":166.67,"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, from a document, driven to yield. +/// +/// It was held back while eta, beta and K_bulk arrived inside a C++ +/// "yield_function" object the JSON reader could not convert: a document naming +/// it would have got a default-constructed cone -- eta = beta = k = 0 -- which +/// builds, runs, never yields, and is indistinguishable from elasticity. #43 +/// made them plain required scalars. This is the test that the condition is +/// actually met, rather than that the type merely appears in the factory. +TEST(MaterialRegistry, ADruckerPragerModelRunsFromADocumentAlone) { + const char* doc = R"([ + {"type":"tensor_component_stepper_rank2","name":"stepper", + "increment":0.01,"indices":[0,0]}, + {"type":"local_newton","name":"solver"}, + {"type":"linear_isotropic_hardening","name":"hardening", + "source":"dp","K":500.0}, + {"type":"drucker_prager_plasticity","name":"dp", + "hardening_source":"hardening","strain_source":"stepper", + "solver_source":"solver","G":76.92,"sigma_0":20.0, + "eta":0.1,"beta":0.05,"K_bulk":166.67} + ])"; + + 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("dp", "equivalent_plastic_strain"), 1e-6) + << "the document built, but the cone never yielded -- the silently " + "elastic failure this material was held back for"; +} + +/// And the reason it is safe to register: an omitted cone parameter stops the +/// build. This is the guarantee that replaced "keep it out of the factory" -- +/// previously a document naming drucker_prager_plasticity got a +/// default-constructed cone with eta = beta = k = 0 and ran as elasticity. +/// +/// The exception carries only a COUNT -- "missing 1 required parameter(s)" -- +/// not the name. numsim-core's input_parameter_controller prints the names to +/// stdout and throws the count separately, so a deck typo is loud but not +/// self-explanatory. That is a numsim-core change, tracked separately; what +/// matters here is that the cone cannot be built without its parameters. +TEST(MaterialRegistry, ADruckerPragerDocumentMissingEtaFailsLoudly) { + const char* doc = R"([ + {"type":"tensor_component_stepper_rank2","name":"stepper", + "increment":0.01,"indices":[0,0]}, + {"type":"local_newton","name":"solver"}, + {"type":"linear_isotropic_hardening","name":"hardening", + "source":"dp","K":500.0}, + {"type":"drucker_prager_plasticity","name":"dp", + "hardening_source":"hardening","strain_source":"stepper", + "solver_source":"solver","G":76.92,"sigma_0":20.0, + "beta":0.05,"K_bulk":166.67} + ])"; + + nm::material_context ctx; + EXPECT_THROW( + { + for (const auto& m : nlohmann::json::parse(doc)) + nm::create_from_json(ctx, m); + }, + std::exception) + << "a cone with no friction coefficient must not build silently"; + EXPECT_EQ(ctx.find("dp"), nullptr) + << "the failed material must not be left behind in the context"; +} + +} // namespace