Files
MeanField_SERiF/tests/normalization/volume_computations.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

164 lines
8.1 KiB
C++

#include <algorithm>
#include <cmath>
#include <limits>
#include <tuple>
#include <vector>
#include <catch2/catch_approx.hpp>
#include <catch2/catch_test_macros.hpp>
#include "mfem/fem/coefficient.hpp"
#include "serif/discretization/domain/schema/schemas.hpp"
#include "serif/backend/mfem/solver/normalization/metric_computations.hpp"
#include "serif/solver/normalization/normalization.hpp"
#include "serif/utils/dependencies/computation_cache.hpp"
namespace
{
namespace normalization = serif::solver::normalization;
namespace backend = normalization::backend::mfem;
namespace dependencies = serif::utils::dependencies;
namespace dimensions = serif::dimensions;
namespace discretization = serif::discretization;
using Density = discretization::blocks::density::mass;
using Schema = discretization::domain::schema::CoreEnvelopeVacuumDomainSchema;
using Binding = discretization::backend::mfem::TermBinding<Density, Schema>;
using Metric = normalization::ReferenceVolumeMetric<Density>;
using ValueRule = normalization::PhysicalValueRule<Density>;
using Plan = normalization::NormalizationPlan<dependencies::Bindings<ValueRule>, dependencies::Bindings<dependencies::Binding<Density, ValueRule::Target>>>;
using Layout = normalization::CoordinateLayout<dependencies::KeyList<Density>, dependencies::KeyList<Density>>;
using Status = dependencies::PreparationStatus;
/** @brief Exercise linear geometry that gains MFEM nodes during partial assembly as well as existing nodal geometry. */
[[nodiscard]] mfem::Mesh make_reference_mesh(const bool nodal, const double length)
{
mfem::Mesh mesh = mfem::Mesh::MakeCartesian3D(2, 1, 1, mfem::Element::HEXAHEDRON, 2.0 * length, length, length);
if (nodal)
{
mesh.SetCurvature(2);
}
return mesh;
}
/** @brief Keep reference geometry, binding identity, and candidate coefficients independently observable. */
struct VolumeFixture
{
mfem::Mesh mesh;
mfem::H1_FECollection collection{Density::FunctionalSpace::order, 3};
mfem::FiniteElementSpace space{&mesh, &collection};
Binding binding{space};
Layout layout{.state_sizes = {static_cast<std::size_t>(binding.size())}, .residual_sizes = {static_cast<std::size_t>(binding.size())}};
normalization::StellarReferenceScales scales = *normalization::prepare_stellar_reference_scales(dimensions::QuantityValue<dimensions::Mass>{4.0}, dimensions::QuantityValue<dimensions::Length>{3.0});
std::vector<double> candidate = std::vector<double>(binding.size(), 1.0);
/** @brief Construct equal layouts on optionally different reference geometries. */
explicit VolumeFixture(const bool nodal, const double length = 1.0) : mesh(make_reference_mesh(nodal, length))
{
}
/** @brief Allow another live binding to replace the reference without changing context or layout types. */
[[nodiscard]] auto inputs(Binding& reference)
{
return dependencies::make_dependency_context(
backend::reference_metric_binding(reference),
dependencies::borrow<normalization::keys::Layout>(layout),
dependencies::borrow<normalization::keys::Scales>(scales),
dependencies::borrow<Density>(candidate));
}
};
/** @brief Compose the shared backend metric with both normalization coordinate roles. */
[[nodiscard]] auto make_cache(VolumeFixture& fixture)
{
auto computations = std::tuple_cat(std::tuple{backend::make_reference_volume_metric_computation<Density, Schema>()}, normalization::make_normalization_computations<Plan>(fixture.layout));
auto graph = std::apply([](auto... nodes)
{
return dependencies::make_computation_graph(std::move(nodes)...);
}, std::move(computations));
return dependencies::make_computation_cache<std::string>(std::move(graph), fixture.inputs(fixture.binding));
}
/** @brief Check exact scale cancellation independently of the graph's factor construction. */
void check_factors(const normalization::DiagonalNormalization& factors, const std::vector<double>& metric, const double scale)
{
REQUIRE(factors.state_size() == metric.size());
REQUIRE(factors.residual_size() == metric.size());
for (std::size_t index = 0; index < metric.size(); ++index)
{
CHECK(factors.state_factors()[index] == Catch::Approx(std::sqrt(metric[index]) / scale).epsilon(3.0e-14));
CHECK(factors.residual_factors()[index] == Catch::Approx(1.0 / (scale * std::sqrt(metric[index]))).epsilon(3.0e-14));
}
}
}
TEST_CASE("Reference volume normalization reuses one metric independently of trial coefficients", "[integration][normalization]")
{
for (const bool nodal : {false, true})
{
CAPTURE(nodal);
VolumeFixture fixture(nodal);
CHECK((fixture.mesh.GetNodes() != nullptr) == nodal);
auto cache = make_cache(fixture);
const auto prepared = cache.try_prepare(fixture.inputs(fixture.binding));
REQUIRE(prepared);
CHECK(prepared->computed_count() == 4);
const std::vector<double> original_metric = cache.get<Metric>();
check_factors(cache.get<normalization::keys::Normalization>(), original_metric, fixture.scales.density.value());
const auto unchanged = cache.try_prepare(fixture.inputs(fixture.binding));
REQUIRE(unchanged);
CHECK(unchanged->computed_count() == 0);
CHECK(unchanged->status<Metric>() == Status::reused);
std::ranges::fill(fixture.candidate, std::numeric_limits<double>::quiet_NaN());
const auto changed_candidate = cache.try_prepare(fixture.inputs(fixture.binding));
REQUIRE(changed_candidate);
CHECK(changed_candidate->computed_count() == 0);
CHECK(cache.get<Metric>() == original_metric);
}
}
TEST_CASE("Reference volume normalization follows geometry revisions and exact binding identity", "[integration][normalization]")
{
for (const bool nodal : {false, true})
{
CAPTURE(nodal);
VolumeFixture fixture(nodal);
auto cache = make_cache(fixture);
REQUIRE(cache.try_prepare(fixture.inputs(fixture.binding)));
const normalization::DiagonalNormalization frozen = cache.get<normalization::keys::Normalization>();
const std::vector<double> original_metric = cache.get<Metric>();
const std::vector<double> original_factors(frozen.state_factors().begin(), frozen.state_factors().end());
mfem::VectorFunctionCoefficient dilation(3, [](const mfem::Vector& point, mfem::Vector& image)
{
image = point;
image *= 2.0;
});
fixture.mesh.Transform(dilation);
const auto changed_geometry = cache.try_prepare(fixture.inputs(fixture.binding));
REQUIRE(changed_geometry);
CHECK(changed_geometry->status<Metric>() == Status::computed);
const std::vector<double> dilated_metric = cache.get<Metric>();
for (std::size_t index = 0; index < original_metric.size(); ++index)
{
CHECK(dilated_metric[index] == Catch::Approx(8.0 * original_metric[index]).epsilon(2.0e-12));
}
check_factors(cache.get<normalization::keys::Normalization>(), dilated_metric, fixture.scales.density.value());
CHECK(std::ranges::equal(frozen.state_factors(), original_factors));
VolumeFixture replacement(nodal, 3.0);
REQUIRE(replacement.binding.size() == fixture.binding.size());
const auto changed_binding = cache.try_prepare(fixture.inputs(replacement.binding));
REQUIRE(changed_binding);
CHECK(changed_binding->status<Metric>() == Status::computed);
const std::vector<double> replaced_metric = cache.get<Metric>();
for (std::size_t index = 0; index < original_metric.size(); ++index)
{
CHECK(replaced_metric[index] == Catch::Approx(27.0 * original_metric[index]).epsilon(2.0e-12));
}
const auto replacement_unchanged = cache.try_prepare(fixture.inputs(replacement.binding));
REQUIRE(replacement_unchanged);
CHECK(replacement_unchanged->computed_count() == 0);
}
}