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

View File

@@ -0,0 +1,159 @@
#include <algorithm>
#include <cmath>
#include <limits>
#include <memory>
#include <tuple>
#include <variant>
#include <vector>
#include <catch2/catch_test_macros.hpp>
#include "serif/backend/mfem/discretization/boundary_trace.hpp"
#include "serif/backend/mfem/geometry/surface/pressure_boundary.hpp"
#include "serif/tests/equations/gravity_fixture.hpp"
#include "serif/tests/equations/volume_equation_fixture.hpp"
#include "serif/tests/surface/boundary_fixture.hpp"
namespace {
namespace test = serif::tests::equations;
namespace gravity = test::gravity;
namespace blocks = serif::discretization::blocks;
namespace physics = serif::geometry::surface;
namespace surface = physics::backend::mfem;
namespace volume = serif::operators::backend::mfem;
namespace dependencies = serif::utils::dependencies;
using Vector = mfem::Vector;
using Enthalpy = ::serif::tests::equations::Enthalpy;
/** @brief Identify boundary coefficients that depend on several native independent coordinates. */
[[nodiscard]] std::vector<int> hanging_boundary_dofs(const mfem::FiniteElementSpace& space, const mfem::Array<int>& attributes) {
const mfem::SparseMatrix* prolongation = space.GetConformingProlongation();
REQUIRE(prolongation != nullptr);
mfem::Array<int> boundary;
space.GetEssentialVDofs(attributes, boundary);
std::vector<int> hanging;
for (int local = 0; local < boundary.Size(); ++local) {
if (boundary[local] == 0) {
continue;
}
int contributors = 0;
for (int entry = prolongation->GetI()[local]; entry < prolongation->GetI()[local + 1]; ++entry) {
contributors += prolongation->GetData()[entry] != 0.0;
}
if (contributors > 1) {
hanging.push_back(local);
}
}
return hanging;
}
/** @brief Evaluate an MFEM field at native restriction representatives, without backend trace or reduction helpers. */
[[nodiscard]] Vector native_boundary_samples(const mfem::FiniteElementSpace& row_space,
const mfem::Array<int>& independent_rows, const mfem::GridFunction& field) {
const mfem::SparseMatrix* restriction = row_space.GetConformingRestriction();
REQUIRE(restriction != nullptr);
std::vector<int> selected(row_space.GetVSize(), -1);
for (int row = 0; row < independent_rows.Size(); ++row) {
const int independent = independent_rows[row];
const int entry = restriction->GetI()[independent];
REQUIRE(restriction->GetI()[independent + 1] == entry + 1);
REQUIRE(restriction->GetData()[entry] == 1.0);
selected[restriction->GetJ()[entry]] = row;
}
Vector samples(independent_rows.Size());
samples = std::numeric_limits<double>::quiet_NaN();
mfem::Array<int> dofs;
for (int element = 0; element < row_space.GetNE(); ++element) {
row_space.GetElementDofs(element, dofs);
const mfem::IntegrationRule& nodes = row_space.GetFE(element)->GetNodes();
for (int local = 0; local < dofs.Size(); ++local) {
const int row = selected[mfem::FiniteElementSpace::DecodeDof(dofs[local])];
if (row < 0 || std::isfinite(samples[row])) {
continue;
}
samples[row] = field.GetValue(element, nodes.IntPoint(local));
}
}
for (int row = 0; row < samples.Size(); ++row) {
REQUIRE(std::isfinite(samples[row]));
}
return samples;
}
} // namespace
TEST_CASE("Nonconforming boundary interpolation retains weighted traces across scalar orders", "[unit][equations][pressure_boundary][nonconforming]") {
namespace finite_elements = serif::discretization::backend::mfem;
namespace boundary = serif::tests::surface::boundary;
using StellarSurface = serif::discretization::domain::StellarSurfaceBoundary;
mfem::Mesh mesh = boundary::make_boundary_mesh();
mesh.EnsureNCMesh();
const mfem::Array<int> refinement{0};
mesh.GeneralRefinement(refinement);
mfem::H1_FECollection cubic_collection(3, 3), quadratic_collection(2, 3);
mfem::FiniteElementSpace row_space(&mesh, &cubic_collection), input_space(&mesh, &quadratic_collection);
finite_elements::TermBinding<Enthalpy, boundary::Schema> row_binding(row_space);
finite_elements::TermBinding<boundary::QuadraticComposition, boundary::Schema> input_binding(input_space);
REQUIRE(row_space.GetTrueVSize() < row_space.GetVSize());
REQUIRE(input_space.GetTrueVSize() < input_space.GetVSize());
// Every cell is material, so supported coordinates have the native true ordering.
REQUIRE(row_binding.size() == row_space.GetTrueVSize());
REQUIRE(input_binding.size() == input_space.GetTrueVSize());
const mfem::Array<int> attributes{1, 0};
REQUIRE_FALSE(hanging_boundary_dofs(row_space, attributes).empty());
const std::vector<int> input_hanging = hanging_boundary_dofs(input_space, attributes);
REQUIRE_FALSE(input_hanging.empty());
const auto selected = std::make_shared<const finite_elements::DofSelection>(
finite_elements::select_boundary_dofs<StellarSurface>(row_binding));
mfem::Array<int> native_selected;
row_space.GetEssentialTrueDofs(attributes, native_selected);
REQUIRE(selected->size() == native_selected.Size());
for (int row = 0; row < selected->size(); ++row) {
CHECK(selected->reduced_dofs()[row] == native_selected[row]);
}
const auto trace = finite_elements::make_boundary_trace(selected, input_binding);
const Vector values = boundary::sequence(input_binding.size(), 0.8, 0.37);
mfem::GridFunction field(&input_space);
field.SetFromTrueDofs(values);
const Vector expected = native_boundary_samples(row_space, native_selected, field);
Vector actual(selected->size());
trace.interpolation().Mult(values, actual);
CHECK(test::relative_error(actual, expected) < 2.0e-12);
// This deliberately broken local copy proves that weighted boundary inputs affect the tested rows.
mfem::GridFunction missing_hanging_values(&input_space);
missing_hanging_values = field;
for (const int local : input_hanging) {
missing_hanging_values[local] = 0.0;
}
Vector omitted = native_boundary_samples(row_space, native_selected, missing_hanging_values);
omitted -= expected;
REQUIRE(omitted.Norml2() > 1.0e-3);
const Vector dual = boundary::sequence(selected->size(), -0.2, 0.81);
Vector transpose(input_binding.size());
trace.interpolation().MultTranspose(dual, transpose);
CHECK(std::abs(values * transpose - actual * dual) < 2.0e-12 * std::max(1.0, std::abs(actual * dual)));
Vector zero_trace(row_binding.size());
zero_trace = 1.0;
for (const int row : selected->reduced_dofs()) {
zero_trace[row] = 0.0;
}
mfem::GridFunction constrained(&row_space);
constrained.SetFromTrueDofs(zero_trace);
double unselected_boundary_maximum = 0.0;
for (int face = 0; face < mesh.GetNBE(); ++face) {
mfem::ElementTransformation& transformation = *mesh.GetBdrElementTransformation(face);
const mfem::IntegrationRule& rule = mfem::IntRules.Get(transformation.GetGeometryType(), 8);
for (int sample = 0; sample < rule.GetNPoints(); ++sample) {
const double value = constrained.GetValue(transformation, rule.IntPoint(sample));
if (mesh.GetBdrAttribute(face) == 1) {
CHECK(std::abs(value) < 2.0e-13);
} else {
unselected_boundary_maximum = std::max(unselected_boundary_maximum, std::abs(value));
}
}
}
CHECK(unselected_boundary_maximum > 0.5);
}