Files
MeanField_SERiF/tests/equations/pressure_boundary.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

160 lines
7.7 KiB
C++

#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);
}