#include #include #include #include #include #include #include #include #include import mean_field; import test_helpers; namespace domain_test_utils { struct UnregisteredDomain final : public mean_field::utils::domain::Domain { static constexpr std::string_view name = "unregistered_domain"; }; struct UnregisteredBoundary final : public mean_field::utils::domain::Boundary { static constexpr std::string_view name = "unregistered_boundary"; }; struct BoundaryEdge { int firstVertexId{-1}; int secondVertexId{-1}; int attribute{0}; }; struct StroidCase { std::string_view name; int refinementLevels{0}; int order{1}; double flattening{0.0}; }; template concept CanFormMaterialList = requires { typename mean_field::utils::domain::MaterialList; }; template concept CanFormBoundaryList = requires { typename mean_field::utils::domain::BoundaryList; }; template concept CanFormDomainBoundary = requires { typename mean_field::utils::domain::DomainBoundary; }; template concept CanFormSchema = requires { typename mean_field::utils::domain::DomainSchema; }; [[nodiscard]] int vertex_id( const int xElementCount, const int x, const int y ) { return y * (xElementCount + 1) + x; } [[nodiscard]] int cell_index( const int xElementCount, const int x, const int y ) { return y * xElementCount + x; } [[nodiscard]] int cell_attribute( const std::vector &attributes, const int xElementCount, const int x, const int y ) { return attributes.at(static_cast(cell_index(xElementCount, x, y))); } template < typename FirstPredicateT, typename SecondPredicateT> void append_interface_boundaries( std::vector &boundaries, const std::vector &attributes, const int xElementCount, const int yElementCount, FirstPredicateT firstPredicate, SecondPredicateT secondPredicate, const int boundaryAttribute ) { /* * Vertical internal faces. */ for (int y = 0; y < yElementCount; ++y) { for (int x = 1; x < xElementCount; ++x) { const int leftAttribute = cell_attribute(attributes, xElementCount, x - 1, y); const int rightAttribute = cell_attribute(attributes, xElementCount, x, y); const bool matches = (firstPredicate(leftAttribute) && secondPredicate(rightAttribute)) || (secondPredicate(leftAttribute) && firstPredicate(rightAttribute)); if (!matches) { continue; } boundaries.push_back( {.firstVertexId = vertex_id(xElementCount, x, y), .secondVertexId = vertex_id(xElementCount, x, y + 1), .attribute = boundaryAttribute} ); } } /* * Horizontal internal faces. */ for (int y = 1; y < yElementCount; ++y) { for (int x = 0; x < xElementCount; ++x) { const int lowerAttribute = cell_attribute(attributes, xElementCount, x, y - 1); const int upperAttribute = cell_attribute(attributes, xElementCount, x, y); const bool matches = (firstPredicate(lowerAttribute) && secondPredicate(upperAttribute)) || (secondPredicate(lowerAttribute) && firstPredicate(upperAttribute)); if (!matches) { continue; } boundaries.push_back( {.firstVertexId = vertex_id(xElementCount, x, y), .secondVertexId = vertex_id(xElementCount, x + 1, y), .attribute = boundaryAttribute} ); } } } template void append_exterior_boundaries( std::vector &boundaries, const std::vector &attributes, const int xElementCount, const int yElementCount, PredicateT predicate, const int boundaryAttribute ) { /* * Bottom. */ for (int x = 0; x < xElementCount; ++x) { if (predicate(cell_attribute(attributes, xElementCount, x, 0))) { boundaries.push_back( {.firstVertexId = vertex_id(xElementCount, x, 0), .secondVertexId = vertex_id(xElementCount, x + 1, 0), .attribute = boundaryAttribute} ); } } /* * Top. */ for (int x = 0; x < xElementCount; ++x) { if (predicate(cell_attribute(attributes, xElementCount, x, yElementCount - 1))) { boundaries.push_back( {.firstVertexId = vertex_id(xElementCount, x, yElementCount), .secondVertexId = vertex_id(xElementCount, x + 1, yElementCount), .attribute = boundaryAttribute} ); } } /* * Left. */ for (int y = 0; y < yElementCount; ++y) { if (predicate(cell_attribute(attributes, xElementCount, 0, y))) { boundaries.push_back( {.firstVertexId = vertex_id(xElementCount, 0, y), .secondVertexId = vertex_id(xElementCount, 0, y + 1), .attribute = boundaryAttribute} ); } } /* * Right. */ for (int y = 0; y < yElementCount; ++y) { if (predicate(cell_attribute(attributes, xElementCount, xElementCount - 1, y))) { boundaries.push_back( {.firstVertexId = vertex_id(xElementCount, xElementCount, y), .secondVertexId = vertex_id(xElementCount, xElementCount, y + 1), .attribute = boundaryAttribute} ); } } } [[nodiscard]] mfem::Mesh make_grid_mesh( const int xElementCount, const int yElementCount, const std::vector &attributes, const std::vector &boundaryEdges ) { REQUIRE(static_cast(attributes.size()) == xElementCount * yElementCount); mfem::Mesh mesh( 2, (xElementCount + 1) * (yElementCount + 1), xElementCount * yElementCount, static_cast(boundaryEdges.size()), 2 ); for (int y = 0; y <= yElementCount; ++y) { for (int x = 0; x <= xElementCount; ++x) { mesh.AddVertex(static_cast(x), static_cast(y)); } } for (int y = 0; y < yElementCount; ++y) { for (int x = 0; x < xElementCount; ++x) { const int lowerLeft = vertex_id(xElementCount, x, y); const int lowerRight = vertex_id(xElementCount, x + 1, y); const int upperRight = vertex_id(xElementCount, x + 1, y + 1); const int upperLeft = vertex_id(xElementCount, x, y + 1); mesh.AddQuad( lowerLeft, lowerRight, upperRight, upperLeft, cell_attribute(attributes, xElementCount, x, y) ); } } for (const BoundaryEdge &boundary : boundaryEdges) { mesh.AddBdrSegment(boundary.firstVertexId, boundary.secondVertexId, boundary.attribute); } mesh.FinalizeTopology(false); mesh.Finalize(false, false); REQUIRE(mesh.GetNBE() == static_cast(boundaryEdges.size())); return mesh; } [[nodiscard]] std::vector make_layered_attributes() { constexpr int xElementCount = 5; constexpr int yElementCount = 5; std::vector attributes(xElementCount * yElementCount, 3); for (int y = 1; y <= 3; ++y) { for (int x = 1; x <= 3; ++x) { attributes[static_cast(cell_index(xElementCount, x, y))] = 2; } } attributes[static_cast(cell_index(xElementCount, 2, 2))] = 1; return attributes; } [[nodiscard]] mfem::Mesh make_layered_mesh( const bool includeStellarSurface = true, const bool includeInfinitySurface = true, const int stellarSurfaceAttribute = 1, const int infinitySurfaceAttribute = 2 ) { constexpr int xElementCount = 5; constexpr int yElementCount = 5; const std::vector attributes = make_layered_attributes(); std::vector boundaries; const auto isStellar = [](const int materialId) { return materialId == 1 || materialId == 2; }; const auto isVacuum = [](const int materialId) { return materialId == 3; }; if (includeStellarSurface) { append_interface_boundaries( boundaries, attributes, xElementCount, yElementCount, isStellar, isVacuum, stellarSurfaceAttribute ); } if (includeInfinitySurface) { append_exterior_boundaries( boundaries, attributes, xElementCount, yElementCount, isVacuum, infinitySurfaceAttribute ); } return make_grid_mesh(xElementCount, yElementCount, attributes, boundaries); } template void check_schema_is_valid(const mfem::Mesh &mesh) { const auto validation = mean_field::utils::domain::validate_schema(mesh); CHECK(validation.relationResults.size() == SchemaT::relationCount); for (const auto &relationResult : validation.relationResults) { INFO("Relation index = " << relationResult.relationIndex); INFO("Relation name = " << relationResult.relationName); INFO("Failure enum = " << static_cast(relationResult.result.failure)); CHECK(relationResult.valid()); } CHECK(validation.valid()); } using AlternateIdSchema = mean_field::utils::domain::DomainSchema< mean_field::utils::domain::MaterialList< mean_field::utils::domain::Material, mean_field::utils::domain::Material, mean_field::utils::domain::Material>, mean_field::utils::domain::BoundaryList< mean_field::utils::domain::BoundaryAttribute, mean_field::utils::domain::BoundaryAttribute>, mean_field::utils::domain::RelationList< mean_field::utils::domain::Connected, mean_field::utils::domain::Connected, mean_field::utils::domain::Connected, mean_field::utils::domain::Inscribed, mean_field::utils::domain::Inscribed, mean_field::utils::domain::DomainBoundary< mean_field::utils::domain::StellarSurface, mean_field::utils::domain::Stellar, mean_field::utils::domain::Vacuum>, mean_field::utils::domain:: DomainBoundary>>; [[nodiscard]] stroid::config::MeshConfig make_stroid_config( const int refinementLevels, const int order, const double flattening ) { stroid::config::MeshConfig config; config.refinement_levels = refinementLevels; config.order = order; config.include_external_domain = true; config.r_core = 0.25; config.r_star = 1.0; config.r_infinity = 4.0; config.flattening = flattening; config.core_id = 1; config.envelope_id = 2; config.vacuum_id = 3; config.surface_bdr_id = 1; config.inf_bdr_id = 2; config.optimization_methods = stroid::config::OptimizationMethods{.tmop = false, .smoothstep = true}; return config; } } // namespace domain_test_utils TEST_CASE( "Domain Types And Composite Domains Preserve Their Semantic Categories", tags::unit &tags::mesh &tags::utils &tags::domain ) { STATIC_REQUIRE(mean_field::utils::domain::IsDomain); STATIC_REQUIRE(mean_field::utils::domain::IsDomain); STATIC_REQUIRE(mean_field::utils::domain::IsDomain); STATIC_REQUIRE(mean_field::utils::domain::IsDomainSet); STATIC_REQUIRE(mean_field::utils::domain::IsDomainSet); STATIC_REQUIRE_FALSE(mean_field::utils::domain::IsDomain); STATIC_REQUIRE(mean_field::utils::domain::IsDomainOrSet); STATIC_REQUIRE(mean_field::utils::domain::IsBoundary); STATIC_REQUIRE(mean_field::utils::domain::IsBoundary); CHECK(true); } TEST_CASE( "Material Lists Reject Duplicate Ids And Duplicate Semantic Domains", tags::unit &tags::mesh &tags::utils &tags::domain ) { STATIC_REQUIRE( domain_test_utils::CanFormMaterialList< mean_field::utils::domain::Material, mean_field::utils::domain::Material> ); STATIC_REQUIRE_FALSE( domain_test_utils::CanFormMaterialList< mean_field::utils::domain::Material, mean_field::utils::domain::Material> ); STATIC_REQUIRE_FALSE( domain_test_utils::CanFormMaterialList< mean_field::utils::domain::Material, mean_field::utils::domain::Material> ); /* * The schema intentionally imposes no convention on the * numerical range or indexing scheme used by a mesh producer. */ STATIC_REQUIRE( domain_test_utils::CanFormMaterialList< mean_field::utils::domain::Material, mean_field::utils::domain::Material, mean_field::utils::domain::Material> ); CHECK(true); } TEST_CASE( "Boundary Lists Reject Duplicate Ids And Duplicate Semantic Boundaries", tags::unit &tags::mesh &tags::utils &tags::domain ) { STATIC_REQUIRE( domain_test_utils::CanFormBoundaryList< mean_field::utils::domain::BoundaryAttribute, mean_field::utils::domain::BoundaryAttribute> ); STATIC_REQUIRE_FALSE( domain_test_utils::CanFormBoundaryList< mean_field::utils::domain::BoundaryAttribute, mean_field::utils::domain::BoundaryAttribute> ); STATIC_REQUIRE_FALSE( domain_test_utils::CanFormBoundaryList< mean_field::utils::domain::BoundaryAttribute, mean_field::utils::domain::BoundaryAttribute> ); STATIC_REQUIRE( domain_test_utils::CanFormBoundaryList< mean_field::utils::domain::BoundaryAttribute, mean_field::utils::domain::BoundaryAttribute> ); CHECK(true); } TEST_CASE( "Domain Boundary Relations Accept Exactly One Or Two Domains", tags::unit &tags::mesh &tags::utils &tags::domain ) { STATIC_REQUIRE( domain_test_utils::CanFormDomainBoundary< mean_field::utils::domain::InfinitySurface, mean_field::utils::domain::Vacuum> ); STATIC_REQUIRE( domain_test_utils::CanFormDomainBoundary< mean_field::utils::domain::StellarSurface, mean_field::utils::domain::Stellar, mean_field::utils::domain::Vacuum> ); STATIC_REQUIRE_FALSE(domain_test_utils::CanFormDomainBoundary); STATIC_REQUIRE_FALSE( domain_test_utils::CanFormDomainBoundary< mean_field::utils::domain::StellarSurface, mean_field::utils::domain::Core, mean_field::utils::domain::Envelope, mean_field::utils::domain::Vacuum> ); CHECK(true); } TEST_CASE( "Domain Schemas Reject Relations That Reference Unregistered Entities", tags::unit &tags::mesh &tags::utils &tags::domain ) { using IncompleteMaterials = mean_field::utils::domain::MaterialList< mean_field::utils::domain::Material, mean_field::utils::domain::Material>; using CompleteMaterials = mean_field::utils::domain::MaterialList< mean_field::utils::domain::Material, mean_field::utils::domain::Material, mean_field::utils::domain::Material>; using CompleteBoundaries = mean_field::utils::domain::BoundaryList< mean_field::utils::domain::BoundaryAttribute, mean_field::utils::domain::BoundaryAttribute>; using InfinityOnlyBoundary = mean_field::utils::domain::BoundaryList< mean_field::utils::domain::BoundaryAttribute>; using MissingEnvelopeRelation = mean_field::utils::domain::RelationList< mean_field::utils::domain::Connected>; using MissingBoundaryRelation = mean_field::utils::domain::RelationList>; STATIC_REQUIRE_FALSE( domain_test_utils::CanFormSchema ); STATIC_REQUIRE_FALSE( domain_test_utils::CanFormSchema ); CHECK(true); } TEST_CASE( "Core Envelope Vacuum Schema Exposes Exact Compile Time And Runtime Metadata", tags::unit &tags::mesh &tags::utils &tags::domain ) { using SchemaT = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema; STATIC_REQUIRE(mean_field::utils::domain::IsSchema); STATIC_REQUIRE(SchemaT::materialCount == 3); STATIC_REQUIRE(SchemaT::boundaryCount == 2); STATIC_REQUIRE(SchemaT::relationCount == 7); constexpr auto materials = SchemaT::materials(); constexpr auto boundaries = SchemaT::boundaries(); STATIC_REQUIRE(materials[0].name == std::string_view{"core"}); STATIC_REQUIRE(materials[0].id == 1); STATIC_REQUIRE(materials[1].name == std::string_view{"envelope"}); STATIC_REQUIRE(materials[1].id == 2); STATIC_REQUIRE(materials[2].name == std::string_view{"vacuum"}); STATIC_REQUIRE(materials[2].id == 3); STATIC_REQUIRE(boundaries[0].name == std::string_view{"stellar_surface"}); STATIC_REQUIRE(boundaries[0].id == 1); STATIC_REQUIRE(boundaries[1].name == std::string_view{"infinity_surface"}); STATIC_REQUIRE(boundaries[1].id == 2); STATIC_REQUIRE(SchemaT::template contains_domain()); STATIC_REQUIRE(SchemaT::template contains_domain()); STATIC_REQUIRE(SchemaT::template contains_domain()); STATIC_REQUIRE(SchemaT::template attribute_belongs_to(1)); STATIC_REQUIRE(SchemaT::template attribute_belongs_to(2)); STATIC_REQUIRE_FALSE(SchemaT::template attribute_belongs_to(3)); STATIC_REQUIRE(SchemaT::template attribute_belongs_to(1)); STATIC_REQUIRE(SchemaT::template attribute_belongs_to(2)); STATIC_REQUIRE(SchemaT::template attribute_belongs_to(3)); STATIC_REQUIRE(SchemaT::template contains_boundary()); STATIC_REQUIRE(SchemaT::template contains_boundary()); STATIC_REQUIRE(SchemaT::template boundary_attribute() == 1); STATIC_REQUIRE(SchemaT::template boundary_attribute() == 2); CHECK(true); } TEST_CASE( "Connected Accepts Face Connected Atomic And Composite Domains", tags::unit &tags::mesh &tags::utils &tags::domain ) { mfem::Mesh mesh = domain_test_utils::make_layered_mesh(); const auto coreResult = mean_field::utils::domain:: RelationValidator>::template validate< mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema>(mesh); REQUIRE(coreResult); REQUIRE(coreResult.connectedDiagnostics.has_value()); CHECK(coreResult.connectedDiagnostics->domainElementCount == 1); CHECK(coreResult.connectedDiagnostics->visitedElementCount == 1); const auto stellarResult = mean_field::utils::domain:: RelationValidator>::template validate< mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema>(mesh); REQUIRE(stellarResult); REQUIRE(stellarResult.connectedDiagnostics.has_value()); CHECK(stellarResult.connectedDiagnostics->domainElementCount == 9); CHECK(stellarResult.connectedDiagnostics->visitedElementCount == 9); } TEST_CASE( "Connected Rejects An Absent Domain", tags::unit &tags::mesh &tags::utils &tags::domain ) { const mfem::Mesh mesh = domain_test_utils::make_grid_mesh(2, 1, {2, 2}, {}); const auto result = mean_field::utils::domain:: RelationValidator>::template validate< mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema>(mesh); CHECK_FALSE(result); CHECK(result.failure == mean_field::utils::domain::RelationValidationFailure::DomainAbsent); REQUIRE(result.connectedDiagnostics.has_value()); CHECK(result.connectedDiagnostics->domainElementCount == 0); CHECK(result.connectedDiagnostics->visitedElementCount == 0); } TEST_CASE( "Connected Rejects Multiple Face Disconnected Components", tags::unit &tags::mesh &tags::utils &tags::domain ) { const mfem::Mesh mesh = domain_test_utils::make_grid_mesh(3, 1, {1, 2, 1}, {}); const auto result = mean_field::utils::domain:: RelationValidator>::template validate< mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema>(mesh); CHECK_FALSE(result); CHECK(result.failure == mean_field::utils::domain::RelationValidationFailure::DomainDisconnected); REQUIRE(result.connectedDiagnostics.has_value()); CHECK(result.connectedDiagnostics->domainElementCount == 2); CHECK(result.connectedDiagnostics->visitedElementCount == 1); CHECK(result.connectedDiagnostics->elementId >= 0); } TEST_CASE( "Inscribed Accepts Nested Atomic And Composite Domains", tags::unit &tags::mesh &tags::utils &tags::domain ) { const mfem::Mesh mesh = domain_test_utils::make_layered_mesh(); const auto coreResult = mean_field::utils::domain::RelationValidator< mean_field::utils::domain::Inscribed>:: template validate(mesh); CHECK(coreResult); const auto stellarResult = mean_field::utils::domain::RelationValidator< mean_field::utils::domain::Inscribed>:: template validate(mesh); CHECK(stellarResult); } TEST_CASE( "Inscribed Rejects An Absent Inner Domain", tags::unit &tags::mesh &tags::utils &tags::domain ) { const mfem::Mesh mesh = domain_test_utils::make_grid_mesh(2, 2, {2, 2, 2, 2}, {}); const auto result = mean_field::utils::domain::RelationValidator< mean_field::utils::domain::Inscribed>:: template validate(mesh); CHECK_FALSE(result); CHECK(result.failure == mean_field::utils::domain::RelationValidationFailure::InnerDomainAbsent); } TEST_CASE( "Inscribed Rejects An Absent Outer Domain", tags::unit &tags::mesh &tags::utils &tags::domain ) { const mfem::Mesh mesh = domain_test_utils::make_grid_mesh(1, 1, {1}, {}); const auto result = mean_field::utils::domain::RelationValidator< mean_field::utils::domain::Inscribed>:: template validate(mesh); CHECK_FALSE(result); CHECK(result.failure == mean_field::utils::domain::RelationValidationFailure::OuterDomainAbsent); } TEST_CASE( "Inscribed Rejects An Inner Domain Touching The Computational Boundary", tags::unit &tags::mesh &tags::utils &tags::domain ) { const mfem::Mesh mesh = domain_test_utils::make_grid_mesh(2, 2, {1, 2, 2, 2}, {}); const auto result = mean_field::utils::domain::RelationValidator< mean_field::utils::domain::Inscribed>:: template validate(mesh); CHECK_FALSE(result); CHECK(result.failure == mean_field::utils::domain::RelationValidationFailure::InnerDomainTouchesMeshBoundary); REQUIRE(result.inscribedDiagnostics.has_value()); CHECK(result.inscribedDiagnostics->faceId >= 0); CHECK(result.inscribedDiagnostics->innerElementId >= 0); CHECK(result.inscribedDiagnostics->adjacentElementId == -1); } TEST_CASE( "Inscribed Rejects An Inner Domain Touching An Unexpected Material", tags::unit &tags::mesh &tags::utils &tags::domain ) { std::vector attributes{2, 2, 2, 2, 1, 3, 2, 2, 2}; const mfem::Mesh mesh = domain_test_utils::make_grid_mesh(3, 3, attributes, {}); const auto result = mean_field::utils::domain::RelationValidator< mean_field::utils::domain::Inscribed>:: template validate(mesh); CHECK_FALSE(result); CHECK(result.failure == mean_field::utils::domain::RelationValidationFailure::InnerDomainTouchesUnexpectedMaterial); REQUIRE(result.inscribedDiagnostics.has_value()); CHECK(result.inscribedDiagnostics->adjacentMaterialId == 3); } TEST_CASE( "Domain Boundary Accepts A Complete Internal Stellar Vacuum Interface", tags::unit &tags::mesh &tags::utils &tags::domain ) { std::vector boundaries{{.firstVertexId = 1, .secondVertexId = 4, .attribute = 1}}; const mfem::Mesh mesh = domain_test_utils::make_grid_mesh(2, 1, {2, 3}, boundaries); const auto result = mean_field::utils::domain::RelationValidator>:: template validate(mesh); CHECK(result); /* * Interface ordering is intentionally semantic rather * than oriented. */ const auto reversedResult = mean_field::utils::domain::RelationValidator>:: template validate(mesh); CHECK(reversedResult); } TEST_CASE( "Domain Boundary Accepts A Complete Exterior Vacuum Boundary", tags::unit &tags::mesh &tags::utils &tags::domain ) { const std::vector attributes{3}; std::vector boundaries; domain_test_utils::append_exterior_boundaries( boundaries, attributes, 1, 1, [](const int materialId) { return materialId == 3; }, 2 ); const mfem::Mesh mesh = domain_test_utils::make_grid_mesh(1, 1, attributes, boundaries); const auto result = mean_field::utils::domain::RelationValidator>:: template validate(mesh); CHECK(result); } TEST_CASE( "Domain Boundary Rejects A Tagged Internal Face For An Exterior Boundary", tags::unit &tags::mesh &tags::utils &tags::domain ) { const mfem::Mesh mesh = domain_test_utils::make_grid_mesh(2, 1, {3, 3}, {{.firstVertexId = 1, .secondVertexId = 4, .attribute = 2}}); const auto result = mean_field::utils::domain::RelationValidator>:: template validate(mesh); CHECK_FALSE(result); CHECK( result.failure == mean_field::utils::domain::RelationValidationFailure::DomainBoundaryTaggedFaceHasWrongTopology ); } TEST_CASE( "Domain Boundary Rejects A Tagged Exterior Face Of The Wrong Material", tags::unit &tags::mesh &tags::utils &tags::domain ) { const mfem::Mesh mesh = domain_test_utils::make_grid_mesh(1, 1, {2}, {{.firstVertexId = 0, .secondVertexId = 1, .attribute = 2}}); const auto result = mean_field::utils::domain::RelationValidator>:: template validate(mesh); CHECK_FALSE(result); CHECK( result.failure == mean_field::utils::domain::RelationValidationFailure::DomainBoundaryTaggedFaceTouchesUnexpectedMaterial ); } TEST_CASE( "Domain Boundary Rejects A Tagged Internal Interface With Unexpected Materials", tags::unit &tags::mesh &tags::utils &tags::domain ) { const mfem::Mesh mesh = domain_test_utils::make_grid_mesh(2, 1, {1, 2}, {{.firstVertexId = 1, .secondVertexId = 4, .attribute = 1}}); const auto result = mean_field::utils::domain::RelationValidator>:: template validate(mesh); CHECK_FALSE(result); CHECK( result.failure == mean_field::utils::domain::RelationValidationFailure::DomainBoundaryTaggedFaceTouchesUnexpectedMaterial ); } TEST_CASE( "Domain Boundary Rejects An Untagged Expected Interface", tags::unit &tags::mesh &tags::utils &tags::domain ) { const mfem::Mesh mesh = domain_test_utils::make_grid_mesh(2, 1, {2, 3}, {}); const auto result = mean_field::utils::domain::RelationValidator>:: template validate(mesh); CHECK_FALSE(result); CHECK(result.failure == mean_field::utils::domain::RelationValidationFailure::DomainBoundaryExpectedFaceIsUntagged); REQUIRE(result.domainBoundaryDiagnostics.has_value()); CHECK(result.domainBoundaryDiagnostics->faceId >= 0); CHECK(result.domainBoundaryDiagnostics->boundaryElementId == -1); CHECK_FALSE(result.domainBoundaryDiagnostics->actualBoundaryAttribute.has_value()); } TEST_CASE( "Domain Boundary Rejects An Expected Interface With The Wrong Attribute", tags::unit &tags::mesh &tags::utils &tags::domain ) { const mfem::Mesh mesh = domain_test_utils::make_grid_mesh(2, 1, {2, 3}, {{.firstVertexId = 1, .secondVertexId = 4, .attribute = 9}}); const auto result = mean_field::utils::domain::RelationValidator>:: template validate(mesh); CHECK_FALSE(result); CHECK( result.failure == mean_field::utils::domain::RelationValidationFailure::DomainBoundaryExpectedFaceHasWrongAttribute ); REQUIRE(result.domainBoundaryDiagnostics.has_value()); REQUIRE(result.domainBoundaryDiagnostics->actualBoundaryAttribute.has_value()); CHECK(*result.domainBoundaryDiagnostics->actualBoundaryAttribute == 9); CHECK(result.domainBoundaryDiagnostics->expectedBoundaryAttribute == 1); } TEST_CASE( "Domain Boundary Rejects A Relation That Is Not Realized Anywhere", tags::unit &tags::mesh &tags::utils &tags::domain ) { const mfem::Mesh mesh = domain_test_utils::make_grid_mesh(2, 1, {2, 2}, {}); const auto result = mean_field::utils::domain::RelationValidator>:: template validate(mesh); CHECK_FALSE(result); CHECK(result.failure == mean_field::utils::domain::RelationValidationFailure::DomainBoundaryAbsent); } TEST_CASE( "Complete Schema Validation Accepts A Synthetic Core Envelope Vacuum Mesh", tags::unit &tags::mesh &tags::utils &tags::domain ) { const mfem::Mesh mesh = domain_test_utils::make_layered_mesh(); const auto validation = mean_field::utils::domain::validate_schema(mesh); REQUIRE(validation.valid()); REQUIRE(validation.relationResults.size() == 7); CHECK(validation.failed_relation_count() == 0); CHECK(validation.passed_relation_count() == 7); CHECK_FALSE(validation.first_failed_relation_index().has_value()); const std::array expectedRelationNames{"connected", "connected", "connected", "inscribed", "inscribed", "domain_boundary", "domain_boundary"}; for (std::size_t relationIndex = 0; relationIndex < expectedRelationNames.size(); ++relationIndex) { CHECK(validation.relationResults[relationIndex].relationIndex == relationIndex); CHECK(validation.relationResults[relationIndex].relationName == expectedRelationNames[relationIndex]); CHECK(validation.relationResults[relationIndex].valid()); } } TEST_CASE( "Complete Schema Validation Evaluates Every Relation After A Failure", tags::unit &tags::mesh &tags::utils &tags::domain ) { /* * All material topology and the outer vacuum boundary are valid. * Only the Stellar/Vacuum boundary tagging is intentionally absent. */ const mfem::Mesh mesh = domain_test_utils::make_layered_mesh(false, true); const auto validation = mean_field::utils::domain::validate_schema(mesh); CHECK_FALSE(validation.valid()); REQUIRE(validation.relationResults.size() == 7); CHECK(validation.failed_relation_count() == 1); CHECK(validation.passed_relation_count() == 6); REQUIRE(validation.first_failed_relation_index().has_value()); CHECK(*validation.first_failed_relation_index() == 5); for (std::size_t relationIndex = 0; relationIndex < 7; ++relationIndex) { CAPTURE(relationIndex); if (relationIndex == 5) { CHECK_FALSE(validation.relationResults[relationIndex].valid()); CHECK( validation.relationResults[relationIndex].result.failure == mean_field::utils::domain::RelationValidationFailure::DomainBoundaryExpectedFaceIsUntagged ); continue; } CHECK(validation.relationResults[relationIndex].valid()); } } TEST_CASE( "STROID Meshes Satisfy The Core Envelope Vacuum Domain Schema", tags::integration &tags::mesh &tags::utils &tags::domain ) { const std::array testCases{ domain_test_utils::StroidCase{ .name = "spherical_low_order", .refinementLevels = 0, .order = 1, .flattening = 0.0 }, domain_test_utils::StroidCase{.name = "oblate", .refinementLevels = 0, .order = 2, .flattening = 0.15}, domain_test_utils::StroidCase{.name = "refined_oblate", .refinementLevels = 1, .order = 2, .flattening = 0.10} }; for (const domain_test_utils::StroidCase &testCase : testCases) { INFO("STROID case = " << testCase.name); INFO("Refinement levels = " << testCase.refinementLevels); INFO("Order = " << testCase.order); INFO("Flattening = " << testCase.flattening); const stroid::config::MeshConfig config = domain_test_utils::make_stroid_config(testCase.refinementLevels, testCase.order, testCase.flattening); stroid::StroidMesh stroidMesh = stroid::GenerateMesh(config); REQUIRE(stroidMesh.reference_mesh != nullptr); REQUIRE(stroidMesh.mesh != nullptr); /* * Validate both the reference topology and the projected * physical mesh. The mapping/projection must not alter * material or boundary semantics. */ domain_test_utils::check_schema_is_valid( *stroidMesh.reference_mesh ); domain_test_utils::check_schema_is_valid( *stroidMesh.mesh ); } } TEST_CASE( "STROID Material And Boundary Id Conventions Are Fully Schema Driven", tags::integration &tags::mesh &tags::utils &tags::domain ) { stroid::config::MeshConfig config = domain_test_utils::make_stroid_config(0, 1, 0.0); config.core_id = 11; config.envelope_id = 17; config.vacuum_id = 29; config.surface_bdr_id = 101; config.inf_bdr_id = 203; stroid::StroidMesh stroidMesh = stroid::GenerateMesh(config); REQUIRE(stroidMesh.reference_mesh != nullptr); REQUIRE(stroidMesh.mesh != nullptr); /* * The same semantic topology must validate when a mesh generator * uses an entirely different attribute numbering convention. */ domain_test_utils::check_schema_is_valid(*stroidMesh.reference_mesh); domain_test_utils::check_schema_is_valid(*stroidMesh.mesh); /* * Conversely, the production 1/2/3 + 1/2 schema must not silently * accept a mesh generated under another numbering convention. */ const auto productionValidation = mean_field::utils::domain::validate_schema( *stroidMesh.mesh ); CHECK_FALSE(productionValidation.valid()); CHECK(productionValidation.failed_relation_count() > 0); } TEST_CASE( "Complete Schema Validation Rejects A Mesh Without Vacuum", tags::unit &tags::mesh &tags::utils &tags::domain ) { const mfem::Mesh mesh = domain_test_utils::make_grid_mesh(3, 3, {2, 2, 2, 2, 1, 2, 2, 2, 2}, {}); const auto validation = mean_field::utils::domain::validate_schema(mesh); CHECK_FALSE(validation.valid()); REQUIRE(validation.relationResults.size() == 7); /* * Connected */ CHECK_FALSE(validation.relationResults[2].valid()); CHECK( validation.relationResults[2].result.failure == mean_field::utils::domain::RelationValidationFailure::DomainAbsent ); /* * Inscribed */ CHECK_FALSE(validation.relationResults[4].valid()); CHECK( validation.relationResults[4].result.failure == mean_field::utils::domain::RelationValidationFailure::OuterDomainAbsent ); }