Files
MeanField_SERiF/tests/deformation/reference_geometry_meanfield.cpp
Emily Boudreaux cb20faeb3d 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.
2026-11-09 13:53:17 -05:00

211 lines
11 KiB
C++

#include <algorithm>
#include <cmath>
#include <memory>
#include <numbers>
#include <vector>
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
#include "mfem/fem/coefficient.hpp"
#include "mfem/fem/gridfunc.hpp"
#include "serif/tests/equations/test_equations.hpp"
#include "serif/backend/mfem/discretization/boundary_trace.hpp"
#include "serif/discretization/blocks/fields.hpp"
#include "serif/backend/mfem/geometry/deformation/inputs.hpp"
#include "serif/backend/mfem/geometry/deformation/reference_geometry.hpp"
#include "serif/backend/mfem/geometry/deformation/surface_metric.hpp"
#include "serif/tests/deformation/mesh.hpp"
namespace
{
namespace deformation = serif::geometry::deformation;
namespace backend = deformation::backend::mfem;
namespace finite_elements = serif::discretization::backend::mfem;
namespace domain = serif::discretization::domain;
using Schema = domain::schema::CoreEnvelopeVacuumDomainSchema;
using Catch::Matchers::WithinAbs;
using Catch::Matchers::WithinRel;
/** @brief Retain the original eight-coordinate indexing example without imposing a higher-order physical field. */
struct LinearEnthalpy : serif::discretization::blocks::term<::serif::tests::equations::enthalpy>
{
using FunctionalSpace = serif::discretization::functional_space::DiscreteFunctionalSpace<serif::discretization::functional_space::H1, 1, 0>;
using Support = domain::StellarDomains;
using Quantity = serif::dimensions::SpecificEnthalpy;
struct value : serif::discretization::blocks::value_block_base
{
};
struct residual : serif::discretization::blocks::residual_block_base
{
};
};
using AlternateBoundarySchema = domain::schema::DomainSchema<typename Schema::domain_ids_type, domain::ids::lists::BoundaryIDList<domain::ids::BoundaryID<domain::StellarSurfaceBoundary, 2>, domain::ids::BoundaryID<domain::InfinitySurfaceBoundary, 1>>, domain::relation::lists::RelationList<>>;
/** @brief Resolve boundary coordinates independently using MFEM's serial essential true-DOF list. */
[[nodiscard]] std::vector<int> essential_dofs(const mfem::FiniteElementSpace& space, const int attribute)
{
mfem::Array<int> marker(space.GetMesh()->bdr_attributes.Max()), selected;
marker = 0;
marker[attribute - 1] = 1;
space.GetEssentialTrueDofs(marker, selected);
return std::vector<int>(selected.begin(), selected.end());
}
} // namespace
TEST_CASE("MeanField canonical scalar coordinates retain exact gather and transpose scatter indexing", "[unit][deformation][meanfield]")
{
mfem::Mesh mesh = mfem::Mesh::MakeCartesian3D(1, 1, 1, mfem::Element::HEXAHEDRON);
mfem::H1_FECollection collection(1, 3);
mfem::FiniteElementSpace space(&mesh, &collection);
finite_elements::TermBinding<LinearEnthalpy, Schema> binding(space);
REQUIRE(binding.size() == 8);
const mfem::Array<int> selected{1, 3, 6};
const auto rows = std::make_shared<const finite_elements::DofSelection>(binding, selected);
const auto trace = finite_elements::make_boundary_trace(rows, binding);
mfem::Vector full(binding.size()), sampled(rows->size()), scattered(binding.size());
for (int dof = 0; dof < full.Size(); ++dof)
{
full[dof] = 10.0 + dof;
}
trace.interpolation().Mult(full, sampled);
CHECK(sampled[0] == 11.0);
CHECK(sampled[1] == 13.0);
CHECK(sampled[2] == 16.0);
trace.interpolation().MultTranspose(sampled, scattered);
for (int dof = 0; dof < scattered.Size(); ++dof)
{
CHECK(scattered[dof] == (dof == 1 || dof == 3 || dof == 6 ? full[dof] : 0.0));
}
CHECK_THROWS_AS((finite_elements::DofSelection{binding, mfem::Array<int>{3, 1}}), std::invalid_argument);
CHECK_THROWS_AS((finite_elements::DofSelection{binding, mfem::Array<int>{1, 1}}), std::invalid_argument);
CHECK_THROWS_AS((finite_elements::DofSelection{binding, mfem::Array<int>{-1}}), std::invalid_argument);
CHECK_THROWS_AS((finite_elements::DofSelection{binding, mfem::Array<int>{8}}), std::invalid_argument);
}
TEST_CASE("MeanField surface coordinates resolve semantic boundary IDs and scalar companion nodes", "[unit][deformation][meanfield]")
{
serif::tests::deformation::PairedMesh fixture(0, 0.1, 3, 4);
const auto primary = backend::deformation_discretization<Schema>(fixture.scalar_space, fixture.vector_space, *fixture.generated.reference_mesh);
const auto alternate = backend::deformation_discretization<AlternateBoundarySchema>(fixture.scalar_space, fixture.vector_space, *fixture.generated.reference_mesh);
const auto stellar = backend::sample_reference_surface(fixture.scalar_space, primary.surface_attribute);
const auto infinity = backend::select_surface_scalar_dofs(fixture.scalar_space, alternate.surface_attribute);
REQUIRE(stellar);
REQUIRE(infinity);
CHECK(stellar->scalar_dofs == essential_dofs(fixture.scalar_space, 1));
CHECK(*infinity == essential_dofs(fixture.scalar_space, 2));
CHECK(stellar->scalar_dofs != *infinity);
CHECK(std::is_sorted(stellar->scalar_dofs.begin(), stellar->scalar_dofs.end()));
REQUIRE(stellar->scalar_dofs.size() > 0);
CHECK(stellar->scalar_dofs.size() < static_cast<std::size_t>(fixture.scalar_space.GetTrueVSize()));
const auto repeated = backend::select_surface_scalar_dofs(fixture.scalar_space, primary.surface_attribute);
REQUIRE(repeated);
CHECK(*repeated == stellar->scalar_dofs);
for (const mfem::Ordering::Type ordering : {mfem::Ordering::byNODES, mfem::Ordering::byVDIM})
{
mfem::FiniteElementSpace vector_space(fixture.generated.mesh.get(), &fixture.collection, 3, ordering);
const std::vector<int> vector_dofs = essential_dofs(vector_space, primary.surface_attribute);
CHECK(vector_dofs.size() == 3 * stellar->scalar_dofs.size());
mfem::VectorFunctionCoefficient coordinates(3, [](const mfem::Vector& position, mfem::Vector& value)
{
value = position;
});
mfem::GridFunction projected(&vector_space);
projected.ProjectCoefficient(coordinates);
for (std::size_t node = 0; node < stellar->positions.size(); ++node)
{
for (int component = 0; component < 3; ++component)
{
CHECK_THAT(stellar->positions[node][component], WithinAbs(projected[vector_space.DofToVDof(stellar->scalar_dofs[node], component)], 2.0e-13));
}
}
}
}
TEST_CASE("MeanField stellar surface metrics include tagged internal interfaces", "[unit][deformation][meanfield]")
{
serif::tests::deformation::PairedMesh fixture;
int selected_faces = 0;
for (int boundary = 0; boundary < fixture.generated.mesh->GetNBE(); ++boundary)
{
if (fixture.generated.mesh->GetBdrAttribute(boundary) == fixture.surface_attribute)
{
const int face = fixture.generated.mesh->GetBdrElementFaceIndex(boundary);
REQUIRE(face >= 0);
CHECK(fixture.generated.mesh->GetFaceInformation(face).IsOfFaceType(mfem::FaceType::Interior));
++selected_faces;
}
}
REQUIRE(selected_faces > 0);
const auto reference = backend::sample_reference_surface(fixture.scalar_space, fixture.surface_attribute);
REQUIRE(reference);
const auto embedding = deformation::build_surface_embedding(deformation::NodalRadialSurface{}, reference->positions);
REQUIRE(embedding);
const auto metric = backend::surface_parameter_mass_diagonal(fixture.scalar_space, fixture.surface_attribute, *reference, *embedding);
REQUIRE(metric);
REQUIRE(metric->size() == reference->scalar_dofs.size());
for (const double diagonal : *metric)
{
CHECK(std::isfinite(diagonal));
CHECK(diagonal > 0.0);
}
}
TEST_CASE("MeanField STROID spherical volume retains its original mesh accuracy", "[integration][deformation][geometry][meanfield]")
{
// Match sandbox.smesh: two refinements, quartic geometry, stellar radius 1 and reference infinity 5.
auto generated = serif::tests::deformation::make_mesh(2, 0.0, 4, 5.0);
long double volume = 0.0L;
int stellar_elements = 0;
for (int element = 0; element < generated.mesh->GetNE(); ++element)
{
if (!Schema::domain_id_belongs_to<domain::StellarDomains>(generated.mesh->GetAttribute(element)))
{
continue;
}
mfem::ElementTransformation& transformation = *generated.mesh->GetElementTransformation(element);
const mfem::IntegrationRule& rule = mfem::IntRules.Get(transformation.GetGeometryType(), transformation.OrderW());
for (int point = 0; point < rule.GetNPoints(); ++point)
{
const mfem::IntegrationPoint& sample = rule.IntPoint(point);
transformation.SetIntPoint(&sample);
volume += sample.weight * transformation.Weight();
}
++stellar_elements;
}
REQUIRE(stellar_elements > 0);
CHECK_THAT(static_cast<double>(volume), WithinRel(4.0 * std::numbers::pi / 3.0, 1.0e-6));
}
TEST_CASE("Shared MFEM reference tracking distinguishes coordinate and label dependencies", "[unit][deformation][reference_tracking]") {
namespace shared = serif::backend::mfem::shared;
mfem::Mesh mesh = mfem::Mesh::MakeCartesian3D(1, 1, 1, mfem::Element::HEXAHEDRON);
mfem::H1_FECollection collection(1, 3);
mfem::FiniteElementSpace space(&mesh, &collection);
const auto topology = shared::mesh_snapshot(&mesh, false, false);
const auto materials = shared::mesh_snapshot(&mesh, false, true);
const auto coordinates = shared::mesh_snapshot(&mesh, true, true);
const auto original_space = shared::space_snapshot(space);
REQUIRE(shared::mesh_matches(topology, &mesh));
REQUIRE(shared::mesh_matches(coordinates, &mesh));
mesh.Transform([](const mfem::Vector& position, mfem::Vector& transformed) {
transformed = position;
transformed[0] *= 1.2;
});
CHECK(shared::mesh_matches(topology, &mesh));
CHECK(shared::mesh_matches(materials, &mesh));
CHECK_FALSE(shared::mesh_matches(coordinates, &mesh));
CHECK(shared::space_snapshot(space) == original_space);
mesh.GetElement(0)->SetAttribute(9);
CHECK(shared::mesh_matches(topology, &mesh));
CHECK_FALSE(shared::mesh_matches(materials, &mesh));
mesh.GetBdrElement(0)->SetAttribute(9);
CHECK_FALSE(shared::mesh_matches(topology, &mesh));
mfem::FiniteElementSpace other(&mesh, &collection);
CHECK_FALSE(shared::space_snapshot(other) == original_space);
CHECK_FALSE(shared::mesh_matches(topology, nullptr));
CHECK(shared::mesh_matches(shared::mesh_snapshot(nullptr, true, true), nullptr));
}