Files
MeanField_SERiF/tests/deformation/domain_actions_meanfield.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

116 lines
6.2 KiB
C++

#include <algorithm>
#include <cmath>
#include <memory>
#include <numeric>
#include <span>
#include <vector>
#include <catch2/catch_test_macros.hpp>
#include "serif/backend/mfem/geometry/deformation/reference_geometry.hpp"
#include "serif/geometry/deformation/composition.hpp"
#include "serif/geometry/deformation/linearization.hpp"
#include "serif/geometry/deformation/surface_embedding.hpp"
#include "serif/tests/deformation/mesh.hpp"
namespace
{
namespace deformation = serif::geometry::deformation;
namespace backend = deformation::backend::mfem;
using DomainMap = deformation::ComposedDeformation<deformation::SurfaceEmbedding, deformation::RadialExtension>;
/** @brief Pair complete surface and volume actions on the same serial STROID geometry. */
template <typename Model>
[[nodiscard]] DomainMap make_domain_map(serif::tests::deformation::PairedMesh& mesh, const Model& model)
{
const auto reference = backend::sample_reference_surface(mesh.scalar_space, mesh.surface_attribute);
REQUIRE(reference);
auto surface = deformation::build_surface_embedding(model, reference->positions);
auto interpolation = mesh.interpolation();
REQUIRE(surface);
REQUIRE(interpolation);
auto extension = deformation::make_radial_extension(std::make_shared<const deformation::RadialInterpolation>(std::move(*interpolation)));
REQUIRE(extension);
auto result = deformation::compose_deformation(std::make_shared<const deformation::SurfaceEmbedding>(std::move(*surface)), std::make_shared<const deformation::RadialExtension>(std::move(*extension)));
REQUIRE(result);
return std::move(*result);
}
/** @brief Compare the assembled physical map and complete transpose with the original MeanField tolerances. */
void check_complete_actions(const DomainMap& map, const std::vector<double>& state, const std::vector<double>& direction, const double step, const double difference_tolerance, const double work_tolerance)
{
std::vector<double> plus(state), minus(state);
for (std::size_t parameter = 0; parameter < state.size(); ++parameter)
{
plus[parameter] += step * direction[parameter];
minus[parameter] -= step * direction[parameter];
}
const auto plus_value = deformation::evaluate_displacement(map, plus);
const auto minus_value = deformation::evaluate_displacement(map, minus);
REQUIRE(plus_value);
REQUIRE(minus_value);
auto workspace = deformation::make_deformation_workspace(map);
std::vector<double> action(map.output_size()), dual(map.output_size()), force(map.input_size()), curvature(map.input_size(), 7.0);
REQUIRE(deformation::apply_jacobian(map, state, direction, action, workspace));
long double error_squared = 0.0L;
long double reference_squared = 0.0L;
for (std::size_t row = 0; row < action.size(); ++row)
{
const double difference = ((*plus_value)[row] - (*minus_value)[row]) / (2.0 * step);
error_squared += static_cast<long double>(action[row] - difference) * (action[row] - difference);
reference_squared += static_cast<long double>(difference) * difference;
const double index = static_cast<double>(row + 1);
dual[row] = std::sin(0.07 * index) + 0.4 * std::cos(0.11 * index);
}
REQUIRE(reference_squared > 0.0L);
CHECK(std::sqrt(error_squared / reference_squared) < difference_tolerance);
REQUIRE(deformation::apply_jacobian_transpose(map, state, dual, force, workspace));
const double volume_work = std::inner_product(action.begin(), action.end(), dual.begin(), 0.0);
const double parameter_work = std::inner_product(direction.begin(), direction.end(), force.begin(), 0.0);
CHECK(std::abs(volume_work - parameter_work) <= work_tolerance * std::max({1.0, std::abs(volume_work), std::abs(parameter_work)}));
REQUIRE(deformation::apply_pullback_derivative(map, state, direction, dual, curvature, workspace));
CHECK(std::ranges::all_of(curvature, [](const double value)
{
return value == 0.0;
}));
std::vector<double> plus_force(map.input_size()), minus_force(map.input_size());
REQUIRE(deformation::apply_jacobian_transpose(map, plus, dual, plus_force, workspace));
REQUIRE(deformation::apply_jacobian_transpose(map, minus, dual, minus_force, workspace));
CHECK(plus_force == minus_force);
}
} // namespace
TEST_CASE("MeanField nodal domain directions preserve complete finite differences and virtual work", "[unit][deformation][meanfield]")
{
// Preserve the original cubic displacement space, including interpolation between refined surface rays.
for (const int refinement : {0, 1})
{
CAPTURE(refinement);
serif::tests::deformation::PairedMesh mesh(refinement, 0.0, 3, 4, 5.0);
const DomainMap map = make_domain_map(mesh, deformation::NodalRadialSurface{});
std::vector<double> state(map.input_size()), direction(map.input_size());
for (std::size_t parameter = 0; parameter < state.size(); ++parameter)
{
const double index = static_cast<double>(parameter + 1);
state[parameter] = 0.008 * std::sin(0.13 * index);
direction[parameter] = std::cos(0.17 * index) - 0.25 * std::sin(0.29 * index);
}
check_complete_actions(map, state, direction, 1.0e-6, 3.0e-10, 8.0e-13);
}
}
TEST_CASE("MeanField harmonic domain directions preserve complete finite differences and dual work", "[unit][deformation][meanfield]")
{
serif::tests::deformation::PairedMesh mesh(0, 0.0, 3, 4, 5.0);
const DomainMap map = make_domain_map(mesh, deformation::SphericalHarmonicSurface{.maximum_degree = 2});
REQUIRE(map.input_size() == 9);
std::vector<double> state(map.input_size()), direction(map.input_size());
for (std::size_t parameter = 0; parameter < state.size(); ++parameter)
{
const double value = std::sin(0.37 * static_cast<double>(parameter + 1));
state[parameter] = 0.001 * value;
direction[parameter] = 0.004 * value;
}
check_complete_actions(map, state, direction, 1.0e-5, 2.0e-9, 1.0e-11);
}