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:
175
tests/model/move_only.cpp
Normal file
175
tests/model/move_only.cpp
Normal file
@@ -0,0 +1,175 @@
|
||||
#include <algorithm>
|
||||
#include <concepts>
|
||||
#include <memory>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <catch2/catch_approx.hpp>
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
|
||||
#include "serif/backend/mfem/model/equilibrium.hpp"
|
||||
#include "serif/tests/equations/fixture.hpp"
|
||||
|
||||
namespace {
|
||||
namespace model = serif::model;
|
||||
namespace backend = model::backend::mfem;
|
||||
namespace equations = serif::equations;
|
||||
namespace execution = serif::operators::backend::mfem;
|
||||
namespace dependencies = serif::utils::dependencies;
|
||||
namespace blocks = serif::discretization::blocks;
|
||||
namespace tests = serif::tests::equations;
|
||||
using EOS = serif::eos::models::Polytrope;
|
||||
using Density = blocks::density::mass;
|
||||
using Enthalpy = ::serif::tests::equations::Enthalpy;
|
||||
using Displacement = blocks::displacement::vector;
|
||||
using Kind = blocks::RepresentedValueKind;
|
||||
using Vector = mfem::Vector;
|
||||
|
||||
/** @brief Require genuine ownership transfer while preserving the wrapped equation's physics and weak test. */
|
||||
template <typename Equation>
|
||||
class MoveOnlyEquation final : public equations::Contribution<equations::TestOperandOf<Equation>, equations::SupportOf<Equation>> {
|
||||
public:
|
||||
using Operands = typename Equation::Operands;
|
||||
static constexpr std::string_view name = "move-only equation";
|
||||
|
||||
/** @brief Retain an evaluation counter so tests can observe lifetime without owning the equation. */
|
||||
MoveOnlyEquation(Equation equation, std::shared_ptr<int> evaluations) :
|
||||
m_equation(std::make_unique<Equation>(std::move(equation))), m_evaluations(std::move(evaluations)) {
|
||||
}
|
||||
|
||||
/** @brief Forward the exact physical residual and derivatives without supplying a clone operation. */
|
||||
[[nodiscard]] auto evaluate(const equations::EquationPoint<Operands>& point) const {
|
||||
++*m_evaluations;
|
||||
return m_equation->evaluate(point);
|
||||
}
|
||||
|
||||
/** @brief Use the same integration order as the underlying physical equation. */
|
||||
template <typename Operand>
|
||||
[[nodiscard]] int additional_order(Operand operand, const int position_order) const {
|
||||
return m_equation->additional_order(operand, position_order);
|
||||
}
|
||||
|
||||
private:
|
||||
std::unique_ptr<Equation> m_equation;
|
||||
std::shared_ptr<int> m_evaluations;
|
||||
};
|
||||
|
||||
static_assert(!std::copy_constructible<MoveOnlyEquation<tests::Closure>>);
|
||||
static_assert(std::move_constructible<MoveOnlyEquation<tests::Closure>>);
|
||||
static_assert(serif::operators::VolumeEquation<MoveOnlyEquation<tests::Closure>>);
|
||||
|
||||
/** @brief All borrowed finite-element bindings belong to the retained fixture. */
|
||||
[[nodiscard]] auto bindings(const tests::Fixture& fields) {
|
||||
return dependencies::make_dependency_context(dependencies::borrow<Density>(fields.density_binding),
|
||||
dependencies::borrow<Enthalpy>(fields.enthalpy_binding), dependencies::borrow<Displacement>(fields.displacement_binding));
|
||||
}
|
||||
|
||||
/** @brief Copy evaluated residuals before perturbing the candidate state. */
|
||||
[[nodiscard]] Vector residual(const backend::PhysicalSystem& system) {
|
||||
Vector result(static_cast<int>(system.residual().size()));
|
||||
std::copy(system.residual().begin(), system.residual().end(), result.GetData());
|
||||
return result;
|
||||
}
|
||||
|
||||
/** @brief Check complete actions and exact transposes after their model and cache have gone away. */
|
||||
void check_retained(const backend::PhysicalSystem& system, const Vector& direction, const Vector& dual,
|
||||
const Vector& expected_action, const Vector& expected_transpose) {
|
||||
Vector action, transpose;
|
||||
system.jacobian().action()->Mult(direction, action);
|
||||
system.jacobian().action()->MultTranspose(dual, transpose);
|
||||
CHECK(tests::relative_error(action, expected_action) == 0.0);
|
||||
CHECK(tests::relative_error(transpose, expected_transpose) == 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Move-only interior equations preserve evaluation derivatives and retained ownership", "[model][move_only][integration]") {
|
||||
std::weak_ptr<tests::Fixture> resources;
|
||||
std::weak_ptr<int> equation_lifetime;
|
||||
std::shared_ptr<const backend::PhysicalSystem> retained;
|
||||
Vector direction, dual, expected_action, expected_transpose, expected_residual;
|
||||
{
|
||||
auto fields = std::make_shared<tests::Fixture>();
|
||||
resources = fields;
|
||||
auto evaluations = std::make_shared<int>(0);
|
||||
equation_lifetime = evaluations;
|
||||
const EOS eos{2.0, 0.3};
|
||||
auto physical = model::make_stellar_model(eos, model::EquilibriumEquations {
|
||||
.interior_equations = equations::EquationRegistry{MoveOnlyEquation{tests::Closure{eos}, std::move(evaluations)}}
|
||||
});
|
||||
STATIC_CHECK_FALSE(std::copy_constructible<decltype(physical)>);
|
||||
auto discretization = backend::make_stellar_discretization(fields, bindings(*fields), fields->compactification, fields->mapper);
|
||||
Vector density = fields->density(), enthalpy = fields->enthalpy();
|
||||
const Vector displacement = fields->displacement(0.03);
|
||||
const auto coefficients = fields->state(density, enthalpy);
|
||||
auto evaluator = backend::bind_stellar_model(std::move(physical), std::move(discretization), coefficients, displacement);
|
||||
CHECK(*equation_lifetime.lock() == 0);
|
||||
auto evaluation = evaluator.evaluate(coefficients, displacement);
|
||||
REQUIRE(evaluation);
|
||||
CHECK(*equation_lifetime.lock() > 0);
|
||||
retained = *evaluation;
|
||||
expected_residual = residual(*retained);
|
||||
|
||||
tests::PreparedClosure reference(tests::Closure{eos}, fields->density_binding, fields->displacement_binding, fields->compactification, fields->mapper);
|
||||
reference.prepare(coefficients, displacement, fields->workspace);
|
||||
CHECK(tests::relative_error(expected_residual, reference.data().residual) < 2.0e-13);
|
||||
const int initial_evaluations = *equation_lifetime.lock();
|
||||
evaluation = evaluator.evaluate(coefficients, displacement);
|
||||
REQUIRE(evaluation);
|
||||
CHECK(*evaluation == retained);
|
||||
CHECK(*equation_lifetime.lock() == initial_evaluations);
|
||||
|
||||
direction = tests::deterministic(retained->jacobian().action()->Width(), 0.41);
|
||||
dual = tests::deterministic(retained->jacobian().action()->Height(), 0.83);
|
||||
retained->jacobian().action()->Mult(direction, expected_action);
|
||||
retained->jacobian().action()->MultTranspose(dual, expected_transpose);
|
||||
REQUIRE(expected_action.Norml2() > 0.0);
|
||||
CHECK((expected_action * dual) == Catch::Approx(direction * expected_transpose).margin(2.0e-12));
|
||||
const auto& layout = evaluator.layout();
|
||||
const int density_offset = static_cast<int>(layout.template offset<Kind::value, Density>());
|
||||
const int enthalpy_offset = static_cast<int>(layout.template offset<Kind::value, Enthalpy>());
|
||||
const Vector initial_density(density), initial_enthalpy(enthalpy);
|
||||
const auto perturb = [&](const double step) {
|
||||
for (int index = 0; index < density.Size(); ++index) {
|
||||
density[index] = initial_density[index] + step * direction[density_offset + index];
|
||||
}
|
||||
for (int index = 0; index < enthalpy.Size(); ++index) {
|
||||
enthalpy[index] = initial_enthalpy[index] + step * direction[enthalpy_offset + index];
|
||||
}
|
||||
};
|
||||
constexpr double step = 1.0e-6;
|
||||
perturb(step);
|
||||
evaluation = evaluator.evaluate(coefficients, displacement);
|
||||
REQUIRE(evaluation);
|
||||
CHECK(*evaluation != retained);
|
||||
CHECK(*equation_lifetime.lock() > initial_evaluations);
|
||||
Vector difference = residual(**evaluation);
|
||||
perturb(-step);
|
||||
evaluation = evaluator.evaluate(coefficients, displacement);
|
||||
REQUIRE(evaluation);
|
||||
difference -= residual(**evaluation);
|
||||
difference /= 2.0 * step;
|
||||
CHECK(tests::relative_error(expected_action, difference) < 2.0e-8);
|
||||
|
||||
const auto before_rejection = *evaluation;
|
||||
const Vector before_rejection_residual = residual(*before_rejection);
|
||||
Vector before_rejection_action, before_rejection_transpose;
|
||||
before_rejection->jacobian().action()->Mult(direction, before_rejection_action);
|
||||
before_rejection->jacobian().action()->MultTranspose(dual, before_rejection_transpose);
|
||||
enthalpy = -1.0;
|
||||
evaluation = evaluator.evaluate(coefficients, displacement);
|
||||
REQUIRE_FALSE(evaluation);
|
||||
CHECK_FALSE(evaluator.cache().ready());
|
||||
CHECK(tests::relative_error(residual(*before_rejection), before_rejection_residual) == 0.0);
|
||||
check_retained(*before_rejection, direction, dual, before_rejection_action, before_rejection_transpose);
|
||||
CHECK(tests::relative_error(residual(*retained), expected_residual) == 0.0);
|
||||
check_retained(*retained, direction, dual, expected_action, expected_transpose);
|
||||
}
|
||||
CHECK_FALSE(resources.expired());
|
||||
CHECK_FALSE(equation_lifetime.expired());
|
||||
CHECK(tests::relative_error(residual(*retained), expected_residual) == 0.0);
|
||||
check_retained(*retained, direction, dual, expected_action, expected_transpose);
|
||||
retained.reset();
|
||||
CHECK(resources.expired());
|
||||
CHECK(equation_lifetime.expired());
|
||||
}
|
||||
Reference in New Issue
Block a user