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:
159
tests/equations/pressure_boundary.cpp
Normal file
159
tests/equations/pressure_boundary.cpp
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user