feat(domain-and-input-skeleton): step 1 — domain-validation
This commit is contained in:
@@ -0,0 +1,572 @@
|
||||
#include <fesa/model/domain_builder.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <limits>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
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::size_t>(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<InvalidSectionProperty> {};
|
||||
|
||||
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<InvalidSectionProperty>& info) {
|
||||
return info.param.name;
|
||||
});
|
||||
|
||||
TEST(DomainValidation, RejectsNonfiniteNodeCoordinate) {
|
||||
const double nan = std::numeric_limits<double>::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<double>::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<double>::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<double>::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<double>::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<double>::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<double>::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<double>::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<double>::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<double>::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<double>::quiet_NaN();
|
||||
const double infinity = std::numeric_limits<double>::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
|
||||
Reference in New Issue
Block a user