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.
94 lines
5.4 KiB
C++
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);
|
|
}
|
|
}
|