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

94 lines
5.4 KiB
C++

#include <cmath>
#include <numbers>
#include <catch2/catch_test_macros.hpp>
#include "serif/tests/deformation/mesh.hpp"
#include "serif/backend/mfem/model/surface_potential.hpp"
#include "serif/backend/mfem/discretization/term_binding.hpp"
#include "serif/backend/mfem/geometry/deformation/reference_geometry.hpp"
namespace {
namespace deformation = serif::geometry::deformation;
namespace backend = serif::model::backend::mfem;
using Potential = serif::discretization::blocks::gravity::potential;
/** @brief Compare owned surface matrices with a coupled finite difference of the physical moments. */
void check_surface(const int surface_order, const bool nodal) {
serif::tests::deformation::PairedMesh mesh(0, 0.1, surface_order);
::mfem::H1_FECollection potential_collection(Potential::FunctionalSpace::order, 3);
::mfem::FiniteElementSpace potential_space(mesh.generated.mesh.get(), &potential_collection);
serif::discretization::backend::mfem::TermBinding<Potential, serif::tests::deformation::Schema> potential_binding(potential_space);
const auto reference = deformation::backend::mfem::sample_reference_surface(mesh.scalar_space, mesh.surface_attribute);
REQUIRE(reference.has_value());
const auto embedding = [&] {
if (nodal) {
return deformation::build_surface_embedding(deformation::NodalRadialSurface{}, reference->positions);
}
return deformation::build_surface_embedding(deformation::SphericalHarmonicSurface{.maximum_degree = 1}, reference->positions);
}();
REQUIRE(embedding.has_value());
const deformation::backend::mfem::SurfaceDiscretization discretization{
.scalar_space = mesh.scalar_space, .surface_attribute = mesh.surface_attribute};
const serif::equations::SurfacePotentialBalance equation{{1.0, 2.0, 3.0}, {0.1, -0.2, 0.3}};
std::vector<double> shape(embedding->input_size()), displacement(embedding->output_size());
::mfem::Vector potential(potential_binding.size()), potential_direction(potential.Size()), shape_direction(static_cast<int>(shape.size()));
for (int index = 0; index < potential.Size(); ++index) {
potential[index] = -1.0 + 0.02 * std::sin(index + 0.3);
potential_direction[index] = std::cos(index + 0.2);
}
for (int index = 0; index < shape_direction.Size(); ++index) {
shape[index] = 0.01 * std::sin(index + 0.7);
shape_direction[index] = 0.03 * std::cos(index + 0.4);
}
REQUIRE(embedding->apply(shape, displacement).has_value());
const auto accepted = backend::assemble_surface_potential(equation, discretization, *reference, *embedding,
displacement, potential_binding, potential, -1.2, 0.4);
::mfem::Vector action(shape_direction.Size()), temporary(action.Size());
accepted.potential->Mult(potential_direction, action);
accepted.shape->Mult(shape_direction, temporary);
action += temporary;
for (int row = 0; row < action.Size(); ++row) {
action[row] += 0.2 * (*accepted.level)(row, 0) - 0.3 * (*accepted.angular_velocity)(row, 0);
}
constexpr double step = 2.0e-6;
auto plus_potential = potential, minus_potential = potential;
plus_potential.Add(step, potential_direction);
minus_potential.Add(-step, potential_direction);
auto plus_shape = shape, minus_shape = shape;
for (int index = 0; index < shape_direction.Size(); ++index) {
plus_shape[index] += step * shape_direction[index];
minus_shape[index] -= step * shape_direction[index];
}
REQUIRE(embedding->apply(plus_shape, displacement).has_value());
const auto plus = backend::assemble_surface_potential(equation, discretization, *reference, *embedding,
displacement, potential_binding, plus_potential, -1.2 + step * 0.2, 0.4 - step * 0.3);
REQUIRE(embedding->apply(minus_shape, displacement).has_value());
const auto minus = backend::assemble_surface_potential(equation, discretization, *reference, *embedding,
displacement, potential_binding, minus_potential, -1.2 - step * 0.2, 0.4 + step * 0.3);
for (int row = 0; row < action.Size(); ++row) {
CHECK(std::abs(action[row] - (plus.residual[row] - minus.residual[row]) / (2.0 * step)) < 2.0e-9);
}
// Constant potential and zero rotation give a known normalized moment, independent of surface area.
potential = -0.8;
const auto constant = backend::assemble_surface_potential(equation, discretization, *reference, *embedding,
displacement, potential_binding, potential, -1.2, 0.0);
double moment = 0.0;
if (nodal) {
for (const double value : constant.residual) { moment += value; }
CHECK(std::abs(moment - 0.4) < 3.0e-12);
} else {
CHECK(std::abs(constant.residual.front() - 0.4 / std::sqrt(4.0 * std::numbers::pi)) < 3.0e-12);
}
constant.shape->Mult(shape_direction, temporary);
CHECK(temporary.Norml2() == 0.0);
}
}
TEST_CASE("Isobar potential moments preserve constant mode and differentiate pullback geometry", "[model][surface_potential][unit]") {
for (const int surface_order : {2, 3}) {
check_surface(surface_order, false);
check_surface(surface_order, true);
}
}