#pragma once #include #include #include #include "serif/discretization/domain/concepts.hpp" #include "serif/discretization/domain/mesh/topology.hpp" #include "serif/discretization/domain/relation/relations.hpp" #include "serif/discretization/domain/relation/validation/runtime.hpp" #include "serif/discretization/domain/schema/concepts.hpp" #include "serif/discretization/domain/schema/validation/validator.hpp" namespace serif::discretization::domain::schema::validation { using relation::DomainBoundary; using relation::validation::RelationValidationResult; using relation::validation::RelationValidationFailure; template struct RelationValidator> { template [[nodiscard]] static RelationValidationResult validate(const mesh::MeshTopology &topology) { static_assert( sizeof...(DomainTs) == 1 || sizeof...(DomainTs) == 2, "DomainBoundary requires exactly one or two domains." ); static_assert( SchemaT::template contains_boundary(), "DomainBoundary refers to a boundary which is not registered in the supplied DomainSchema." ); static_assert( (SchemaT::template contains_domain() && ...), "DomainBoundary refers to a domain which is not completely registered in the supplied DomainSchema." ); constexpr int expectedBoundaryID = SchemaT::template boundary_id(); using DomainsTuple = std::tuple; const auto make_diagnostics = [&](const int faceID, const int boundaryElementID, const std::optional actualBoundaryID) { RelationValidationResult::DomainBoundaryDiagnostics diagnostics{ .faceID = faceID, .boundaryElementID = boundaryElementID, .expectedBoundaryID = expectedBoundaryID, .actualBoundaryID = actualBoundaryID }; if (faceID < 0 || faceID >= topology.face_count()) { return diagnostics; } const mesh::FaceElements faceElements = topology.face_elements(faceID); diagnostics.firstElementID = faceElements.firstElementID; diagnostics.secondElementID = faceElements.secondElementID; if (diagnostics.firstElementID >= 0) { diagnostics.firstDomainID = topology.element_domain_id(diagnostics.firstElementID); } if (diagnostics.secondElementID >= 0) { diagnostics.secondDomainID = topology.element_domain_id(diagnostics.secondElementID); } return diagnostics; }; // Check only the cardinality/topological shape required by the relation. // // One-domain form: // // Domain | computational exterior // // Exactly one adjacent volume element must exist. // // Two-domain form: // // DomainA | DomainB // // Both adjacent volume elements must exist. const auto has_required_topology = [](const int firstElementID, const int secondElementID) noexcept { if constexpr (sizeof...(DomainTs) == 1) { const bool firstExists = firstElementID >= 0; const bool secondExists = secondElementID >= 0; return firstExists != secondExists; } else { return firstElementID >= 0 && secondElementID >= 0; } }; // Determine whether a face is exactly one of the faces described by // DomainBoundary. For two domains, ordering // is intentionally irrelevant. const auto face_matches_domains = [&topology](const int firstElementID, const int secondElementID) noexcept { if constexpr (sizeof...(DomainTs) == 1) { using DomainT = std::tuple_element_t<0, DomainsTuple>; const bool firstExists = firstElementID >= 0; const bool secondExists = secondElementID >= 0; if (firstExists == secondExists) { return false; } const int elementID = firstExists ? firstElementID : secondElementID; const int domainID = topology.element_domain_id(elementID); return SchemaT::template domain_id_belongs_to(domainID); } else { using FirstDomainT = std::tuple_element_t<0, DomainsTuple>; using SecondDomainT = std::tuple_element_t<1, DomainsTuple>; if (firstElementID < 0 || secondElementID < 0) { return false; } const int firstDomainID = topology.element_domain_id(firstElementID); const int secondDomainID = topology.element_domain_id(secondElementID); const bool forwardMatch = SchemaT::template domain_id_belongs_to(firstDomainID) && SchemaT::template domain_id_belongs_to(secondDomainID); const bool reverseMatch = SchemaT::template domain_id_belongs_to(firstDomainID) && SchemaT::template domain_id_belongs_to(secondDomainID); return forwardMatch || reverseMatch; } }; bool foundTaggedBoundary = false; // Forward validation: every boundary element carrying BoundaryT must // lie on exactly the interface declared by DomainBoundary. for (int boundaryElementID = 0; boundaryElementID < topology.boundary_element_count(); ++boundaryElementID) { const int boundaryID = topology.boundary_element_boundary_id(boundaryElementID); if (boundaryID != expectedBoundaryID) { continue; } foundTaggedBoundary = true; const int faceID = topology.boundary_element_face(boundaryElementID); const auto [firstElementID, secondElementID] = topology.face_elements(faceID); if (!has_required_topology(firstElementID, secondElementID)) { return { .failure = RelationValidationFailure::DomainBoundaryTaggedFaceHasWrongTopology, .domainBoundaryDiagnostics = std::make_optional( make_diagnostics(faceID, boundaryElementID, boundaryID) ) }; } if (!face_matches_domains(firstElementID, secondElementID)) { return { .failure = RelationValidationFailure::DomainBoundaryTaggedFaceTouchesUnexpectedDomain, .domainBoundaryDiagnostics = std::make_optional( make_diagnostics(faceID, boundaryElementID, boundaryID) ) }; } } bool foundExpectedFace = false; // Reverse validation: every face having the declared domain // adjacency must carry BoundaryT. for (int faceID = 0; faceID < topology.face_count(); ++faceID) { const auto [firstElementID, secondElementID] = topology.face_elements(faceID); if (!face_matches_domains(firstElementID, secondElementID)) { continue; } foundExpectedFace = true; const std::vector &boundaryElementIDs = topology.boundary_elements_on_face(faceID); if (boundaryElementIDs.empty()) { return { .failure = RelationValidationFailure::DomainBoundaryExpectedFaceIsUntagged, .domainBoundaryDiagnostics = std::make_optional( make_diagnostics(faceID, -1, std::nullopt) ) }; } for (const int boundaryElementID : boundaryElementIDs) { const int actualBoundaryID = topology.boundary_element_boundary_id(boundaryElementID); if (actualBoundaryID == expectedBoundaryID) { continue; } return { .failure = RelationValidationFailure::DomainBoundaryExpectedFaceHasWrongID, .domainBoundaryDiagnostics = std::make_optional( make_diagnostics(faceID, boundaryElementID, actualBoundaryID) ) }; } } // If neither a correctly tagged boundary nor a face having the // required semantic topology exists, the declared DomainBoundary // simply is not realized by this mesh. if (!foundTaggedBoundary || !foundExpectedFace) { return { .failure = RelationValidationFailure::DomainBoundaryAbsent, .domainBoundaryDiagnostics = std::make_optional( make_diagnostics(-1, -1, std::nullopt) ) }; } return {}; } }; }