#include #include #include #include #include #include #include namespace { fesa::Node first_node() { return { fesa::NodeId{0}, fesa::EntityOrigin{"BeamPart", "Beam-1", 1}, fesa::Vec3{0.0, 0.0, 0.0}, }; } fesa::Node second_node() { return { fesa::NodeId{1}, fesa::EntityOrigin{"BeamPart", "Beam-1", 2}, fesa::Vec3{2.0, 0.0, 0.0}, }; } fesa::IsotropicElastic valid_material() { return { fesa::MaterialId{0}, "Steel", 210.0e9, 0.3, }; } fesa::BeamSection valid_section() { return { fesa::SectionId{0}, "General", 0.04, 1.2e-4, 1.4e-4, 2.0e-4, 0.03, 0.031, fesa::ShearPropertySource::input, fesa::Vec3{0.0, 1.0, 0.0}, {{-0.1, 0.0}, {0.1, 0.0}}, }; } fesa::BeamElement valid_element() { return { fesa::ElementId{0}, fesa::EntityOrigin{"BeamPart", "Beam-1", 1}, {fesa::NodeId{0}, fesa::NodeId{1}}, fesa::MaterialId{0}, fesa::SectionId{0}, }; } fesa::StepDefinition valid_step() { return { "Load", {{fesa::NodeId{0}, 1, 0.0}}, {{fesa::NodeId{1}, {0.0, -100.0, 0.0, 0.0, 0.0, 0.0}}}, }; } fesa::DomainBuilder make_builder( fesa::IsotropicElastic material, fesa::BeamSection section, fesa::BeamElement element, fesa::StepDefinition step) { fesa::DomainBuilder builder; builder.add_node(first_node()); builder.add_node(second_node()); builder.add_material(std::move(material)); builder.add_section(std::move(section)); builder.add_beam_element(std::move(element)); builder.add_node_set({"Fixed", {fesa::NodeId{0}}}); builder.add_element_set({"Beam", {fesa::ElementId{0}}}); builder.set_step(std::move(step)); return builder; } fesa::DomainBuilder make_valid_builder() { return make_builder( valid_material(), valid_section(), valid_element(), valid_step()); } bool has_diagnostic( const fesa::DomainBuildResult& result, const std::string_view code) { return std::ranges::any_of( result.diagnostics, [code](const fesa::Diagnostic& diagnostic) { return diagnostic.code == code; }); } std::size_t diagnostic_count( const fesa::DomainBuildResult& result, const std::string_view code) { return static_cast(std::ranges::count_if( result.diagnostics, [code](const fesa::Diagnostic& diagnostic) { return diagnostic.code == code; })); } TEST(DomainBuilder, BuildsImmutableDomainAndDenseNodeLookups) { auto result = std::move(make_valid_builder()).build(); ASSERT_TRUE(result.domain.has_value()); EXPECT_TRUE(result.diagnostics.empty()); const fesa::Domain& domain = *result.domain; ASSERT_EQ(domain.nodes().size(), 2); ASSERT_EQ(domain.beam_elements().size(), 1); ASSERT_EQ(domain.materials().size(), 1); ASSERT_EQ(domain.sections().size(), 1); ASSERT_EQ(domain.node_sets().size(), 1); ASSERT_EQ(domain.element_sets().size(), 1); EXPECT_EQ(domain.step().name, "Load"); EXPECT_EQ(&domain.node(fesa::NodeId{1}), &domain.nodes()[1]); EXPECT_EQ( &domain.node(fesa::EntityOrigin{"BeamPart", "Beam-1", 2}), &domain.nodes()[1]); } TEST(DomainValidation, RejectsDuplicateInternalId) { auto builder = make_valid_builder(); builder.add_node({ fesa::NodeId{1}, fesa::EntityOrigin{"BeamPart", "Beam-1", 3}, fesa::Vec3{3.0, 0.0, 0.0}, }); const auto result = std::move(builder).build(); EXPECT_FALSE(result.domain.has_value()); EXPECT_TRUE(has_diagnostic(result, "model.duplicate_node_id")); } TEST(DomainValidation, RejectsDuplicateMaterialId) { auto builder = make_valid_builder(); auto material = valid_material(); material.name = "Duplicate"; builder.add_material(std::move(material)); const auto result = std::move(builder).build(); EXPECT_FALSE(result.domain.has_value()); EXPECT_TRUE(has_diagnostic(result, "model.duplicate_material_id")); } TEST(DomainValidation, RejectsDuplicateSectionId) { auto builder = make_valid_builder(); auto section = valid_section(); section.name = "Duplicate"; builder.add_section(std::move(section)); const auto result = std::move(builder).build(); EXPECT_FALSE(result.domain.has_value()); EXPECT_TRUE(has_diagnostic(result, "model.duplicate_section_id")); } TEST(DomainValidation, RejectsDuplicateElementId) { auto builder = make_valid_builder(); auto element = valid_element(); element.origin.local_label = 2; builder.add_beam_element(std::move(element)); const auto result = std::move(builder).build(); EXPECT_FALSE(result.domain.has_value()); EXPECT_TRUE(has_diagnostic(result, "model.duplicate_element_id")); } TEST(DomainValidation, RejectsDuplicateOriginWithinEntityKind) { auto builder = make_valid_builder(); builder.add_node({ fesa::NodeId{2}, fesa::EntityOrigin{"OtherPart", "Beam-1", 2}, fesa::Vec3{3.0, 0.0, 0.0}, }); const auto result = std::move(builder).build(); EXPECT_FALSE(result.domain.has_value()); EXPECT_TRUE(has_diagnostic(result, "model.duplicate_node_origin")); } TEST(DomainValidation, RejectsDuplicateElementOriginWithinEntityKind) { auto builder = make_valid_builder(); auto element = valid_element(); element.id = fesa::ElementId{1}; element.origin.part_name = "OtherPart"; builder.add_beam_element(std::move(element)); const auto result = std::move(builder).build(); EXPECT_FALSE(result.domain.has_value()); EXPECT_TRUE(has_diagnostic(result, "model.duplicate_element_origin")); } TEST(DomainValidation, CollectsMissingReferencesAcrossSemanticEntities) { auto builder = make_valid_builder(); builder.add_beam_element({ fesa::ElementId{1}, fesa::EntityOrigin{"BeamPart", "Beam-1", 2}, {fesa::NodeId{90}, fesa::NodeId{91}}, fesa::MaterialId{92}, fesa::SectionId{93}, }); builder.add_node_set({"MissingNodes", {fesa::NodeId{94}}}); builder.add_element_set({"MissingElements", {fesa::ElementId{95}}}); builder.set_step({ "Load", {{fesa::NodeId{96}, 1, 0.0}}, {{fesa::NodeId{97}, {1.0, 0.0, 0.0, 0.0, 0.0, 0.0}}}, }); const auto result = std::move(builder).build(); EXPECT_FALSE(result.domain.has_value()); EXPECT_TRUE(has_diagnostic(result, "model.missing_node_reference")); EXPECT_TRUE(has_diagnostic(result, "model.missing_material_reference")); EXPECT_TRUE(has_diagnostic(result, "model.missing_section_reference")); EXPECT_TRUE(has_diagnostic(result, "model.missing_element_reference")); } TEST(DomainValidation, RejectsNonpositiveYoungsModulus) { auto material = valid_material(); material.young = 0.0; const auto result = std::move(make_builder( material, valid_section(), valid_element(), valid_step())) .build(); EXPECT_FALSE(result.domain.has_value()); EXPECT_TRUE(has_diagnostic(result, "model.invalid_material")); } TEST(DomainValidation, RejectsPoissonRatioOutsideOpenPhysicalRange) { for (const double poisson : {-1.0, 0.5}) { auto material = valid_material(); material.poisson = poisson; const auto result = std::move(make_builder( material, valid_section(), valid_element(), valid_step())) .build(); EXPECT_FALSE(result.domain.has_value()) << "poisson=" << poisson; EXPECT_TRUE(has_diagnostic(result, "model.invalid_material")) << "poisson=" << poisson; } } struct InvalidSectionProperty final { const char* name; double fesa::BeamSection::*member; }; class DomainValidationInvalidSection : public testing::TestWithParam {}; TEST_P(DomainValidationInvalidSection, RejectsNonpositiveProperty) { auto section = valid_section(); section.*(GetParam().member) = 0.0; const auto result = std::move(make_builder( valid_material(), section, valid_element(), valid_step())) .build(); EXPECT_FALSE(result.domain.has_value()); EXPECT_TRUE(has_diagnostic(result, "model.invalid_section")); } INSTANTIATE_TEST_SUITE_P( SectionProperties, DomainValidationInvalidSection, testing::Values( InvalidSectionProperty{"Area", &fesa::BeamSection::area}, InvalidSectionProperty{"Iy", &fesa::BeamSection::iy}, InvalidSectionProperty{"Iz", &fesa::BeamSection::iz}, InvalidSectionProperty{"TorsionJ", &fesa::BeamSection::torsion_j}, InvalidSectionProperty{ "ShearAreaY", &fesa::BeamSection::shear_area_y}, InvalidSectionProperty{ "ShearAreaZ", &fesa::BeamSection::shear_area_z}), [](const testing::TestParamInfo& info) { return info.param.name; }); TEST(DomainValidation, RejectsNonfiniteNodeCoordinate) { const double nan = std::numeric_limits::quiet_NaN(); auto builder = make_valid_builder(); builder.add_node({ fesa::NodeId{2}, fesa::EntityOrigin{"BeamPart", "Beam-1", 3}, fesa::Vec3{nan, 0.0, 0.0}, }); const auto result = std::move(builder).build(); EXPECT_FALSE(result.domain.has_value()); EXPECT_TRUE(has_diagnostic(result, "model.nonfinite_value")); } TEST(DomainValidation, RejectsNonfiniteMaterialConstant) { auto material = valid_material(); material.young = std::numeric_limits::infinity(); const auto result = std::move(make_builder( material, valid_section(), valid_element(), valid_step())) .build(); EXPECT_FALSE(result.domain.has_value()); EXPECT_TRUE(has_diagnostic(result, "model.nonfinite_value")); } TEST(DomainValidation, RejectsNonfinitePoissonRatio) { auto material = valid_material(); material.poisson = std::numeric_limits::quiet_NaN(); const auto result = std::move(make_builder( material, valid_section(), valid_element(), valid_step())) .build(); EXPECT_FALSE(result.domain.has_value()); EXPECT_TRUE(has_diagnostic(result, "model.nonfinite_value")); } TEST(DomainValidation, RejectsNonfiniteSectionProperty) { auto section = valid_section(); section.area = std::numeric_limits::infinity(); const auto result = std::move(make_builder( valid_material(), section, valid_element(), valid_step())) .build(); EXPECT_FALSE(result.domain.has_value()); EXPECT_TRUE(has_diagnostic(result, "model.nonfinite_value")); } TEST(DomainValidation, RejectsNonfiniteSectionOrientation) { auto section = valid_section(); section.orientation.y = std::numeric_limits::quiet_NaN(); const auto result = std::move(make_builder( valid_material(), section, valid_element(), valid_step())) .build(); EXPECT_FALSE(result.domain.has_value()); EXPECT_TRUE(has_diagnostic(result, "model.nonfinite_value")); } TEST(DomainValidation, RejectsNonfiniteRecoveryPoint) { auto section = valid_section(); const double nan = std::numeric_limits::quiet_NaN(); section.recovery_points = {{nan, 0.0}}; const auto result = std::move(make_builder( valid_material(), section, valid_element(), valid_step())) .build(); EXPECT_FALSE(result.domain.has_value()); EXPECT_TRUE(has_diagnostic(result, "model.nonfinite_value")); } TEST(DomainValidation, RejectsNonfinitePrescribedValue) { auto step = valid_step(); step.prescribed_dofs[0].value = std::numeric_limits::infinity(); const auto result = std::move(make_builder( valid_material(), valid_section(), valid_element(), step)) .build(); EXPECT_FALSE(result.domain.has_value()); EXPECT_TRUE(has_diagnostic(result, "model.nonfinite_value")); } TEST(DomainValidation, RejectsNonfiniteLoadComponent) { auto step = valid_step(); step.nodal_loads[0].values[0] = std::numeric_limits::quiet_NaN(); const auto result = std::move(make_builder( valid_material(), valid_section(), valid_element(), step)) .build(); EXPECT_FALSE(result.domain.has_value()); EXPECT_TRUE(has_diagnostic(result, "model.nonfinite_value")); } TEST(DomainValidation, CollectsIndependentMaterialPropertyDiagnostics) { auto material = valid_material(); material.young = std::numeric_limits::quiet_NaN(); material.poisson = 0.5; const auto result = std::move(make_builder( material, valid_section(), valid_element(), valid_step())) .build(); EXPECT_FALSE(result.domain.has_value()); EXPECT_TRUE(has_diagnostic(result, "model.nonfinite_value")); EXPECT_TRUE(has_diagnostic(result, "model.invalid_material")); } TEST(DomainValidation, CollectsIndependentSectionPropertyDiagnostics) { auto section = valid_section(); section.area = -1.0; section.iy = std::numeric_limits::quiet_NaN(); const auto result = std::move(make_builder( valid_material(), section, valid_element(), valid_step())) .build(); EXPECT_FALSE(result.domain.has_value()); EXPECT_TRUE(has_diagnostic(result, "model.nonfinite_value")); EXPECT_TRUE(has_diagnostic(result, "model.invalid_section")); } TEST(DomainValidation, CollectsNonfiniteStepValuesIndependently) { const double nan = std::numeric_limits::quiet_NaN(); const double infinity = std::numeric_limits::infinity(); auto step = fesa::StepDefinition{ "Load", {{fesa::NodeId{0}, 1, infinity}}, {{fesa::NodeId{1}, {nan, 0.0, 0.0, 0.0, 0.0, 0.0}}}, }; const auto result = std::move(make_builder( valid_material(), valid_section(), valid_element(), step)) .build(); EXPECT_FALSE(result.domain.has_value()); EXPECT_EQ(diagnostic_count(result, "model.nonfinite_value"), 2); } TEST(DomainValidation, RejectsZeroLengthElement) { auto element = valid_element(); element.nodes[1] = fesa::NodeId{0}; const auto result = std::move(make_builder( valid_material(), valid_section(), element, valid_step())) .build(); EXPECT_FALSE(result.domain.has_value()); EXPECT_TRUE(has_diagnostic(result, "model.zero_length_element")); } TEST(DomainValidation, RejectsZeroAndElementParallelOrientation) { for (const fesa::Vec3 orientation : {fesa::Vec3{0.0, 0.0, 0.0}, fesa::Vec3{1.0, 0.0, 0.0}}) { auto section = valid_section(); section.orientation = orientation; const auto result = std::move(make_builder( valid_material(), section, valid_element(), valid_step())) .build(); EXPECT_FALSE(result.domain.has_value()); EXPECT_TRUE(has_diagnostic(result, "model.invalid_orientation")); } } TEST(DomainValidation, RejectsElementWithoutMaterialAndSectionAssignments) { auto element = valid_element(); element.material = fesa::MaterialId{8}; element.section = fesa::SectionId{9}; const auto result = std::move(make_builder( valid_material(), valid_section(), element, valid_step())) .build(); EXPECT_FALSE(result.domain.has_value()); EXPECT_TRUE(has_diagnostic(result, "model.missing_material_reference")); EXPECT_TRUE(has_diagnostic(result, "model.missing_section_reference")); } TEST(DomainValidation, RejectsConflictingBoundaryConditions) { auto step = valid_step(); step.prescribed_dofs.push_back({fesa::NodeId{0}, 1, 0.25}); const auto result = std::move(make_builder( valid_material(), valid_section(), valid_element(), step)) .build(); EXPECT_FALSE(result.domain.has_value()); EXPECT_TRUE( has_diagnostic(result, "model.conflicting_boundary_condition")); } TEST(DomainValidation, CollectsIndependentDiagnosticsWithoutEarlyExit) { auto material = valid_material(); material.young = -1.0; auto section = valid_section(); section.area = -1.0; section.orientation = fesa::Vec3{1.0, 0.0, 0.0}; const auto result = std::move(make_builder( material, section, valid_element(), valid_step())) .build(); EXPECT_FALSE(result.domain.has_value()); EXPECT_TRUE(has_diagnostic(result, "model.invalid_material")); EXPECT_TRUE(has_diagnostic(result, "model.invalid_section")); EXPECT_TRUE(has_diagnostic(result, "model.invalid_orientation")); } } // namespace