feat(mean_field): added dimensions, discritization, and start of eos

The full rewrite of mean_field into something maintainable is progressing. dimensions is mostly done, discritization (domain, blocks, and fields) is done, and eos is progressing quickly
This commit is contained in:
2026-09-15 10:42:00 -04:00
parent 7c99debf2f
commit d1f59d6d70
88 changed files with 324020 additions and 268 deletions

View File

@@ -0,0 +1,55 @@
#pragma once
#include <cstddef>
#include "serif/discretization/domain/types.hpp"
#include "serif/utils/misc/std_helper/cleaning.hpp"
#include "serif/utils/misc/std_helper/variadic_cleaning.hpp"
namespace serif::discretization::domain {
using utils::misc::std_helper::AIsBaseClassOfB;
using utils::misc::std_helper::CountVariadicArguments;
template <typename T>
concept IsDomain = AIsBaseClassOfB<T, Domain>;
template <typename T>
concept IsBoundary = AIsBaseClassOfB<T, Boundary>;
// A DomainSet is several volumes treated as one. StellarDomains
// (core + envelope) is the motivating example.
template <IsDomain... DomainTs>
struct DomainSet {
static constexpr std::size_t count = CountVariadicArguments<DomainTs...>();
};
template <IsBoundary... BoundaryTs>
struct BoundarySet {
static constexpr std::size_t count = CountVariadicArguments<BoundaryTs...>();
};
template <typename T>
constexpr bool is_domain_set_v = false;
template <IsDomain... DomainTs>
constexpr bool is_domain_set_v<DomainSet<DomainTs...>> = true;
template <typename T>
concept IsDomainSet = is_domain_set_v<T>;
template <typename T>
concept IsDomainOrSet = IsDomain<T> || IsDomainSet<T>;
template <IsDomainOrSet... DomainTs>
struct DomainOrSetList {
static constexpr std::size_t count = CountVariadicArguments<DomainTs...>();
};
// "This volume is the one tagged by that DomainID" / "this surface is the one
// tagged by that BoundaryID". Used by the resolvers in resolver.hpp.
template <typename DomainT, typename DomainIDT>
concept DomainIsIdentifiedBy = utils::misc::std_helper::SameType<DomainT, typename DomainIDT::domain_type>;
template <typename BoundaryT, typename BoundaryIDT>
concept BoundaryIsIdentifiedBy = utils::misc::std_helper::SameType<BoundaryT, typename BoundaryIDT::boundary_type>;
}

View File

@@ -0,0 +1,23 @@
#pragma once
#include <string_view>
#include "serif/discretization/domain/concepts.hpp"
// BoundaryID binds a semantic surface (a Boundary) to the integer boundary
// attribute MFEM stores in the mesh file.
//
// BoundaryID<StellarSurfaceBoundary, 1>
// "boundary elements with attribute 1 are the stellar surface"
namespace serif::discretization::domain::ids {
template <IsBoundary BoundaryT, int IdentifierValue>
struct BoundaryID {
using boundary_type = BoundaryT;
static constexpr int ID = IdentifierValue;
};
struct BoundaryDescriptor {
std::string_view name;
int ID;
};
}

View File

@@ -0,0 +1,27 @@
#pragma once
#include "serif/discretization/domain/ids/boundary.hpp"
#include "serif/discretization/domain/ids/domain.hpp"
namespace serif::discretization::domain::ids {
template <typename T>
constexpr bool is_domain_id_v = false;
template <IsDomain DomainT, int IdentifierValue>
constexpr bool is_domain_id_v<DomainID<DomainT, IdentifierValue>> = true;
template <typename T>
constexpr bool is_boundary_id_v = false;
template <IsBoundary BoundaryT, int IdentifierValue>
constexpr bool is_boundary_id_v<BoundaryID<BoundaryT, IdentifierValue>> = true;
template <typename T>
concept IsDomainID = is_domain_id_v<T>;
template <typename T>
concept IsBoundaryID = is_boundary_id_v<T>;
template <typename T>
concept IsDomainOrBoundaryID = IsDomainID<T> || IsBoundaryID<T>;
}

View File

@@ -0,0 +1,24 @@
#pragma once
#include <string_view>
#include "serif/discretization/domain/concepts.hpp"
// DomainID binds a semantic volume (a Domain) to the integer element attribute
// MFEM stores in the mesh file. In MFEM's own vocabulary this integer is the
// element "attribute"; in a mesh generator it is usually the "material id".
//
// DomainID<CoreDomain, 1> "elements with attribute 1 are the core"
namespace serif::discretization::domain::ids {
template <IsDomain DomainT, int IdentifierValue>
struct DomainID {
using domain_type = DomainT;
static constexpr int ID = IdentifierValue;
};
// Runtime-friendly (name, ID) pair, for logging and error messages.
struct DomainDescriptor {
std::string_view name;
int ID;
};
}

View File

@@ -0,0 +1,11 @@
#pragma once
#include "serif/discretization/domain/ids/lists/traits.hpp"
namespace serif::discretization::domain::ids::lists {
template <typename T>
concept IsDomainIDList = is_domain_id_list_v<T>;
template <typename T>
concept IsBoundaryIDList = is_boundary_id_list_v<T>;
}

View File

@@ -0,0 +1,31 @@
#pragma once
#include <array>
#include <cstddef>
#include "serif/discretization/domain/ids/validation/concepts.hpp"
#include "serif/utils/misc/std_helper/variadic_cleaning.hpp"
namespace serif::discretization::domain::ids::lists {
using utils::misc::std_helper::CountVariadicArguments;
template <IsDomainID... DomainIDTs>
requires validation::AreDistinctDomainIDs<DomainIDTs...>
struct DomainIDList {
static constexpr std::size_t count = CountVariadicArguments<DomainIDTs...>();
[[nodiscard]] static constexpr std::array<DomainDescriptor, count> descriptors() noexcept {
return {DomainDescriptor{.name = DomainIDTs::domain_type::name, .ID = DomainIDTs::ID}...};
}
};
template <IsBoundaryID... BoundaryIDTs>
requires validation::AreDistinctBoundaryIDs<BoundaryIDTs...>
struct BoundaryIDList {
static constexpr std::size_t count = CountVariadicArguments<BoundaryIDTs...>();
[[nodiscard]] static constexpr std::array<BoundaryDescriptor, count> descriptors() noexcept {
return {BoundaryDescriptor{.name = BoundaryIDTs::boundary_type::name, .ID = BoundaryIDTs::ID}...};
}
};
}

View File

@@ -0,0 +1,17 @@
#pragma once
#include "serif/discretization/domain/ids/lists/lists.hpp"
namespace serif::discretization::domain::ids::lists {
template <typename T>
constexpr bool is_domain_id_list_v = false;
template <IsDomainID... DomainIDTs>
constexpr bool is_domain_id_list_v<DomainIDList<DomainIDTs...>> = true;
template <typename T>
constexpr bool is_boundary_id_list_v = false;
template <IsBoundaryID... BoundaryIDTs>
constexpr bool is_boundary_id_list_v<BoundaryIDList<BoundaryIDTs...>> = true;
}

View File

@@ -0,0 +1,42 @@
#pragma once
#include "serif/discretization/domain/ids/concepts.hpp"
#include "serif/discretization/domain/ids/validation/unique_ids.hpp"
#include "serif/utils/misc/concepts/type_uniqueness.hpp"
namespace serif::discretization::domain::ids::validation {
using utils::misc::concepts::AllTypesAreUnique;
// --- ID value validation ---
template <typename... DomainIDTs>
concept HaveUniqueDomainIDs = all_unique_values(DomainIDTs::ID...);
template <typename... BoundaryIDTs>
concept HaveUniqueBoundaryIDs = all_unique_values(BoundaryIDTs::ID...);
// --- Type validation (Domains & Boundaries) ---
template <typename... DomainIDTs>
concept HaveUniqueDomains = AllTypesAreUnique<typename DomainIDTs::domain_type...>;
template <typename... BoundaryIDTs>
concept HaveUniqueBoundaries = AllTypesAreUnique<typename BoundaryIDTs::boundary_type...>;
// A well formed ID table: every entry is really an ID, no integer is reused,
// and no volume (or surface) is tagged twice.
//
// Note that these are variadic concepts, therefore they must be applied to a pack as a whole
// via a requires-clause. Writing them as a template parameter constraint
// (e.g. template <AreDistinctDomainIDs... Ts>) will apply them to each element
// individually, this checks nothing.
template <typename... DomainIDTs>
concept AreDistinctDomainIDs =
(IsDomainID<DomainIDTs> && ...) && // 1. everything in the pack really is a DomainID
HaveUniqueDomainIDs<DomainIDTs...> && // 2. no repeated integer attribute
HaveUniqueDomains<DomainIDTs...>; // 3. no volume tagged twice
template <typename... BoundaryIDTs>
concept AreDistinctBoundaryIDs =
(IsBoundaryID<BoundaryIDTs> && ...) && // 1. everything in the pack really is a BoundaryID
HaveUniqueBoundaryIDs<BoundaryIDTs...> && // 2. no repeated integer attribute
HaveUniqueBoundaries<BoundaryIDTs...>; // 3. no surface tagged twice
}

View File

@@ -0,0 +1,29 @@
#pragma once
#include <array>
#include <concepts>
#include <type_traits>
#include "serif/utils/misc/std_helper/variadic_cleaning.hpp"
namespace serif::discretization::domain::ids::validation {
// Returns true when every value handed to it is distinct.
template <typename... Ts>
requires (std::equality_comparable<Ts> && ...)
[[nodiscard]] consteval bool all_unique_values(Ts... values) noexcept {
constexpr std::size_t valueCount = utils::misc::std_helper::CountVariadicArguments<Ts...>();
if constexpr (valueCount < 2) {
return true;
} else {
const std::array<std::common_type_t<Ts...>, valueCount> vals{values...};
for (std::size_t i = 0; i < vals.size(); ++i) {
for (std::size_t j = i + 1; j < vals.size(); ++j) {
if (vals[i] == vals[j]) return false;
}
}
return true;
}
}
}

View File

@@ -0,0 +1,56 @@
#pragma once
#include <cstddef>
#include <vector>
namespace mfem {
class Mesh;
}
// MeshTopology
namespace serif::discretization::domain::mesh {
struct FaceElements {
int firstElementID{-1};
int secondElementID{-1};
};
class MeshTopology final {
public:
explicit MeshTopology(const mfem::Mesh &mesh);
[[nodiscard]] int element_count() const noexcept;
[[nodiscard]] int face_count() const noexcept;
[[nodiscard]] int boundary_element_count() const noexcept;
// The MFEM element attribute of a volume element, i.e. which DomainID
// the mesh claims this element belongs to.
[[nodiscard]] int element_domain_id(int elementID) const;
// The MFEM boundary attribute of a boundary element, i.e. which
// BoundaryID the mesh claims this boundary element belongs to.
[[nodiscard]] int boundary_element_boundary_id(int boundaryElementID) const;
[[nodiscard]] FaceElements face_elements(int faceID) const;
// Elements sharing an interior face with elementID. Not filtered by
// domain: callers that care about one domain filter as they walk.
[[nodiscard]] const std::vector<int> &element_neighbors(int elementID) const;
// The face a boundary element sits on, or -1 if MFEM reports none.
[[nodiscard]] int boundary_element_face(int boundaryElementID) const;
// A face may carry more than one boundary element
[[nodiscard]] const std::vector<int> &boundary_elements_on_face(int faceID) const;
private:
std::vector<int> m_elementDomainIDs;
std::vector<FaceElements> m_faceElements;
std::vector<std::vector<int>> m_elementNeighbors;
std::vector<int> m_boundaryElementBoundaryIDs;
std::vector<int> m_boundaryElementFaceIDs;
std::vector<std::vector<int>> m_boundaryElementsByFace;
static const std::vector<int> s_noElements;
};
}

View File

@@ -0,0 +1,11 @@
#pragma once
#include "serif/discretization/domain/types.hpp"
#include "serif/discretization/domain/concepts.hpp"
namespace serif::discretization::domain {
// Sets are named in the plural to distinguish them from the single volumes
// in types.hpp.
using StellarDomains = DomainSet<CoreDomain, EnvelopeDomain>;
using AllDomains = DomainSet<CoreDomain, EnvelopeDomain, VacuumDomain>;
}

View File

@@ -0,0 +1,11 @@
#pragma once
#include "serif/discretization/domain/relation/relations.hpp"
#include "serif/utils/misc/std_helper/cleaning.hpp"
namespace serif::discretization::domain::relation {
using utils::misc::std_helper::AIsBaseClassOfB;
template <typename R>
concept IsRelation = AIsBaseClassOfB<R, DomainRelation>;
}

View File

@@ -0,0 +1,8 @@
#pragma once
#include "serif/discretization/domain/relation/lists/traits.hpp"
namespace serif::discretization::domain::relation::lists {
template <typename RL>
concept IsRelationList = is_relation_list_v<RL>;
}

View File

@@ -0,0 +1,15 @@
#pragma once
#include <cstddef>
#include "serif/discretization/domain/relation/concepts.hpp"
#include "serif/utils/misc/std_helper/variadic_cleaning.hpp"
namespace serif::discretization::domain::relation::lists {
using utils::misc::std_helper::CountVariadicArguments;
template <IsRelation... RelationTs>
struct RelationList {
static constexpr std::size_t count = CountVariadicArguments<RelationTs...>();
};
}

View File

@@ -0,0 +1,11 @@
#pragma once
#include "serif/discretization/domain/relation/lists/relation_list.hpp"
namespace serif::discretization::domain::relation::lists {
template <typename T>
constexpr bool is_relation_list_v = false;
template <IsRelation... RelationTs>
constexpr bool is_relation_list_v<RelationList<RelationTs...>> = true;
}

View File

@@ -0,0 +1,49 @@
#pragma once
#include <string_view>
#include "serif/discretization/domain/concepts.hpp"
#include "serif/utils/misc/concepts/numeric.hpp"
#include "serif/utils/misc/std_helper/variadic_cleaning.hpp"
// The relations a schema can declare about a mesh. Each one is a statement that
// must hold for the mesh to be usable, and each has a matching runtime check in
// schema/validation/.
namespace serif::discretization::domain::relation {
using utils::misc::concepts::SetOfOneOrTwo;
using utils::misc::std_helper::CountVariadicArguments;
struct DomainRelation { };
// "Every face of A that is not interior to A borders B."
template <IsDomainOrSet A, IsDomainOrSet B>
struct Inscribed final : DomainRelation {
using inner_type = A;
using outer_type = B;
static constexpr std::string_view name = "inscribed";
};
// "A is a single connected blob of elements, not two separate pieces."
template <IsDomainOrSet A>
struct FullyConnected final : DomainRelation {
using domain_type = A;
static constexpr std::string_view name = "fully_connected";
};
// "BoundaryT is exactly the surface between these one or two volumes."
//
// One domain -> the surface between that volume and the outside of the mesh.
// Two domains -> the interface between the two volumes.
template <IsBoundary BoundaryT, IsDomainOrSet... DomainTs>
requires SetOfOneOrTwo<DomainTs...>
struct DomainBoundary final : DomainRelation {
using boundary_type = BoundaryT;
using domain_types = DomainOrSetList<DomainTs...>;
static constexpr std::size_t domain_count = CountVariadicArguments<DomainTs...>();
static constexpr std::string_view name = "domain_boundary";
};
}

View File

@@ -0,0 +1,8 @@
#pragma once
#include "serif/discretization/domain/relation/validation/validation.hpp"
namespace serif::discretization::domain::relation::validation {
template <typename DomainIDsT, typename BoundaryIDsT, typename RelationsT>
concept HaveValidRelationEntities = RelationsUseRegisteredEntities<DomainIDsT, BoundaryIDsT, RelationsT>::value;
}

View File

@@ -0,0 +1,73 @@
#pragma once
#include <cstdint>
#include <optional>
namespace serif::discretization::domain::relation::validation {
enum class RelationValidationFailure : std::uint8_t {
None,
// FullyConnected<DomainT>
DomainAbsent,
DomainDisconnected,
// Inscribed<InnerT, OuterT>
InnerDomainAbsent,
OuterDomainAbsent,
InnerDomainHasNoBoundary,
InnerDomainTouchesMeshBoundary,
InnerDomainTouchesUnexpectedDomain,
// DomainBoundary<BoundaryT, DomainTs...>
DomainBoundaryAbsent,
DomainBoundaryTaggedFaceHasWrongTopology,
DomainBoundaryTaggedFaceTouchesUnexpectedDomain,
DomainBoundaryExpectedFaceIsUntagged,
DomainBoundaryExpectedFaceHasWrongID
};
struct RelationValidationResult {
RelationValidationFailure failure{RelationValidationFailure::None};
struct InscribedDiagnostics {
int faceID{-1};
int innerElementID{-1};
int adjacentElementID{-1};
int adjacentDomainID{-1};
};
std::optional<InscribedDiagnostics> inscribedDiagnostics = std::nullopt;
struct ConnectedDiagnostics {
int elementID{-1};
int domainElementCount{0};
int visitedElementCount{0};
};
std::optional<ConnectedDiagnostics> connectedDiagnostics = std::nullopt;
struct DomainBoundaryDiagnostics {
int faceID{-1};
int boundaryElementID{-1};
int expectedBoundaryID{0};
std::optional<int> actualBoundaryID = std::nullopt;
int firstElementID{-1};
int secondElementID{-1};
std::optional<int> firstDomainID = std::nullopt;
std::optional<int> secondDomainID = std::nullopt;
};
std::optional<DomainBoundaryDiagnostics> domainBoundaryDiagnostics = std::nullopt;
[[nodiscard]] bool valid() const noexcept {
return failure == RelationValidationFailure::None;
}
[[nodiscard]] explicit operator bool() const noexcept {
return valid();
}
};
}

View File

@@ -0,0 +1,47 @@
#pragma once
#include <type_traits>
#include "serif/discretization/domain/concepts.hpp"
#include "serif/discretization/domain/ids/lists/concepts.hpp"
#include "serif/discretization/domain/relation/concepts.hpp"
#include "serif/discretization/domain/relation/lists/concepts.hpp"
#include "serif/discretization/domain/relation/lists/relation_list.hpp"
#include "serif/discretization/domain/resolver.hpp"
// Compile-time check that every volume and surface named by a relation is
// actually registered in the schema's ID tables.
namespace serif::discretization::domain::relation::validation {
using ids::lists::IsDomainIDList;
using ids::lists::IsBoundaryIDList;
using lists::IsRelationList;
// This header is primarily split into a singular and a plural section. The plural section simply folds over a pack to invoke the singular section.
template <IsRelation RelationT, IsDomainIDList DomainIDsT, IsBoundaryIDList BoundaryIDsT>
struct RelationUsesRegisteredEntities;
template <IsDomainOrSet DomainT, IsDomainIDList DomainIDsT, IsBoundaryIDList BoundaryIDsT>
struct RelationUsesRegisteredEntities<FullyConnected<DomainT>, DomainIDsT, BoundaryIDsT> :
std::bool_constant<DomainIDResolver<DomainT, DomainIDsT>::registered> { };
template <IsDomainOrSet InnerDomainT, IsDomainOrSet OuterDomainT, IsDomainIDList DomainIDsT, IsBoundaryIDList BoundaryIDsT>
struct RelationUsesRegisteredEntities<Inscribed<InnerDomainT, OuterDomainT>, DomainIDsT, BoundaryIDsT> :
std::bool_constant<
DomainIDResolver<InnerDomainT, DomainIDsT>::registered &&
DomainIDResolver<OuterDomainT, DomainIDsT>::registered> { };
//Note that the fold below is over DomainTs (the volumes the relation names).
template <IsBoundary BoundaryT, IsDomainOrSet... DomainTs, IsDomainIDList DomainIDsT, IsBoundaryIDList BoundaryIDsT>
struct RelationUsesRegisteredEntities<DomainBoundary<BoundaryT, DomainTs...>, DomainIDsT, BoundaryIDsT> :
std::bool_constant<
BoundaryIDResolver<BoundaryT, BoundaryIDsT>::registered &&
(DomainIDResolver<DomainTs, DomainIDsT>::registered && ...)> { };
template <IsDomainIDList DomainIDsT, IsBoundaryIDList BoundaryIDsT, typename RelationListT>
struct RelationsUseRegisteredEntities;
template <IsDomainIDList DomainIDsT, IsBoundaryIDList BoundaryIDsT, IsRelation... RelationTs>
struct RelationsUseRegisteredEntities<DomainIDsT, BoundaryIDsT, lists::RelationList<RelationTs...>> :
std::bool_constant<(RelationUsesRegisteredEntities<RelationTs, DomainIDsT, BoundaryIDsT>::value && ...)> { };
}

View File

@@ -0,0 +1,94 @@
#pragma once
#include "serif/discretization/domain/concepts.hpp"
#include "serif/discretization/domain/ids/lists/concepts.hpp"
#include "serif/discretization/domain/ids/lists/lists.hpp"
namespace serif::discretization::domain {
template <typename DomainT, typename DomainIDListT>
struct DomainIDResolver;
// In the following two structs there is a lot of folding, again (I am writing this comment after the comment regarding folding in utils/misc/traits/type_uniqueness.hpp was written).
// Anyway a brief overview of the folding in these structs follow
// # For the single domain specialization:
// 1. We make registered using a fold that checks that the domain is tagged by at least one of the IDs in the list.
// 2. contains_id then checks that both the domain is tagged by at least one entry and that the entry tagging the domain has the ID value that is passed in.
// note that this latter check is why we cannot simply check registered again.
// 3. id first validates that the domain is registered properly (e.g. the domain is tagged by at least one of the IDs in the list) and
// then it uses a fold to return the ID value of the entry that tags the domain using a ternary. Read this as looping over all entries in the
// list, then if the entry tags the domain then accumulate that ID, otherwise accumulate 0. Note that MFEM convention has it that element attributes start
// at one. This is a legitimate safety concern we may want to address at some point given that there is nothing stopping someone from making a mesh with
// an attribute of 0.
// # For the multiple domain specialization:
// 1. We make a helper alias which lets us query the single domain resolver for each domain in the set.
// 2. registered therefore asks if all of the domains in the set are registered with the ID list by folding over all domains and then asking the single domain resolver if that domain is registered with the ID list.
// 3. contains_id does basically the same thing as registered, but instead of asking if the domain is registered it asks if the domain contains the ID that is passed in.
// A specialization to resolve the element attribute for a single Domain
template <IsDomain DomainT, ids::IsDomainID... DomainIDTs>
struct DomainIDResolver<DomainT, ids::lists::DomainIDList<DomainIDTs...>> {
static constexpr bool registered = (DomainIsIdentifiedBy<DomainT, DomainIDTs> || ...);
[[nodiscard]] static constexpr bool contains_id(const int domainID) noexcept {
return ((DomainIsIdentifiedBy<DomainT, DomainIDTs> && DomainIDTs::ID == domainID) || ...);
}
[[nodiscard]] static consteval int id() {
static_assert(registered, "Requested domain is not registered in this schema.");
return ((DomainIsIdentifiedBy<DomainT, DomainIDTs> ? DomainIDTs::ID : 0) + ...);
}
};
// This specialization resolves the element attributes for a set of Domains
template <IsDomain... DomainTs, ids::IsDomainID... DomainIDTs>
struct DomainIDResolver<DomainSet<DomainTs...>, ids::lists::DomainIDList<DomainIDTs...>> {
template <typename D>
using ResolverFor = DomainIDResolver<D, ids::lists::DomainIDList<DomainIDTs...>>;
static constexpr bool registered = (ResolverFor<DomainTs>::registered && ...);
[[nodiscard]] static constexpr bool contains_id(const int domainID) noexcept {
return (ResolverFor<DomainTs>::contains_id(domainID) || ...);
}
};
template <typename BoundaryT, typename BoundaryIDListT>
struct BoundaryIDResolver;
// MFEM has no pleasant and clear way to
// distinguish the *idea* of a boundary from the integer attribute tagging it.
// We elect to use the vocabulary Boundary for the idea and BoundaryID for the MFEM integer (and
// Domain / DomainID on the volume side), so the two sides read symmetrically. Generally in MFEM these would be called Boundary and BoundaryAttribute.
// Aside from that these follow the same logic as the DomainIDResolver
// specializations above, so I will not repeat the comments here.
template <IsBoundary BoundaryT, ids::IsBoundaryID... BoundaryIDTs>
struct BoundaryIDResolver<BoundaryT, ids::lists::BoundaryIDList<BoundaryIDTs...>> {
static constexpr bool registered = (BoundaryIsIdentifiedBy<BoundaryT, BoundaryIDTs> || ...);
[[nodiscard]] static constexpr bool contains_id(const int boundaryID) noexcept {
return ((BoundaryIsIdentifiedBy<BoundaryT, BoundaryIDTs> && BoundaryIDTs::ID == boundaryID) || ...);
}
[[nodiscard]] static consteval int id() {
static_assert(registered, "Requested boundary is not registered in this schema.");
return ((BoundaryIsIdentifiedBy<BoundaryT, BoundaryIDTs> ? BoundaryIDTs::ID : 0) + ...);
}
};
template <IsBoundary... BoundaryTs, ids::IsBoundaryID... BoundaryIDTs>
struct BoundaryIDResolver<BoundarySet<BoundaryTs...>, ids::lists::BoundaryIDList<BoundaryIDTs...>> {
template <typename B>
using ResolverFor = BoundaryIDResolver<B, ids::lists::BoundaryIDList<BoundaryIDTs...>>;
static constexpr bool registered = (ResolverFor<BoundaryTs>::registered && ...);
[[nodiscard]] static constexpr bool contains_id(const int boundaryID) noexcept {
return (ResolverFor<BoundaryTs>::contains_id(boundaryID) || ...);
}
};
}

View File

@@ -0,0 +1,8 @@
#pragma once
#include "serif/discretization/domain/schema/traits.hpp"
namespace serif::discretization::domain::schema {
template <typename T>
concept IsSchema = is_schema_v<T>;
}

View File

@@ -0,0 +1,77 @@
#pragma once
#include <cstddef>
#include "serif/discretization/domain/concepts.hpp"
#include "serif/discretization/domain/ids/lists/concepts.hpp"
#include "serif/discretization/domain/relation/lists/concepts.hpp"
#include "serif/discretization/domain/relation/validation/concepts.hpp"
#include "serif/discretization/domain/resolver.hpp"
namespace serif::discretization::domain::schema {
using ids::lists::IsDomainIDList;
using ids::lists::IsBoundaryIDList;
using relation::lists::IsRelationList;
using relation::validation::HaveValidRelationEntities;
template <IsDomainIDList DomainIDs, IsBoundaryIDList BoundaryIDs, IsRelationList Relations>
requires HaveValidRelationEntities<DomainIDs, BoundaryIDs, Relations>
struct DomainSchema {
using domain_ids_type = DomainIDs;
using boundary_ids_type = BoundaryIDs;
using relations_type = Relations;
static constexpr std::size_t domain_id_count = DomainIDs::count;
static constexpr std::size_t boundary_id_count = BoundaryIDs::count;
static constexpr std::size_t relation_count = Relations::count;
[[nodiscard]] static constexpr auto domain_descriptors() noexcept {
return DomainIDs::descriptors();
}
[[nodiscard]] static constexpr auto boundary_descriptors() noexcept {
return BoundaryIDs::descriptors();
}
template <IsDomainOrSet DomainT>
[[nodiscard]] static consteval bool contains_domain() noexcept {
return DomainIDResolver<DomainT, DomainIDs>::registered;
}
// Does an element attribute read from a mesh belong to DomainT?
template <IsDomainOrSet DomainT>
[[nodiscard]] static constexpr bool domain_id_belongs_to(const int domainID) noexcept {
static_assert(contains_domain<DomainT>(), "DomainT is not registered in this schema.");
return DomainIDResolver<DomainT, DomainIDs>::contains_id(domainID);
}
// The single element attribute of a single volume.
template <IsDomain DomainT>
[[nodiscard]] static consteval int domain_id() noexcept {
static_assert(contains_domain<DomainT>(), "DomainT is not registered in this schema.");
return DomainIDResolver<DomainT, DomainIDs>::id();
}
template <IsBoundary BoundaryT>
[[nodiscard]] static consteval bool contains_boundary() noexcept {
return BoundaryIDResolver<BoundaryT, BoundaryIDs>::registered;
}
template <IsBoundary BoundaryT>
[[nodiscard]] static consteval int boundary_id() noexcept {
static_assert(contains_boundary<BoundaryT>(), "Requested boundary is not registered in this schema.");
return BoundaryIDResolver<BoundaryT, BoundaryIDs>::id();
}
// Does a boundary attribute read from a mesh belong to BoundaryT?
template <IsBoundary BoundaryT>
[[nodiscard]] static constexpr bool boundary_id_matches(const int boundaryID) noexcept {
static_assert(contains_boundary<BoundaryT>(), "Requested boundary is not registered in this schema.");
return BoundaryIDResolver<BoundaryT, BoundaryIDs>::contains_id(boundaryID);
}
};
}

View File

@@ -0,0 +1,50 @@
#pragma once
#include "serif/discretization/domain/ids/boundary.hpp"
#include "serif/discretization/domain/ids/domain.hpp"
#include "serif/discretization/domain/ids/lists/lists.hpp"
#include "serif/discretization/domain/physical_domains.hpp"
#include "serif/discretization/domain/relation/lists/relation_list.hpp"
#include "serif/discretization/domain/relation/relations.hpp"
#include "serif/discretization/domain/schema/domain_schema.hpp"
#include "serif/discretization/domain/types.hpp"
namespace serif::discretization::domain::schema {
using ids::DomainID;
using ids::BoundaryID;
using ids::lists::DomainIDList;
using ids::lists::BoundaryIDList;
using relation::lists::RelationList;
// A star (core inside envelope) surrounded by a vacuum region, with a
// tagged stellar surface and a tagged outer surface.
using CoreEnvelopeVacuumDomainSchema = DomainSchema<
DomainIDList<
DomainID<CoreDomain, 1>,
DomainID<EnvelopeDomain, 2>,
DomainID<VacuumDomain, 3>
>,
BoundaryIDList<
BoundaryID<StellarSurfaceBoundary, 1>,
BoundaryID<InfinitySurfaceBoundary, 2>
>,
RelationList<
// No domain may arrive as two disconnected blobs.
relation::FullyConnected<CoreDomain>,
relation::FullyConnected<EnvelopeDomain>,
relation::FullyConnected<VacuumDomain>,
// The core sits inside the envelope, and the star as a whole sits
// inside the vacuum region.
relation::Inscribed<CoreDomain, EnvelopeDomain>,
relation::Inscribed<StellarDomains, VacuumDomain>,
// The stellar surface is the star/vacuum interface; the infinity
// surface is the outer edge of the vacuum region.
relation::DomainBoundary<StellarSurfaceBoundary, StellarDomains, VacuumDomain>,
relation::DomainBoundary<InfinitySurfaceBoundary, VacuumDomain>
>
>;
}

View File

@@ -0,0 +1,11 @@
#pragma once
#include "serif/discretization/domain/schema/domain_schema.hpp"
namespace serif::discretization::domain::schema {
template <typename S>
constexpr bool is_schema_v = false;
template <IsDomainIDList DomainIDs, IsBoundaryIDList BoundaryIDs, IsRelationList Relations>
constexpr bool is_schema_v<DomainSchema<DomainIDs, BoundaryIDs, Relations>> = true;
}

View File

@@ -0,0 +1,24 @@
#pragma once
#include <mfem.hpp>
#include "serif/discretization/domain/concepts.hpp"
#include "serif/discretization/domain/schema/concepts.hpp"
namespace serif::discretization::domain::schema {
// Build the 0/1 marker array MFEM wants when an integrator or an essential
// BC should apply only on part of the mesh.
template <IsDomainOrSet DomainT, IsSchema SchemaT>
[[nodiscard]] mfem::Array<int> make_attribute_marker(const mfem::Mesh &mesh) {
static_assert(
SchemaT::template contains_domain<DomainT>(),
"Requested marker domain is not completely registered in the supplied DomainSchema."
);
mfem::Array<int> marker(mesh.attributes.Max());
for (int domainID = 1; domainID <= marker.Size(); ++domainID) {
marker[domainID - 1] = SchemaT::template domain_id_belongs_to<DomainT>(domainID) ? 1 : 0;
}
return marker;
}
}

View File

@@ -0,0 +1,33 @@
#pragma once
#include <mfem.hpp>
#include "serif/discretization/domain/mesh/topology.hpp"
#include "serif/discretization/domain/schema/concepts.hpp"
#include "serif/discretization/domain/schema/validation/results.hpp"
#include "serif/discretization/domain/schema/validation/validator.hpp"
// Every RelationValidator specialization must be visible before
// SchemaRelationValidator is instantiated, so they are all included here and
// validate_schema is defined only in this header. Include this file to validate
// a mesh; including validator.hpp alone is not enough.
#include "serif/discretization/domain/schema/validation/boundary.hpp"
#include "serif/discretization/domain/schema/validation/connected.hpp"
#include "serif/discretization/domain/schema/validation/inscribed.hpp"
namespace serif::discretization::domain::schema::validation {
// Validate against connectivity that has already been extracted. Prefer
// this overload when validating several schemas against the same mesh.
template <IsSchema SchemaT>
[[nodiscard]] SchemaValidationResult validate_schema(const mesh::MeshTopology &topology) {
using RelationsT = typename SchemaT::relations_type;
return SchemaRelationValidator<SchemaT, RelationsT>::validate(topology);
}
// Extract the mesh connectivity once, then check every relation against it.
template <IsSchema SchemaT>
[[nodiscard]] SchemaValidationResult validate_schema(const mfem::Mesh &mfemMesh) {
const mesh::MeshTopology topology{mfemMesh};
return validate_schema<SchemaT>(topology);
}
}

View File

@@ -0,0 +1,229 @@
#pragma once
#include <optional>
#include <tuple>
#include <vector>
#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 <IsBoundary BoundaryT, IsDomainOrSet... DomainTs>
struct RelationValidator<DomainBoundary<BoundaryT, DomainTs...>> {
template <IsSchema SchemaT>
[[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<BoundaryT>(),
"DomainBoundary refers to a boundary which is not registered in the supplied DomainSchema."
);
static_assert(
(SchemaT::template contains_domain<DomainTs>() && ...),
"DomainBoundary refers to a domain which is not completely registered in the supplied DomainSchema."
);
constexpr int expectedBoundaryID = SchemaT::template boundary_id<BoundaryT>();
using DomainsTuple = std::tuple<DomainTs...>;
const auto make_diagnostics = [&](const int faceID, const int boundaryElementID, const std::optional<int> 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<BoundaryT, DomainTs...>. 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<DomainT>(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<FirstDomainT>(firstDomainID) &&
SchemaT::template domain_id_belongs_to<SecondDomainT>(secondDomainID);
const bool reverseMatch = SchemaT::template domain_id_belongs_to<SecondDomainT>(firstDomainID) &&
SchemaT::template domain_id_belongs_to<FirstDomainT>(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<RelationValidationResult::DomainBoundaryDiagnostics>(
make_diagnostics(faceID, boundaryElementID, boundaryID)
)
};
}
if (!face_matches_domains(firstElementID, secondElementID)) {
return {
.failure = RelationValidationFailure::DomainBoundaryTaggedFaceTouchesUnexpectedDomain,
.domainBoundaryDiagnostics =
std::make_optional<RelationValidationResult::DomainBoundaryDiagnostics>(
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<int> &boundaryElementIDs = topology.boundary_elements_on_face(faceID);
if (boundaryElementIDs.empty()) {
return {
.failure = RelationValidationFailure::DomainBoundaryExpectedFaceIsUntagged,
.domainBoundaryDiagnostics =
std::make_optional<RelationValidationResult::DomainBoundaryDiagnostics>(
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<RelationValidationResult::DomainBoundaryDiagnostics>(
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<RelationValidationResult::DomainBoundaryDiagnostics>(
make_diagnostics(-1, -1, std::nullopt)
)
};
}
return {};
}
};
}

View File

@@ -0,0 +1,117 @@
#pragma once
#include <cstddef>
#include <optional>
#include <vector>
#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"
// FullyConnected<DomainT>: walk the domain from one of its elements and check we
// reach all of them.
namespace serif::discretization::domain::schema::validation {
using relation::FullyConnected;
using relation::validation::RelationValidationResult;
using relation::validation::RelationValidationFailure;
template <IsDomainOrSet DomainT>
struct RelationValidator<FullyConnected<DomainT>> {
template <IsSchema SchemaT>
[[nodiscard]] static RelationValidationResult validate(const mesh::MeshTopology &topology) {
static_assert(SchemaT::template contains_domain<DomainT>(), "Domain not present, cannot determine connectedness.");
const int elementCount = topology.element_count();
// We should be careful here that I have not accidentally introduced an issue due to the specialized vector bool override. I think this is safe though.
std::vector<bool> belongsToDomain(static_cast<std::size_t>(elementCount), false);
int domainElementCount = 0;
int firstDomainElement = -1;
for (int elementID = 0; elementID < elementCount; ++elementID) {
const int domainID = topology.element_domain_id(elementID);
const bool belongs = SchemaT::template domain_id_belongs_to<DomainT>(domainID);
belongsToDomain[static_cast<std::size_t>(elementID)] = belongs;
if (!belongs) {
continue;
}
++domainElementCount;
if (firstDomainElement < 0) {
firstDomainElement = elementID;
}
}
if (domainElementCount == 0) {
return {
.failure = RelationValidationFailure::DomainAbsent,
.connectedDiagnostics = std::make_optional<RelationValidationResult::ConnectedDiagnostics>({
.domainElementCount = 0,
.visitedElementCount = 0
})};
}
std::vector<bool> visited(static_cast<std::size_t>(elementCount), false);
std::vector<int> pending;
pending.reserve(static_cast<std::size_t>(domainElementCount));
pending.push_back(firstDomainElement);
int visitedElementCount = 0;
while (!pending.empty()) {
const int elementID = pending.back();
pending.pop_back();
if (visited[static_cast<std::size_t>(elementID)]) {
continue;
}
visited[static_cast<std::size_t>(elementID)] = true;
++visitedElementCount;
for (const int neighborElementID : topology.element_neighbors(elementID)) {
const std::size_t neighborIndex = static_cast<std::size_t>(neighborElementID);
if (belongsToDomain[neighborIndex] && !visited[neighborIndex]) {
pending.push_back(neighborElementID);
}
}
}
if (visitedElementCount == domainElementCount) {
return {
.connectedDiagnostics = std::make_optional<RelationValidationResult::ConnectedDiagnostics>(
{.domainElementCount = domainElementCount, .visitedElementCount = visitedElementCount}
)
};
}
int disconnectedElementID = -1;
for (int elementID = 0; elementID < elementCount; ++elementID) {
const std::size_t index = static_cast<std::size_t>(elementID);
if (belongsToDomain[index] && !visited[index]) {
disconnectedElementID = elementID;
break;
}
}
return {
.failure = RelationValidationFailure::DomainDisconnected,
.connectedDiagnostics = std::make_optional<RelationValidationResult::ConnectedDiagnostics>({
.elementID = disconnectedElementID,
.domainElementCount = domainElementCount,
.visitedElementCount = visitedElementCount
})};
}
};
}

View File

@@ -0,0 +1,101 @@
#pragma once
#include <optional>
#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"
// Inscribed<InnerT, OuterT>: every face where the inner domain stops must have
// the outer domain on the far side.
namespace serif::discretization::domain::schema::validation {
using relation::Inscribed;
using relation::validation::RelationValidationResult;
using relation::validation::RelationValidationFailure;
template <IsDomainOrSet InnerT, IsDomainOrSet OuterT>
struct RelationValidator<Inscribed<InnerT, OuterT>> {
template <IsSchema SchemaT>
[[nodiscard]] static RelationValidationResult validate(const mesh::MeshTopology &topology) {
static_assert(
SchemaT::template contains_domain<InnerT>(), "The inner domain of an Inscribed relation is not present in the supplied schema. Inscribed cannot be enforced"
);
static_assert(
SchemaT::template contains_domain<OuterT>(), "The outer domain of an Inscribed relation is not present in the supplied schema. Inscribed cannot be enforced"
);
bool foundInnerElement = false;
bool foundOuterElement = false;
bool foundInnerBoundary = false;
for (int elementID = 0; elementID < topology.element_count(); ++elementID) {
const int domainID = topology.element_domain_id(elementID);
foundInnerElement = foundInnerElement || SchemaT::template domain_id_belongs_to<InnerT>(domainID);
foundOuterElement = foundOuterElement || SchemaT::template domain_id_belongs_to<OuterT>(domainID);
}
if (!foundInnerElement) {
return {.failure = RelationValidationFailure::InnerDomainAbsent};
}
if (!foundOuterElement) {
return {.failure = RelationValidationFailure::OuterDomainAbsent};
}
for (int faceID = 0; faceID < topology.face_count(); ++faceID) {
const mesh::FaceElements faceElements = topology.face_elements(faceID);
const int firstElementID = faceElements.firstElementID;
const int secondElementID = faceElements.secondElementID;
const bool firstIsInner = firstElementID >= 0 &&
SchemaT::template domain_id_belongs_to<InnerT>(topology.element_domain_id(firstElementID));
const bool secondIsInner = secondElementID >= 0 &&
SchemaT::template domain_id_belongs_to<InnerT>(topology.element_domain_id(secondElementID));
if (firstIsInner == secondIsInner) {
continue;
}
foundInnerBoundary = true;
const int innerElementID = firstIsInner ? firstElementID : secondElementID;
const int adjacentElementID = firstIsInner ? secondElementID : firstElementID;
if (adjacentElementID < 0) {
return {
.failure = RelationValidationFailure::InnerDomainTouchesMeshBoundary,
.inscribedDiagnostics = std::make_optional<RelationValidationResult::InscribedDiagnostics>({
.faceID = faceID,
.innerElementID = innerElementID,
})};
}
const int adjacentDomainID = topology.element_domain_id(adjacentElementID);
if (!SchemaT::template domain_id_belongs_to<OuterT>(adjacentDomainID)) {
return {
.failure = RelationValidationFailure::InnerDomainTouchesUnexpectedDomain,
.inscribedDiagnostics = std::make_optional<RelationValidationResult::InscribedDiagnostics>({
.faceID = faceID,
.innerElementID = innerElementID,
.adjacentElementID = adjacentElementID,
.adjacentDomainID = adjacentDomainID
})};
}
}
if (!foundInnerBoundary) {
return {.failure = RelationValidationFailure::InnerDomainHasNoBoundary};
}
return {};
}
};
}

View File

@@ -0,0 +1,58 @@
#pragma once
#include <cstddef>
#include <optional>
#include <string_view>
#include <vector>
#include <format>
#include <string>
#include "serif/discretization/domain/relation/validation/runtime.hpp"
// The aggregate result of validating a whole schema against a mesh
namespace serif::discretization::domain::schema::validation {
struct SchemaRelationValidationResult {
std::size_t relationIndex{0};
std::string_view relationName;
relation::validation::RelationValidationResult result;
[[nodiscard]] bool valid() const noexcept;
[[nodiscard]] explicit operator bool() const noexcept;
};
struct SchemaValidationResult {
std::vector<SchemaRelationValidationResult> relationResults;
[[nodiscard]] bool valid() const noexcept;
[[nodiscard]] explicit operator bool() const noexcept;
[[nodiscard]] std::size_t relation_count() const noexcept;
[[nodiscard]] std::size_t failed_relation_count() const noexcept;
[[nodiscard]] std::size_t passed_relation_count() const noexcept;
[[nodiscard]] std::optional<std::size_t> first_failed_relation_index() const noexcept;
};
}
template <>
struct std::formatter<serif::discretization::domain::schema::validation::SchemaValidationResult> {
std::formatter<std::string> string_formatter;
constexpr auto parse(std::format_parse_context& ctx) {
return string_formatter.parse(ctx);
}
auto format(const serif::discretization::domain::schema::validation::SchemaValidationResult& result, std::format_context& ctx) const {
std::string output;
output += "SchemaValidationResult:\n";
output += std::format(" Valid: {}\n", result.valid());
output += std::format(" Relation count: {}\n", result.relation_count());
output += std::format(" Passed relation count: {}\n", result.passed_relation_count());
output += std::format(" Failed relation count: {}\n", result.failed_relation_count());
if (auto first_failed_index = result.first_failed_relation_index()) {
output += std::format(" First failed relation index: {}\n", *first_failed_index);
} else {
output += " No failed relations.\n";
}
return string_formatter.format(output, ctx);
}
};

View File

@@ -0,0 +1,49 @@
#pragma once
#include <cstddef>
#include "serif/discretization/domain/mesh/topology.hpp"
#include "serif/discretization/domain/relation/concepts.hpp"
#include "serif/discretization/domain/relation/lists/relation_list.hpp"
#include "serif/discretization/domain/relation/validation/runtime.hpp"
#include "serif/discretization/domain/schema/concepts.hpp"
#include "serif/discretization/domain/schema/validation/results.hpp"
// The primary RelationValidator template, plus the fold that runs one validator
// per declared relation.
//
// IMPORTANT: every relation kind is validated by an explicit specialization of
// RelationValidator, and those specializations live in sibling headers
// (inscribed.hpp, connected.hpp, boundary.hpp). Under headers, instantiating
// SchemaRelationValidator in a translation unit that has not yet seen all of
// those specializations is ill-formed with no diagnostic required. That is why
// validate_schema lives in all.hpp, which includes this header and every
// specialization. Include all.hpp, not this header, to validate a mesh.
namespace serif::discretization::domain::schema::validation {
using relation::IsRelation;
template <IsRelation RelationT>
struct RelationValidator;
template <IsSchema SchemaT, typename RelationListT>
struct SchemaRelationValidator;
template <IsSchema SchemaT, IsRelation... RelationTs>
struct SchemaRelationValidator<SchemaT, relation::lists::RelationList<RelationTs...>> {
[[nodiscard]] static SchemaValidationResult validate(const mesh::MeshTopology &topology) {
SchemaValidationResult schemaResult;
schemaResult.relationResults.reserve(sizeof...(RelationTs));
std::size_t relationIndex = 0;
(schemaResult.relationResults.push_back(SchemaRelationValidationResult{
.relationIndex = relationIndex++,
.relationName = RelationTs::name,
.result = RelationValidator<RelationTs>::template validate<SchemaT>(topology)
}), ...);
return schemaResult;
}
};
}

View File

@@ -0,0 +1,32 @@
#pragma once
#include <string_view>
// A domain is a volume while a boundary is
// a surface. The mapping from these types to
// the integer attributes stored in a mesh file lives in ids/ (DomainID and
// BoundaryID) and defined by a schema.
namespace serif::discretization::domain {
struct Domain { };
struct CoreDomain final : Domain {
static constexpr std::string_view name = "core";
};
struct EnvelopeDomain final : Domain {
static constexpr std::string_view name = "envelope";
};
struct VacuumDomain final : Domain {
static constexpr std::string_view name = "vacuum";
};
struct Boundary { };
struct StellarSurfaceBoundary final : Boundary {
static constexpr std::string_view name = "stellar_surface";
};
struct InfinitySurfaceBoundary final : Boundary {
static constexpr std::string_view name = "infinity_surface";
};
}