diff --git a/src/targets/moose_material.cpp b/src/targets/moose_material.cpp index d684576..23808ba 100644 --- a/src/targets/moose_material.cpp +++ b/src/targets/moose_material.cpp @@ -342,6 +342,21 @@ auto emit_source(ConstitutiveModel const &model, std::string const &app_name, } os << "\n"; + // issue #10: the tmech::adaptor boundary assumes RankTwoTensor / + // RankFourTensor store contiguous row-major Real with no members beyond + // the data — an assumption about MOOSE internals that could silently + // break on a MOOSE upgrade. Emit it as a static_assert into the .C so a + // layout change becomes a compile error in the consumer's build, not a + // silent miscompute at the quadrature points. + os << "// Layout preconditions for the tmech::adaptor boundary — see\n"; + os << "// numsim-codegen issue #10.\n"; + os << "static_assert(sizeof(RankTwoTensor) == 9 * sizeof(Real),\n"; + os << " \"numsim-codegen assumes RankTwoTensor is contiguous " + "3x3 Real storage\");\n"; + os << "static_assert(sizeof(RankFourTensor) == 81 * sizeof(Real),\n"; + os << " \"numsim-codegen assumes RankFourTensor is contiguous " + "3x3x3x3 Real storage\");\n\n"; + // Embed the Layer 2 compute function as an anonymous-namespace inline // function at file scope. Keeps the implementation self-contained in // a single .C file without leaking the helper into other TUs. @@ -444,6 +459,35 @@ auto emit_source(ConstitutiveModel const &model, std::string const &app_name, auto MooseMaterialTarget::emit(ConstitutiveModel const &model) const -> std::vector { + // issue #7: MOOSE's RankTwoTensor/RankFourTensor are ALWAYS 3D + // (LIBMESH_DIM = 3). A dim != 3 recipe would emit tmech::adaptor reads + // with dim-2 strides over 3x3 storage — a silent miscompute, not a + // compile error. Reject loudly; plane-strain/axisymmetric models must + // be authored as dim-3 recipes (embed the 2D state in a 3x3 tensor), + // matching how MOOSE's own tensor-mechanics materials treat 2D. + auto reject_non_3d = [&](std::size_t dim, std::string const &what, + std::string const &name) { + if (dim != 3) { + throw std::runtime_error( + "MooseMaterialTarget: " + what + " '" + name + "' has dim " + + std::to_string(dim) + + ", but MOOSE RankTwoTensor/RankFourTensor storage is always 3D " + "(LIBMESH_DIM = 3); emitting would silently miscompute at the " + "adaptor boundary. Author the recipe with dim = 3 (embed " + "plane-strain/axisymmetric states in a 3x3 tensor)."); + } + }; + for (auto const &i : model.inputs()) { + if (i.kind == SymbolDecl::Kind::Tensor) { + reject_non_3d(i.dim, "tensor input", i.name); + } + } + for (auto const &o : model.outputs()) { + if (o.kind == OutputDecl::Kind::Tensor) { + reject_non_3d(o.dim, "tensor output", o.name); + } + } + // Stateful symbols would need old/new MaterialProperty pair handling // that the MOOSE backend doesn't implement yet. Fail loudly rather // than silently emit a regular read. diff --git a/tests/AlgorithmicTangentTest.cpp b/tests/AlgorithmicTangentTest.cpp index 0271650..688ff55 100644 --- a/tests/AlgorithmicTangentTest.cpp +++ b/tests/AlgorithmicTangentTest.cpp @@ -187,11 +187,20 @@ TEST(AlgorithmicTangent, TangentDimFollowsStrainDimNotHardcoded3) { } } EXPECT_TRUE(found); - auto const source = MooseMaterialTarget{}.emit(m).at(1).contents; - EXPECT_NE(source.find("tmech::adaptor> " - "Jacobian_mult_ad"), - std::string::npos) - << source; + // issue #7: the MOOSE half of this test previously asserted a dim-2 + // adaptor over Jacobian_mult — but RankTwoTensor/RankFourTensor are + // unconditionally 3D (LIBMESH_DIM = 3), so a full<2> adaptor over that + // storage reads the wrong elements: the "propagated" dim made the + // miscompute deterministic, not correct. Dim propagation stays pinned + // above at the canonical_arguments level (the Layer-2 / standalone path + // genuinely supports dim 2); the MOOSE backend must now REJECT dim != 3. + try { + [[maybe_unused]] auto const discarded = MooseMaterialTarget{}.emit(m); + ADD_FAILURE() << "expected MooseMaterialTarget to reject the dim-2 recipe"; + } catch (std::runtime_error const &e) { + EXPECT_NE(std::string(e.what()).find("LIBMESH_DIM"), std::string::npos) + << e.what(); + } } // Round-2 review (coverage): the tangent through the MOOSE backend ALONGSIDE a diff --git a/tests/MooseTargetTest.cpp b/tests/MooseTargetTest.cpp index f262b25..50dba81 100644 --- a/tests/MooseTargetTest.cpp +++ b/tests/MooseTargetTest.cpp @@ -172,6 +172,40 @@ TEST(MooseTarget, SpectralSourceIncludesRuntimeHeader) { } // A non-spectral material must NOT drag in the spectral header. +// issue #7: RankTwoTensor is always 3D; a dim-2 recipe must be rejected +// loudly instead of silently miscomputing through the adaptor boundary. +TEST(MooseTarget, RejectsNonThreeDimensionalTensors) { + ConstitutiveModel m("PlaneStrain2D"); + auto mu = m.add_parameter("mu", 0.5, "Shear modulus"); + auto eps = m.add_tensor_input("eps", 2, 2, roles::Strain); + m.add_output("stress", 2 * mu * eps, roles::Stress); + MooseMaterialTarget target; + try { + [[maybe_unused]] auto const discarded = target.emit(m); + ADD_FAILURE() << "expected emit() to reject the dim-2 tensor input"; + } catch (std::runtime_error const &e) { + std::string const msg = e.what(); + EXPECT_NE(msg.find("dim 2"), std::string::npos) << msg; + EXPECT_NE(msg.find("LIBMESH_DIM"), std::string::npos) << msg; + } +} + +// issue #10: the storage-layout assumption behind the tmech::adaptor +// boundary must be a compile-time check in the consumer's build. +TEST(MooseTarget, SourceEmitsLayoutStaticAsserts) { + MooseMaterialTarget target; + auto files = target.emit(build_linear_elastic_shear()); + auto const &source = files[1].contents; + EXPECT_NE(source.find("static_assert(sizeof(RankTwoTensor) == 9 * " + "sizeof(Real)"), + std::string::npos) + << source; + EXPECT_NE(source.find("static_assert(sizeof(RankFourTensor) == 81 * " + "sizeof(Real)"), + std::string::npos) + << source; +} + TEST(MooseTarget, NonSpectralSourceOmitsRuntimeHeader) { MooseMaterialTarget target; auto files = target.emit(build_linear_elastic_shear());