test(tests): added test suite

The test suite is extensive. Note that LLMs were used to aid in the development of the test suite. All LLM generated code was manually reviewed.
This commit is contained in:
2026-11-09 13:53:17 -05:00
parent 74e55a6859
commit cb20faeb3d
202 changed files with 35505 additions and 953 deletions

Binary file not shown.

View File

@@ -0,0 +1,59 @@
#include <vector>
#include <catch2/catch_test_macros.hpp>
#include "serif/discretization/domain/mesh/topology.hpp"
#include "serif/discretization/domain/schema/schemas.hpp"
#include "serif/discretization/domain/schema/validation/all.hpp"
namespace {
namespace domain = serif::discretization::domain;
/** @brief Represent the core, envelope, vacuum and their interfaces without a finite-element backend. */
[[nodiscard]] domain::mesh::MeshTopologyData layered_topology() {
return {
.elementDomainIDs = {1, 2, 3},
.faceElements = {{.firstElementID = 0, .secondElementID = 1}, {.firstElementID = 1, .secondElementID = 2}, {.firstElementID = 2}},
.boundaryElementBoundaryIDs = {1, 2},
.boundaryElementFaceIDs = {1, 2}
};
}
}
TEST_CASE("Physical schema validation uses backend-independent mesh connectivity", "[unit][discretization][domain]") {
const domain::mesh::MeshTopology topology(layered_topology());
const auto result = domain::schema::validation::validate_schema<domain::schema::CoreEnvelopeVacuumDomainSchema>(topology);
REQUIRE(result);
CHECK(result.relation_count() == 7);
CHECK(topology.element_neighbors(0) == std::vector<int>{1});
CHECK((topology.element_neighbors(1) == std::vector<int>{0, 2}));
CHECK(topology.boundary_elements_on_face(1) == std::vector<int>{0});
CHECK(topology.boundary_element_face(1) == 2);
CHECK(topology.face_elements(2).secondElementID == -1);
}
TEST_CASE("Neutral topology rejects invalid indexing before deriving adjacency", "[unit][discretization][domain]") {
domain::mesh::MeshTopologyData data = layered_topology();
SECTION("A face names an absent volume element") {
data.faceElements.front().firstElementID = 3;
}
SECTION("Boundary labels and faces have different counts") {
data.boundaryElementFaceIDs.pop_back();
}
SECTION("A boundary names an absent face") {
data.boundaryElementFaceIDs.front() = 3;
}
CHECK_THROWS(domain::mesh::MeshTopology(data));
}
TEST_CASE("Neutral topology retains unresolved and multiply labelled boundary faces", "[unit][discretization][domain]") {
domain::mesh::MeshTopologyData data = layered_topology();
data.boundaryElementBoundaryIDs.insert(data.boundaryElementBoundaryIDs.end(), {7, 8});
data.boundaryElementFaceIDs.insert(data.boundaryElementFaceIDs.end(), {1, -1});
const domain::mesh::MeshTopology topology(data);
CHECK((topology.boundary_elements_on_face(1) == std::vector<int>{0, 2}));
CHECK(topology.boundary_element_face(3) == -1);
CHECK(topology.boundary_element_boundary_id(3) == 8);
CHECK(topology.element_neighbors(-1).empty());
CHECK(topology.boundary_elements_on_face(100).empty());
CHECK(topology.element_domain_id(100) == 0);
}

Binary file not shown.

View File

@@ -1,42 +0,0 @@
#include <catch2/catch_test_macros.hpp>
#include <mfem.hpp>
#include "serif/discretization/domain/physical_domains.hpp"
#include "serif/discretization/domain/schema/schemas.hpp"
#include "serif/discretization/domain/schema/utils.hpp"
#include "serif/discretization/domain/types.hpp"
#include "serif/tests/test_tags.hpp"
#include "serif/tests/discritization/domain/domain_test_utils.hpp"
namespace domain = serif::discretization::domain;
namespace schema = domain::schema;
TEST_CASE(
"Domain Schema Builds Exact MFEM Attribute Markers",
tags::domain &tags::utils &tags::unit
) {
using Schema = schema::CoreEnvelopeVacuumDomainSchema;
const mfem::Mesh mesh = domain_test_utils::make_layered_mesh();
const mfem::Array<int> stellarMarker = schema::make_attribute_marker<domain::StellarDomains, Schema>(mesh);
const mfem::Array<int> vacuumMarker = schema::make_attribute_marker<domain::VacuumDomain, Schema>(mesh);
const mfem::Array<int> allMarker = schema::make_attribute_marker<domain::AllDomains, Schema>(mesh);
REQUIRE(stellarMarker.Size() == 3);
REQUIRE(vacuumMarker.Size() == 3);
REQUIRE(allMarker.Size() == 3);
CHECK(stellarMarker[0] == 1);
CHECK(stellarMarker[1] == 1);
CHECK(stellarMarker[2] == 0);
CHECK(vacuumMarker[0] == 0);
CHECK(vacuumMarker[1] == 0);
CHECK(vacuumMarker[2] == 1);
CHECK(allMarker[0] == 1);
CHECK(allMarker[1] == 1);
CHECK(allMarker[2] == 1);
}

View File

@@ -3,11 +3,11 @@
#include <string_view>
#include <catch2/catch_test_macros.hpp>
#include <mfem.hpp>
#include "mfem/mesh/mesh.hpp"
#include <stroid/stroid.h>
#include "serif/discretization/domain/schema/schemas.hpp"
#include "serif/discretization/domain/schema/validation/all.hpp"
#include "serif/backend/mfem/discretization/schema.hpp"
#include "serif/tests/test_tags.hpp"
#include "serif/tests/discritization/domain/domain_test_utils.hpp"
@@ -23,7 +23,7 @@ TEST_CASE(
const mfem::Mesh mesh = domain_test_utils::make_layered_mesh();
const auto validation =
schema_validation::validate_schema<schema::CoreEnvelopeVacuumDomainSchema>(mesh);
serif::backend::mfem::discretization::validate_schema<schema::CoreEnvelopeVacuumDomainSchema>(mesh);
REQUIRE(validation.valid());
REQUIRE(validation.relationResults.size() == 7);
@@ -53,7 +53,7 @@ TEST_CASE(
*/
const mfem::Mesh mesh = domain_test_utils::make_layered_mesh(false, true);
const auto validation =
schema_validation::validate_schema<schema::CoreEnvelopeVacuumDomainSchema>(mesh);
serif::backend::mfem::discretization::validate_schema<schema::CoreEnvelopeVacuumDomainSchema>(mesh);
CHECK_FALSE(validation.valid());
REQUIRE(validation.relationResults.size() == 7);
CHECK(validation.failed_relation_count() == 1);
@@ -145,7 +145,7 @@ TEST_CASE(
* accept a mesh generated under another numbering convention.
*/
const auto productionValidation =
schema_validation::validate_schema<schema::CoreEnvelopeVacuumDomainSchema>(*stroidMesh.mesh);
serif::backend::mfem::discretization::validate_schema<schema::CoreEnvelopeVacuumDomainSchema>(*stroidMesh.mesh);
CHECK_FALSE(productionValidation.valid());
CHECK(productionValidation.failed_relation_count() > 0);
@@ -157,7 +157,7 @@ TEST_CASE(
) {
const mfem::Mesh mesh = domain_test_utils::make_grid_mesh(3, 3, {2, 2, 2, 2, 1, 2, 2, 2, 2}, {});
const auto validation = schema_validation::validate_schema<schema::CoreEnvelopeVacuumDomainSchema>(mesh);
const auto validation = serif::backend::mfem::discretization::validate_schema<schema::CoreEnvelopeVacuumDomainSchema>(mesh);
CHECK_FALSE(validation.valid());
REQUIRE(validation.relationResults.size() == 7);

View File

@@ -1,9 +1,9 @@
#include <vector>
#include <catch2/catch_test_macros.hpp>
#include <mfem.hpp>
#include "mfem/mesh/mesh.hpp"
#include "serif/discretization/domain/mesh/topology.hpp"
#include "serif/backend/mfem/discretization/topology.hpp"
#include "serif/discretization/domain/physical_domains.hpp"
#include "serif/discretization/domain/relation/relations.hpp"
#include "serif/discretization/domain/relation/validation/runtime.hpp"
@@ -20,6 +20,7 @@ namespace schema = domain::schema;
namespace validation = schema::validation;
using domain::mesh::MeshTopology;
using serif::backend::mfem::discretization::make_mesh_topology;
TEST_CASE(
"Domain Boundary Accepts A Complete Internal Stellar Vacuum Interface",
@@ -28,7 +29,7 @@ TEST_CASE(
std::vector<domain_test_utils::BoundaryEdge> boundaries{{.firstVertexId = 1, .secondVertexId = 4, .attribute = 1}};
const mfem::Mesh mesh = domain_test_utils::make_grid_mesh(2, 1, {2, 3}, boundaries);
const MeshTopology topology{mesh};
const MeshTopology topology = make_mesh_topology(mesh);
const auto result = validation::RelationValidator<relation::DomainBoundary<
domain::StellarSurfaceBoundary, domain::StellarDomains,
@@ -63,7 +64,7 @@ TEST_CASE(
const mfem::Mesh mesh = domain_test_utils::make_grid_mesh(1, 1, attributes, boundaries);
const auto result = validation::RelationValidator<relation::DomainBoundary<
domain::InfinitySurfaceBoundary, domain::VacuumDomain>>::
template validate<schema::CoreEnvelopeVacuumDomainSchema>(MeshTopology{mesh});
template validate<schema::CoreEnvelopeVacuumDomainSchema>(make_mesh_topology(mesh));
CHECK(result);
}
@@ -76,7 +77,7 @@ TEST_CASE(
const auto result = validation::RelationValidator<relation::DomainBoundary<
domain::InfinitySurfaceBoundary, domain::VacuumDomain>>::
template validate<schema::CoreEnvelopeVacuumDomainSchema>(MeshTopology{mesh});
template validate<schema::CoreEnvelopeVacuumDomainSchema>(make_mesh_topology(mesh));
CHECK_FALSE(result);
CHECK(
@@ -92,7 +93,7 @@ TEST_CASE(
const auto result = validation::RelationValidator<relation::DomainBoundary<
domain::InfinitySurfaceBoundary, domain::VacuumDomain>>::
template validate<schema::CoreEnvelopeVacuumDomainSchema>(MeshTopology{mesh});
template validate<schema::CoreEnvelopeVacuumDomainSchema>(make_mesh_topology(mesh));
CHECK_FALSE(result);
CHECK(
@@ -112,7 +113,7 @@ TEST_CASE(
const auto result = validation::RelationValidator<relation::DomainBoundary<
domain::StellarSurfaceBoundary, domain::StellarDomains,
domain::VacuumDomain>>::
template validate<schema::CoreEnvelopeVacuumDomainSchema>(MeshTopology{mesh});
template validate<schema::CoreEnvelopeVacuumDomainSchema>(make_mesh_topology(mesh));
CHECK_FALSE(result);
@@ -131,7 +132,7 @@ TEST_CASE(
const auto result = validation::RelationValidator<relation::DomainBoundary<
domain::StellarSurfaceBoundary, domain::StellarDomains,
domain::VacuumDomain>>::
template validate<schema::CoreEnvelopeVacuumDomainSchema>(MeshTopology{mesh});
template validate<schema::CoreEnvelopeVacuumDomainSchema>(make_mesh_topology(mesh));
CHECK_FALSE(result);
CHECK(result.failure == validation::RelationValidationFailure::DomainBoundaryExpectedFaceIsUntagged);
@@ -150,7 +151,7 @@ TEST_CASE(
const auto result = validation::RelationValidator<relation::DomainBoundary<
domain::StellarSurfaceBoundary, domain::StellarDomains,
domain::VacuumDomain>>::
template validate<schema::CoreEnvelopeVacuumDomainSchema>(MeshTopology{mesh});
template validate<schema::CoreEnvelopeVacuumDomainSchema>(make_mesh_topology(mesh));
CHECK_FALSE(result);
CHECK(
@@ -172,7 +173,7 @@ TEST_CASE(
const auto result = validation::RelationValidator<relation::DomainBoundary<
domain::StellarSurfaceBoundary, domain::StellarDomains,
domain::VacuumDomain>>::
template validate<schema::CoreEnvelopeVacuumDomainSchema>(MeshTopology{mesh});
template validate<schema::CoreEnvelopeVacuumDomainSchema>(make_mesh_topology(mesh));
CHECK_FALSE(result);
CHECK(result.failure == validation::RelationValidationFailure::DomainBoundaryAbsent);

View File

@@ -1,7 +1,7 @@
#include <catch2/catch_test_macros.hpp>
#include <mfem.hpp>
#include "mfem/mesh/mesh.hpp"
#include "serif/discretization/domain/mesh/topology.hpp"
#include "serif/backend/mfem/discretization/topology.hpp"
#include "serif/discretization/domain/physical_domains.hpp"
#include "serif/discretization/domain/relation/relations.hpp"
#include "serif/discretization/domain/relation/validation/runtime.hpp"
@@ -18,13 +18,14 @@ namespace schema = domain::schema;
namespace validation = schema::validation;
using domain::mesh::MeshTopology;
using serif::backend::mfem::discretization::make_mesh_topology;
TEST_CASE(
"Connected Accepts Face Connected Atomic And Composite Domains",
tags::unit &tags::mesh &tags::utils &tags::domain
) {
const mfem::Mesh mesh = domain_test_utils::make_layered_mesh();
const MeshTopology topology{mesh};
const MeshTopology topology = make_mesh_topology(mesh);
const auto coreResult = validation::RelationValidator<relation::FullyConnected<domain::CoreDomain>>::template validate<schema::CoreEnvelopeVacuumDomainSchema>(topology);
@@ -47,7 +48,7 @@ TEST_CASE(
) {
const mfem::Mesh mesh = domain_test_utils::make_grid_mesh(2, 1, {2, 2}, {});
const auto result = validation::RelationValidator<relation::FullyConnected<domain::CoreDomain>>::template validate<schema::CoreEnvelopeVacuumDomainSchema>(MeshTopology{mesh});
const auto result = validation::RelationValidator<relation::FullyConnected<domain::CoreDomain>>::template validate<schema::CoreEnvelopeVacuumDomainSchema>(make_mesh_topology(mesh));
CHECK_FALSE(result);
CHECK(result.failure == validation::RelationValidationFailure::DomainAbsent);
@@ -62,7 +63,7 @@ TEST_CASE(
) {
const mfem::Mesh mesh = domain_test_utils::make_grid_mesh(3, 1, {1, 2, 1}, {});
const auto result = validation::RelationValidator<relation::FullyConnected<domain::CoreDomain>>::template validate<schema::CoreEnvelopeVacuumDomainSchema>(MeshTopology{mesh});
const auto result = validation::RelationValidator<relation::FullyConnected<domain::CoreDomain>>::template validate<schema::CoreEnvelopeVacuumDomainSchema>(make_mesh_topology(mesh));
CHECK_FALSE(result);
CHECK(result.failure == validation::RelationValidationFailure::DomainDisconnected);

View File

@@ -1,9 +1,9 @@
#include <vector>
#include <catch2/catch_test_macros.hpp>
#include <mfem.hpp>
#include "mfem/mesh/mesh.hpp"
#include "serif/discretization/domain/mesh/topology.hpp"
#include "serif/backend/mfem/discretization/topology.hpp"
#include "serif/discretization/domain/physical_domains.hpp"
#include "serif/discretization/domain/relation/relations.hpp"
#include "serif/discretization/domain/relation/validation/runtime.hpp"
@@ -20,13 +20,14 @@ namespace schema = domain::schema;
namespace validation = schema::validation;
using domain::mesh::MeshTopology;
using serif::backend::mfem::discretization::make_mesh_topology;
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 MeshTopology topology{mesh};
const MeshTopology topology = make_mesh_topology(mesh);
const auto coreResult = validation::RelationValidator<relation::Inscribed<domain::CoreDomain, domain::EnvelopeDomain>>::template validate<schema::CoreEnvelopeVacuumDomainSchema>(topology);
@@ -43,7 +44,7 @@ TEST_CASE(
) {
const mfem::Mesh mesh = domain_test_utils::make_grid_mesh(2, 2, {2, 2, 2, 2}, {});
const auto result = validation::RelationValidator<relation::Inscribed<domain::CoreDomain, domain::EnvelopeDomain>>::template validate<schema::CoreEnvelopeVacuumDomainSchema>(MeshTopology{mesh});
const auto result = validation::RelationValidator<relation::Inscribed<domain::CoreDomain, domain::EnvelopeDomain>>::template validate<schema::CoreEnvelopeVacuumDomainSchema>(make_mesh_topology(mesh));
CHECK_FALSE(result);
CHECK(result.failure == validation::RelationValidationFailure::InnerDomainAbsent);
@@ -55,7 +56,7 @@ TEST_CASE(
) {
const mfem::Mesh mesh = domain_test_utils::make_grid_mesh(1, 1, {1}, {});
const auto result = validation::RelationValidator<relation::Inscribed<domain::CoreDomain, domain::EnvelopeDomain>>::template validate<schema::CoreEnvelopeVacuumDomainSchema>(MeshTopology{mesh});
const auto result = validation::RelationValidator<relation::Inscribed<domain::CoreDomain, domain::EnvelopeDomain>>::template validate<schema::CoreEnvelopeVacuumDomainSchema>(make_mesh_topology(mesh));
CHECK_FALSE(result);
CHECK(result.failure == validation::RelationValidationFailure::OuterDomainAbsent);
@@ -67,7 +68,7 @@ TEST_CASE(
) {
const mfem::Mesh mesh = domain_test_utils::make_grid_mesh(2, 2, {1, 2, 2, 2}, {});
const auto result = validation::RelationValidator<relation::Inscribed<domain::CoreDomain, domain::EnvelopeDomain>>::template validate<schema::CoreEnvelopeVacuumDomainSchema>(MeshTopology{mesh});
const auto result = validation::RelationValidator<relation::Inscribed<domain::CoreDomain, domain::EnvelopeDomain>>::template validate<schema::CoreEnvelopeVacuumDomainSchema>(make_mesh_topology(mesh));
CHECK_FALSE(result);
CHECK(result.failure == validation::RelationValidationFailure::InnerDomainTouchesMeshBoundary);
@@ -87,7 +88,7 @@ TEST_CASE(
const auto result = validation::RelationValidator<
relation::Inscribed<domain::CoreDomain, domain::EnvelopeDomain>>::
template validate<schema::CoreEnvelopeVacuumDomainSchema>(MeshTopology{mesh});
template validate<schema::CoreEnvelopeVacuumDomainSchema>(make_mesh_topology(mesh));
CHECK_FALSE(result);
CHECK(result.failure == validation::RelationValidationFailure::InnerDomainTouchesUnexpectedDomain);

View File

@@ -0,0 +1,279 @@
// MeanField field_dof_map.cpp and field_mfem.cpp invariants expressed through serial TermBinding.
#include <algorithm>
#include <array>
#include <limits>
#include <memory>
#include <vector>
#include <catch2/catch_test_macros.hpp>
#include "mfem/fem/gridfunc.hpp"
#include "serif/tests/equations/test_equations.hpp"
#include "serif/backend/mfem/discretization/dof_selection.hpp"
#include "serif/discretization/domain/schema/schemas.hpp"
#include "serif/tests/deformation/mesh.hpp"
namespace
{
namespace discretization = serif::discretization;
namespace backend = discretization::backend::mfem;
namespace blocks = discretization::blocks;
namespace domain = discretization::domain;
using Schema = domain::schema::CoreEnvelopeVacuumDomainSchema;
using AlternateSchema = domain::schema::DomainSchema<domain::ids::lists::DomainIDList<domain::ids::DomainID<domain::CoreDomain, 11>, domain::ids::DomainID<domain::EnvelopeDomain, 17>, domain::ids::DomainID<domain::VacuumDomain, 29>>, domain::ids::lists::BoundaryIDList<>, domain::relation::lists::RelationList<>>;
/** @brief A stellar velocity represented by independent discontinuous scalar components. */
struct DiscontinuousVelocity final : blocks::field
{
struct vector final : blocks::term<DiscontinuousVelocity>
{
using FunctionalSpace = discretization::functional_space::DiscreteFunctionalSpace<discretization::functional_space::L2, 2, 1>;
using Support = domain::StellarDomains;
};
};
/** @brief Two adjacent cells expose both inactive vacuum DOFs and shared continuous trace DOFs. */
[[nodiscard]] mfem::Mesh split_mesh(const int stellar_attribute = 2, const int vacuum_attribute = 3)
{
mfem::Mesh mesh = mfem::Mesh::MakeCartesian3D(2, 1, 1, mfem::Element::HEXAHEDRON, 2.0, 1.0, 1.0);
mesh.GetElement(0)->SetAttribute(stellar_attribute);
mesh.GetElement(1)->SetAttribute(vacuum_attribute);
mesh.SetAttributes();
return mesh;
}
/** @brief Independently mark the native element DOFs selected by semantic support. */
template <typename Term, typename DomainSchema>
[[nodiscard]] std::vector<bool> expected_support(const mfem::FiniteElementSpace& space)
{
std::vector<bool> active(space.GetVSize(), false);
mfem::Array<int> dofs;
for (int element = 0; element < space.GetNE(); ++element)
{
if (!DomainSchema::template domain_id_belongs_to<typename Term::Support>(space.GetMesh()->GetAttribute(element)))
{
continue;
}
space.GetElementVDofs(element, dofs);
for (const int encoded : dofs)
{
active[encoded < 0 ? -1 - encoded : encoded] = true;
}
}
return active;
}
/** @brief Compare canonical indexing, projection, and GridFunction transfer with the independent marker. */
template <typename Term, typename DomainSchema = Schema>
void check_supported_projection(mfem::FiniteElementSpace& space)
{
const backend::TermBinding<Term, DomainSchema> binding{space};
const std::vector<bool> active = expected_support<Term, DomainSchema>(space);
REQUIRE(binding.full_size() == space.GetVSize());
CHECK(binding.size() == std::ranges::count(active, true));
mfem::Vector full(binding.full_size());
for (int dof = 0; dof < full.Size(); ++dof)
{
full[dof] = 10.0 + dof;
}
const mfem::Vector reduced = binding.gather(full);
const mfem::Vector projected = binding.scatter(reduced);
const mfem::Vector recovered = binding.gather(projected);
int next_reduced = 0;
for (int dof = 0; dof < full.Size(); ++dof)
{
CAPTURE(dof);
const std::optional<int> index = binding.reduced_dof(dof);
CHECK(index.has_value() == active[dof]);
if (active[dof])
{
REQUIRE(index);
CHECK(*index == next_reduced++);
CHECK(reduced[*index] == full[dof]);
CHECK(recovered[*index] == reduced[*index]);
CHECK(projected[dof] == full[dof]);
}
else
{
CHECK(projected[dof] == 0.0);
}
}
// MFEM owns the GridFunction conversion; no additional field-adapter lifecycle is needed.
mfem::GridFunction grid(&space);
grid.SetFromTrueDofs(projected);
mfem::Vector true_values;
grid.GetTrueDofs(true_values);
const mfem::Vector grid_values = binding.gather(true_values);
for (int dof = 0; dof < reduced.Size(); ++dof)
{
CHECK(grid_values[dof] == reduced[dof]);
}
std::vector<double> storage(full.Size() + 2, -77.0);
std::copy_n(full.GetData(), full.Size(), storage.data() + 1);
const mfem::Vector full_view(storage.data() + 1, full.Size());
const mfem::Vector from_view = binding.gather(full_view);
for (int dof = 0; dof < reduced.Size(); ++dof)
{
CHECK(from_view[dof] == reduced[dof]);
}
CHECK(storage.front() == -77.0);
CHECK(storage.back() == -77.0);
CHECK_THROWS_AS(binding.reduced_dof(-1), std::out_of_range);
CHECK_THROWS_AS(binding.reduced_dof(binding.full_size()), std::out_of_range);
CHECK_THROWS_AS(binding.supports(-1), std::out_of_range);
CHECK_THROWS_AS(binding.supports(space.GetNE()), std::out_of_range);
CHECK_THROWS_AS(binding.gather(mfem::Vector(binding.full_size() + 1)), std::invalid_argument);
CHECK_THROWS_AS(binding.scatter(mfem::Vector(binding.size() + 1)), std::invalid_argument);
}
}
TEST_CASE("MeanField supported field projections preserve canonical indexing and native GridFunctions", "[unit][discretization][meanfield]")
{
mfem::Mesh mesh = split_mesh();
mfem::H1_FECollection density_collection(blocks::density::mass::FunctionalSpace::order, 3);
mfem::L2_FECollection velocity_collection(2, 3);
mfem::H1_FECollection enthalpy_collection(3, 3);
mfem::FiniteElementSpace density(&mesh, &density_collection);
mfem::FiniteElementSpace enthalpy(&mesh, &enthalpy_collection);
mfem::H1_FECollection potential_collection(blocks::gravity::potential::FunctionalSpace::order, 3);
mfem::FiniteElementSpace potential(&mesh, &potential_collection);
check_supported_projection<blocks::density::mass>(density);
check_supported_projection<::serif::tests::equations::Enthalpy>(enthalpy);
check_supported_projection<blocks::gravity::potential>(potential);
for (const mfem::Ordering::Type ordering : {mfem::Ordering::byNODES, mfem::Ordering::byVDIM})
{
mfem::FiniteElementSpace displacement(&mesh, &enthalpy_collection, 3, ordering);
check_supported_projection<blocks::displacement::vector>(displacement);
mfem::FiniteElementSpace velocity(&mesh, &velocity_collection, 3, ordering);
CHECK(velocity.GetFE(0)->GetOrder() == 2);
CHECK(velocity.GetVDim() == 3);
CHECK(velocity.GetOrdering() == ordering);
check_supported_projection<DiscontinuousVelocity::vector>(velocity);
}
CHECK_THROWS_AS((backend::TermBinding<DiscontinuousVelocity::vector, Schema>{density}), std::invalid_argument);
}
TEST_CASE("MeanField stellar H1 fields retain interface values and exclude vacuum-only coefficients", "[unit][discretization][meanfield]")
{
mfem::Mesh mesh = split_mesh();
mfem::H1_FECollection h1(3, 3);
mfem::FiniteElementSpace enthalpy_space(&mesh, &h1), density_space(&mesh, &h1);
const backend::TermBinding<::serif::tests::equations::Enthalpy, Schema> enthalpy{enthalpy_space};
const backend::TermBinding<blocks::density::mass, Schema> density{density_space};
mfem::Array<int> stellar, vacuum;
enthalpy_space.GetElementDofs(0, stellar);
enthalpy_space.GetElementDofs(1, vacuum);
int shared = 0;
int excluded = 0;
for (const int dof : vacuum)
{
const bool on_interface = std::find(stellar.begin(), stellar.end(), dof) != stellar.end();
CHECK(enthalpy.reduced_dof(dof).has_value() == on_interface);
shared += on_interface;
excluded += !on_interface;
}
CHECK(shared > 0);
CHECK(excluded > 0);
density_space.GetElementDofs(0, stellar);
density_space.GetElementDofs(1, vacuum);
shared = 0;
excluded = 0;
for (const int dof : vacuum)
{
const bool on_interface = std::find(stellar.begin(), stellar.end(), dof) != stellar.end();
CHECK(density.reduced_dof(dof).has_value() == on_interface);
shared += on_interface;
excluded += !on_interface;
}
CHECK(shared > 0);
CHECK(excluded > 0);
CHECK(enthalpy.supports(0));
CHECK_FALSE(enthalpy.supports(1));
CHECK(density.supports(0));
CHECK_FALSE(density.supports(1));
}
TEST_CASE("MeanField field support follows alternate semantic material IDs", "[unit][discretization][meanfield]")
{
mfem::Mesh mesh = split_mesh(17, 29);
mfem::H1_FECollection h1(3, 3);
mfem::FiniteElementSpace density(&mesh, &h1), enthalpy(&mesh, &h1);
check_supported_projection<blocks::density::mass, AlternateSchema>(density);
check_supported_projection<::serif::tests::equations::Enthalpy, AlternateSchema>(enthalpy);
}
TEST_CASE("MeanField supported maps reject stale bindings and malformed selections", "[unit][discretization][meanfield]")
{
mfem::Mesh mesh = split_mesh();
mfem::H1_FECollection collection(3, 3);
mfem::FiniteElementSpace space(&mesh, &collection, 3);
const backend::TermBinding<blocks::displacement::vector, Schema> binding{space};
for (const std::vector<int>& entries : {std::vector<int>{-1, 2}, {1, binding.size()}, {1, 1, 3}, {1, 3, 2}})
{
mfem::Array<int> selected(static_cast<int>(entries.size()));
std::copy(entries.begin(), entries.end(), selected.begin());
CHECK_THROWS_AS(backend::DofSelection(binding, selected), std::invalid_argument);
}
mfem::Vector values(binding.size());
values = 0.0;
CHECK_NOTHROW(binding.validate_values(values));
values[0] = std::numeric_limits<double>::quiet_NaN();
CHECK_THROWS_AS(binding.scatter(values), std::domain_error);
mesh.GetElement(0)->SetAttribute(3);
CHECK_THROWS(binding.validate());
}
TEST_CASE("MeanField registered spaces retain family order vector dimension and global scalar storage", "[unit][discretization][meanfield]")
{
mfem::Mesh mesh = split_mesh();
mfem::H1_FECollection h1(3, 3);
mfem::H1_FECollection potential_collection(blocks::gravity::potential::FunctionalSpace::order, 3);
mfem::FiniteElementSpace potential(&mesh, &potential_collection), displacement(&mesh, &h1, 3);
const backend::TermBinding<blocks::gravity::potential, Schema> potential_binding{potential};
const backend::TermBinding<blocks::displacement::vector, Schema> displacement_binding{displacement};
CHECK(potential.GetFE(0)->GetOrder() == blocks::gravity::potential::FunctionalSpace::order);
CHECK(displacement.GetFE(0)->GetOrder() == 3);
CHECK(potential.GetVDim() == 1);
CHECK(displacement.GetVDim() == 3);
CHECK(potential_binding.size() == potential.GetVSize());
CHECK(displacement_binding.size() == displacement.GetVSize());
const backend::TermBinding<blocks::barotropic_constant::mass_normalization, Schema> global{mesh};
CHECK(global.size() == 1);
CHECK(global.full_size() == 1);
CHECK_THROWS_AS(global.space(), std::logic_error);
mfem::Vector scalar(1);
scalar[0] = 2.7;
CHECK(global.gather(global.scatter(scalar))[0] == 2.7);
mfem::Vector local;
global.element_values(0, scalar, local);
CHECK(local[0] == 2.7);
global.element_values(1, scalar, local);
CHECK(local[0] == 0.0);
CHECK_THROWS_AS((backend::TermBinding<blocks::density::mass, Schema>{displacement}), std::invalid_argument);
}
TEST_CASE("MeanField stellar boundary selections preserve exact reduced field ordering", "[unit][discretization][meanfield]")
{
serif::tests::deformation::PairedMesh fixture(0, 0.0, 3, 4);
const backend::TermBinding<::serif::tests::equations::Enthalpy, Schema> enthalpy{fixture.scalar_space};
const backend::DofSelection selected = backend::select_boundary_dofs<domain::StellarSurfaceBoundary>(enthalpy);
REQUIRE(selected.size() > 0);
CHECK(selected.size() < enthalpy.size());
CHECK(&selected.binding() == &enthalpy);
mfem::Array<int> marker{1, 0};
mfem::Array<int> full_boundary;
fixture.scalar_space.GetEssentialTrueDofs(marker, full_boundary);
REQUIRE(selected.size() == full_boundary.Size());
for (int index = 0; index < full_boundary.Size(); ++index)
{
const std::optional<int> reduced = enthalpy.reduced_dof(full_boundary[index]);
REQUIRE(reduced);
CHECK(selected.reduced_dofs()[index] == *reduced);
if (index > 0)
{
CHECK(selected.reduced_dofs()[index] > selected.reduced_dofs()[index - 1]);
}
}
CHECK_NOTHROW(selected.validate());
}

View File

@@ -1,12 +1,15 @@
discretization_test_sources = files(
'domain/mesh_topology.cpp',
'field_support_meanfield.cpp',
'nonconforming.cpp',
'reconstruction.cpp',
'domain/concepts.cpp',
# 'domain/ids/lists/lists.cpp',
# 'domain/relation/relations.cpp',
# 'domain/schema/domain_schema.cpp',
# 'domain/schema/schemas.cpp',
# 'domain/schema/utils.cpp',
# 'domain/schema/validation/all.cpp',
# 'domain/schema/validation/boundary.cpp',
# 'domain/schema/validation/connected.cpp',
# 'domain/schema/validation/inscribed.cpp',
'domain/ids/lists/lists.cpp',
'domain/relation/relations.cpp',
'domain/schema/domain_schema.cpp',
'domain/schema/schemas.cpp',
'domain/schema/validation/all.cpp',
'domain/schema/validation/boundary.cpp',
'domain/schema/validation/connected.cpp',
'domain/schema/validation/inscribed.cpp',
)

View File

@@ -0,0 +1,219 @@
#include <algorithm>
#include <chrono>
#include <concepts>
#include <cmath>
#include <filesystem>
#include <memory>
#include <vector>
#include <catch2/catch_test_macros.hpp>
#include "mfem/fem/bilinearform.hpp"
#include "mfem/fem/bilininteg.hpp"
#include "serif/backend/mfem/discretization/element_coordinates.hpp"
#include "serif/backend/mfem/discretization/mass_diagonal.hpp"
#include "serif/backend/mfem/discretization/schema.hpp"
#include "serif/tests/nonconforming.hpp"
#include "serif/discretization/domain/schema/schemas.hpp"
namespace {
namespace backend = serif::discretization::backend::mfem;
namespace blocks = serif::discretization::blocks;
using Schema = serif::discretization::domain::schema::CoreEnvelopeVacuumDomainSchema;
/** @brief Keep each saved STROID fixture private to this invocation and remove it after any assertion. */
struct TemporaryMesh {
std::filesystem::path path = std::filesystem::temp_directory_path() /
("serif-nc-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()) + ".smesh");
/** @brief Release only this test's generated file. */
~TemporaryMesh() {
std::error_code error;
std::filesystem::remove(path, error);
}
};
/** @brief Use nonconstant signed data so primal restriction cannot masquerade as dual accumulation. */
[[nodiscard]] ::mfem::Vector sequence(const int size) {
::mfem::Vector values(size);
for (int index = 0; index < size; ++index) {
values[index] = std::sin(0.17 * (index + 1)) + 0.3 * std::cos(0.23 * index);
}
return values;
}
/** @brief Compare complete vectors with a norm-scaled roundoff tolerance. */
void check_values(const ::mfem::Vector& actual, const ::mfem::Vector& expected) {
REQUIRE(actual.Size() == expected.Size());
::mfem::Vector difference(actual);
difference -= expected;
CHECK(difference.Norml2() <= 2.0e-12 * std::max(1.0, expected.Norml2()));
}
/** @brief Independently close element support through native MFEM prolongation rows. */
[[nodiscard]] std::vector<int> supported_true_dofs(const backend::DofMap& binding) {
const ::mfem::SparseMatrix& prolongation = *binding.space().GetConformingProlongation();
std::vector<bool> supported(prolongation.Width(), false);
::mfem::Array<int> dofs;
for (int element = 0; element < binding.mesh().GetNE(); ++element) {
if (!binding.supports(element)) {
continue;
}
binding.space().GetElementVDofs(element, dofs);
for (const int encoded : dofs) {
const int local = ::mfem::FiniteElementSpace::DecodeDof(encoded);
for (int entry = prolongation.GetI()[local]; entry < prolongation.GetI()[local + 1]; ++entry) {
if (prolongation.GetData()[entry] != 0.0) {
supported[prolongation.GetJ()[entry]] = true;
}
}
}
}
std::vector<int> result;
for (int index = 0; index < prolongation.Width(); ++index) {
if (supported[index]) {
result.push_back(index);
}
}
return result;
}
/** @brief Compare primal, dual, and oriented element coordinates with independent native matrices. */
void check_transfers(const backend::DofMap& binding) {
REQUIRE(binding.constrained());
const auto supported = supported_true_dofs(binding);
REQUIRE(static_cast<int>(supported.size()) == binding.size());
const ::mfem::Vector reduced = sequence(binding.size());
::mfem::Vector independent(binding.true_size());
independent = 0.0;
for (int index = 0; index < binding.size(); ++index) {
independent[supported[index]] = reduced[index];
}
::mfem::Vector expected(binding.full_size());
binding.space().GetConformingProlongation()->Mult(independent, expected);
check_values(binding.scatter(reduced), expected);
check_values(binding.scatter_true(reduced), independent);
check_values(binding.gather(expected), reduced);
const ::mfem::Vector dual = sequence(binding.full_size());
::mfem::Vector true_dual(binding.true_size()), expected_dual(binding.size());
binding.space().GetConformingProlongation()->MultTranspose(dual, true_dual);
for (int index = 0; index < binding.size(); ++index) {
expected_dual[index] = true_dual[supported[index]];
}
check_values(binding.accumulate_dual(dual), expected_dual);
const double full_work = expected * dual;
CHECK(std::abs(reduced * expected_dual - full_work) <= 2.0e-11 * std::max(1.0, std::abs(full_work)));
::mfem::Vector arbitrary_true(binding.true_size()), expected_primal(binding.size());
binding.space().GetConformingRestriction()->Mult(dual, arbitrary_true);
for (int index = 0; index < binding.size(); ++index) {
expected_primal[index] = arbitrary_true[supported[index]];
}
check_values(binding.gather(dual), expected_primal);
const auto elements = binding.validated_elements();
for (int element = 0; element < binding.mesh().GetNE(); ++element) {
if (!binding.supports(element)) {
continue;
}
::mfem::Array<int> dofs;
const auto* transformation = binding.space().GetElementVDofs(element, dofs);
::mfem::Vector native, actual;
expected.GetSubVector(dofs, native);
if (transformation != nullptr) {
transformation->InvTransformPrimal(native);
}
elements.element_values(element, reduced, actual);
check_values(actual, native);
}
}
/** @brief Retain every local mass coupling before reducing; local diagonal restriction is not an oracle. */
template <typename Term>
void check_mass(const backend::TermBinding<Term, Schema>& binding) {
::mfem::Array<int> marker(binding.mesh().attributes.Max());
marker = 0;
for (int element = 0; element < binding.mesh().GetNE(); ++element) {
if (binding.supports(element)) {
marker[binding.mesh().GetAttribute(element) - 1] = 1;
}
}
::mfem::BilinearForm native(&binding.space());
if constexpr (std::same_as<Term, blocks::displacement::vector>) {
native.AddDomainIntegrator(new ::mfem::VectorMassIntegrator(), marker);
} else {
native.AddDomainIntegrator(new ::mfem::MassIntegrator(), marker);
}
native.Assemble();
native.Finalize();
const ::mfem::SparseMatrix& prolongation = *binding.space().GetConformingProlongation();
const std::unique_ptr<::mfem::SparseMatrix> constrained(::mfem::RAP(prolongation, native.SpMat(), prolongation));
::mfem::Vector diagonal;
constrained->GetDiag(diagonal);
const auto supported = supported_true_dofs(binding);
const auto actual = backend::reference_mass_diagonal(binding);
REQUIRE(actual);
REQUIRE(actual->size() == supported.size());
for (int index = 0; index < binding.size(); ++index) {
const double expected = diagonal[supported[index]];
CHECK(std::abs((*actual)[index] - expected) <= 2.0e-11 * expected);
}
}
}
TEST_CASE("STROID nonconforming meshes preserve native coefficient and dual transfers", "[integration][nonconforming]") {
auto generated = serif::tests::nonconforming::mesh(0, 3);
REQUIRE(generated.mesh->Nonconforming());
CHECK(serif::backend::mfem::discretization::validate_schema<Schema>(*generated.mesh).valid());
CHECK(serif::backend::mfem::discretization::validate_schema<Schema>(*generated.reference_mesh).valid());
::mfem::H1_FECollection scalar_collection(blocks::density::mass::FunctionalSpace::order, 3);
::mfem::FiniteElementSpace scalar(generated.mesh.get(), &scalar_collection);
REQUIRE(scalar.GetTrueVSize() < scalar.GetVSize());
backend::TermBinding<blocks::density::mass, Schema> density(scalar);
::mfem::H1_FECollection potential_collection(blocks::gravity::potential::FunctionalSpace::order, 3);
::mfem::FiniteElementSpace potential_space(generated.mesh.get(), &potential_collection);
REQUIRE(potential_space.GetTrueVSize() < potential_space.GetVSize());
backend::TermBinding<blocks::gravity::potential, Schema> potential(potential_space);
CHECK(density.size() < potential.size());
REQUIRE(potential.size() == potential_space.GetTrueVSize());
const ::mfem::SparseMatrix& density_prolongation = *scalar.GetConformingProlongation();
int material_hanging_coefficients = 0;
::mfem::Array<int> material_dofs;
for (int element = 0; element < scalar.GetNE(); ++element) {
if (!density.supports(element)) {
continue;
}
scalar.GetElementDofs(element, material_dofs);
for (const int local : material_dofs) {
const int begin = density_prolongation.GetI()[local];
if (density_prolongation.GetI()[local + 1] != begin + 1 || density_prolongation.GetData()[begin] != 1.0) {
++material_hanging_coefficients;
}
}
}
REQUIRE(material_hanging_coefficients > 0);
check_transfers(density);
check_transfers(potential);
check_mass(density);
check_mass(potential);
for (const auto ordering : {::mfem::Ordering::byNODES, ::mfem::Ordering::byVDIM}) {
::mfem::FiniteElementSpace vector(generated.mesh.get(), &scalar_collection, 3, ordering);
backend::TermBinding<blocks::displacement::vector, Schema> displacement(vector);
check_transfers(displacement);
check_mass(displacement);
}
}
TEST_CASE("STROID saved meshes retain genuine hanging faces when loaded directly", "[integration][nonconforming]") {
auto generated = serif::tests::nonconforming::mesh();
const TemporaryMesh file;
stroid::IO::SaveStroidMesh(generated, file.path.string());
auto restored = stroid::IO::LoadStroidMesh(file.path.string());
INFO((restored ? "restored" : restored.error()));
REQUIRE(restored);
REQUIRE(restored->mesh->Nonconforming());
REQUIRE(restored->reference_mesh->Nonconforming());
CHECK(restored->mesh->GetNE() == generated.mesh->GetNE());
::mfem::H1_FECollection collection(blocks::gravity::potential::FunctionalSpace::order, 3);
::mfem::FiniteElementSpace space(restored->mesh.get(), &collection);
REQUIRE(space.GetTrueVSize() < space.GetVSize());
backend::TermBinding<blocks::gravity::potential, Schema> potential(space);
check_transfers(potential);
CHECK(serif::backend::mfem::discretization::validate_schema<Schema>(*restored->mesh).valid());
}

View File

@@ -0,0 +1,77 @@
#include <array>
#include <cmath>
#include <catch2/catch_approx.hpp>
#include <catch2/catch_test_macros.hpp>
#include "mfem/fem/fe/fe_h1.hpp"
#include "serif/backend/mfem/discretization/basis_evaluation.hpp"
TEST_CASE("Surface power retains nodal density and its analytic surface limit", "[unit][reconstruction]") {
namespace reconstruction = serif::discretization::reconstruction;
const std::array nodes{0.0, 0.2, 0.7, 1.0};
std::array<double, 4> values, slopes;
for (const double exponent : {1.0, 4.0 / 3.0, 5.0 / 3.0, 2.0, 2.8}) {
const reconstruction::SurfacePowerProfile profile(exponent);
for (std::size_t node = 0; node < nodes.size(); ++node) {
profile.evaluate(nodes, nodes[node], values, slopes);
for (std::size_t basis = 0; basis < nodes.size(); ++basis) {
CHECK(values[basis] == Catch::Approx(node == basis ? 1.0 : 0.0).margin(2e-14));
CHECK(std::isfinite(slopes[basis]));
}
}
for (const double point : {0.0, 1e-12, 0.01, 0.35, 0.9, 1.0}) {
profile.evaluate(nodes, point, values, slopes);
double density = 0.0, derivative = 0.0, constant = 0.0, constant_derivative = 0.0;
for (std::size_t basis = 0; basis < nodes.size(); ++basis) {
density += values[basis] * std::pow(nodes[basis], exponent);
derivative += slopes[basis] * std::pow(nodes[basis], exponent);
constant += values[basis];
constant_derivative += slopes[basis];
}
CHECK(density == Catch::Approx(std::pow(point, exponent)).margin(3e-14));
CHECK(derivative == Catch::Approx(exponent * std::pow(point, exponent - 1.0)).margin(3e-13));
CHECK(constant == Catch::Approx(1.0).margin(3e-14));
CHECK(constant_derivative == Catch::Approx(0.0).margin(3e-13));
}
}
CHECK_THROWS(reconstruction::SurfacePowerProfile(0.8));
CHECK_THROWS(reconstruction::SurfacePowerProfile(NAN));
}
TEST_CASE("Reconstructed tensor values gradients and transposes agree in every surface orientation", "[unit][reconstruction]") {
namespace backend = serif::discretization::backend::mfem;
namespace reconstruction = serif::discretization::reconstruction;
::mfem::H1_HexahedronElement element(3);
const auto& rule = ::mfem::IntRules.Get(::mfem::Geometry::CUBE, 12);
backend::TensorBasisCache cache;
backend::BasisWorkspace workspace;
for (const double exponent : {1.0, 4.0 / 3.0, 2.8}) {
const reconstruction::SurfacePowerProfile profile(exponent);
for (int axis = 0; axis < 3; ++axis) {
for (const bool high : {false, true}) {
const backend::ElementReconstruction reconstruction{&profile, axis, high};
for (const auto operation : {backend::ReferenceOperation::value, backend::ReferenceOperation::gradient, backend::ReferenceOperation::value_and_gradient}) {
backend::BasisEvaluation dense, tensor;
dense.sample(element, rule, operation, 1, cache, backend::ContractionMode::dense, &reconstruction);
tensor.sample(element, rule, operation, 1, cache, backend::ContractionMode::automatic, &reconstruction);
REQUIRE(tensor.uses_tensor());
::mfem::Vector coefficients(element.GetDof()), dense_values, tensor_values;
for (int i = 0; i < coefficients.Size(); ++i) { coefficients[i] = std::sin(0.3 * i); }
dense.evaluate(coefficients, dense_values, workspace);
tensor.evaluate(coefficients, tensor_values, workspace);
tensor_values -= dense_values;
CHECK(tensor_values.Normlinf() < 2e-12);
::mfem::Vector dual(dense.height()), dense_transpose, tensor_transpose;
for (int i = 0; i < dual.Size(); ++i) { dual[i] = std::cos(0.13 * i); }
dense.integrate(dual, dense_transpose, workspace);
tensor.integrate(dual, tensor_transpose, workspace);
CHECK((coefficients * tensor_transpose) == Catch::Approx(dense_values * dual).margin(1e-9));
tensor_transpose -= dense_transpose;
CHECK(tensor_transpose.Normlinf() < 2e-10);
}
}
}
// The cache borrows the immutable profile, just as it borrows finite elements.
cache = backend::TensorBasisCache{};
}
}