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:
197
tests/preconditioner/graph.cpp
Normal file
197
tests/preconditioner/graph.cpp
Normal file
@@ -0,0 +1,197 @@
|
||||
#include <array>
|
||||
#include <memory>
|
||||
#include <span>
|
||||
|
||||
#include <catch2/catch_approx.hpp>
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <catch2/matchers/catch_matchers_string.hpp>
|
||||
|
||||
#include "serif/backend/mfem/operators/actions.hpp"
|
||||
#include "serif/solver/preconditioner/preconditioner.hpp"
|
||||
#include "serif/backend/mfem/solver/normalization/operators.hpp"
|
||||
#include "serif/utils/dependencies/computation_cache.hpp"
|
||||
|
||||
namespace
|
||||
{
|
||||
namespace pc = serif::solver::preconditioner;
|
||||
namespace dependencies = serif::utils::dependencies;
|
||||
namespace operators = serif::operators::backend::mfem;
|
||||
namespace inverse = pc::strategies::inverse;
|
||||
namespace coupling = pc::strategies::coupling;
|
||||
using Inverse = pc::backend::mfem::Inverse;
|
||||
using Code = serif::utils::errors::SERiFErrorCode;
|
||||
|
||||
struct Temperature
|
||||
{
|
||||
};
|
||||
struct HeatBalance
|
||||
{
|
||||
};
|
||||
struct Density
|
||||
{
|
||||
};
|
||||
struct MassConstraint
|
||||
{
|
||||
};
|
||||
struct MassParameter
|
||||
{
|
||||
};
|
||||
struct Potential
|
||||
{
|
||||
};
|
||||
struct Gradient
|
||||
{
|
||||
};
|
||||
|
||||
/** @brief Construct owned matrices independently of the preconditioner factorizations. */
|
||||
std::shared_ptr<const mfem::DenseMatrix> matrix(const int rows, const int columns, std::initializer_list<double> values)
|
||||
{
|
||||
auto result = std::make_shared<mfem::DenseMatrix>(rows, columns);
|
||||
auto value = values.begin();
|
||||
for (int row = 0; row < rows; ++row)
|
||||
{
|
||||
for (int column = 0; column < columns; ++column)
|
||||
{
|
||||
(*result)(row, column) = *value++;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** @brief Apply using preallocated MFEM coordinates, retaining the factory's zero-guess contract. */
|
||||
mfem::Vector apply(const Inverse& inverse, std::initializer_list<double> values)
|
||||
{
|
||||
mfem::Vector input(values.size());
|
||||
std::copy(values.begin(), values.end(), input.GetData());
|
||||
mfem::Vector output(inverse.action->Height());
|
||||
output = 91.0;
|
||||
inverse.action->Mult(input, output);
|
||||
return output;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("A physical inverse graph reuses accepted results and preserves a frozen solve after failed preparation", "[unit][preconditioner][graph]")
|
||||
{
|
||||
const auto choice = pc::make_preconditioner<Temperature, HeatBalance>(inverse::Diagonal{});
|
||||
using Recipe = decltype(choice);
|
||||
using Block = pc::keys::Block<Recipe::Rows, Recipe::Values>;
|
||||
auto physical = operators::linearized_block(matrix(2, 2, {2.0, 0.0, 0.0, -4.0}));
|
||||
const auto context = dependencies::make_dependency_context(dependencies::borrow<Block>(physical));
|
||||
auto cache = dependencies::make_computation_cache<pc::Failure>(pc::make_preconditioner_graph(choice), context);
|
||||
REQUIRE(cache.try_prepare(context));
|
||||
auto frozen = cache.share<Recipe::Result>();
|
||||
CHECK(apply(*frozen, {6.0, 8.0})[0] == Catch::Approx(3.0));
|
||||
CHECK(apply(*frozen, {6.0, 8.0})[1] == Catch::Approx(-2.0));
|
||||
const auto reused = cache.try_prepare(context);
|
||||
REQUIRE(reused);
|
||||
CHECK(reused->computed_count() == 0);
|
||||
CHECK(cache.share<Recipe::Result>() == frozen);
|
||||
|
||||
physical = operators::linearized_block(matrix(2, 2, {0.0, 0.0, 0.0, -4.0}));
|
||||
const auto rejected = cache.try_prepare(context);
|
||||
REQUIRE_FALSE(rejected);
|
||||
CHECK(rejected.error().error.code == Code::PRECONDITIONER_SINGULAR_OPERATOR);
|
||||
CHECK_FALSE(cache.ready());
|
||||
CHECK_THROWS(cache.share<Recipe::Result>());
|
||||
CHECK(apply(*frozen, {6.0, 8.0})[0] == Catch::Approx(3.0));
|
||||
|
||||
physical = operators::linearized_block(matrix(2, 2, {3.0, 0.0, 0.0, -8.0}));
|
||||
REQUIRE(cache.try_prepare(context));
|
||||
CHECK(cache.share<Recipe::Result>() != frozen);
|
||||
CHECK(apply(cache.get<Recipe::Result>(), {6.0, 8.0})[0] == Catch::Approx(2.0));
|
||||
cache.invalidate();
|
||||
CHECK(apply(*frozen, {6.0, 8.0})[1] == Catch::Approx(-2.0));
|
||||
}
|
||||
|
||||
TEST_CASE("Composed inverse graphs obey separate physical state and equation orders", "[unit][preconditioner][graph]")
|
||||
{
|
||||
using Values = dependencies::KeyList<Density, Temperature>;
|
||||
using Rows = dependencies::KeyList<HeatBalance, Density>;
|
||||
const serif::discretization::blocks::SystemLayout<Values, Rows> layout{.state_sizes = {1, 2}, .residual_sizes = {2, 1}};
|
||||
const auto heat = pc::make_preconditioner<Temperature, HeatBalance>(inverse::Diagonal{});
|
||||
const auto density = pc::make_preconditioner<Density>(inverse::Diagonal{});
|
||||
// A zero cross block makes the triangular coupling act blockwise, isolating the coordinate orders.
|
||||
const auto choice = pc::couple(heat, density, coupling::SecondThenFirst{});
|
||||
const auto context = dependencies::make_dependency_context(
|
||||
pc::physical_block<HeatBalance, Temperature>(operators::linearized_block(matrix(2, 2, {2.0, 0.0, 0.0, 4.0}))),
|
||||
pc::physical_block<Density, Density>(operators::linearized_block(matrix(1, 1, {5.0}))),
|
||||
pc::physical_block<HeatBalance, Density>(operators::linearized_block(matrix(2, 1, {0.0, 0.0}))),
|
||||
dependencies::borrow<pc::keys::Layout>(layout));
|
||||
auto cache = dependencies::make_computation_cache<pc::Failure>(pc::make_preconditioner_graph(choice, layout), context);
|
||||
REQUIRE(cache.try_prepare(context));
|
||||
const auto result = apply(cache.get<pc::keys::Preconditioner>(), {6.0, 8.0, 20.0});
|
||||
CHECK(result[0] == Catch::Approx(4.0));
|
||||
CHECK(result[1] == Catch::Approx(3.0));
|
||||
CHECK(result[2] == Catch::Approx(2.0));
|
||||
}
|
||||
|
||||
TEST_CASE("Constraint graph refresh follows child inverse changes without requiring a new root Jacobian", "[unit][preconditioner][graph]")
|
||||
{
|
||||
using D = dependencies::KeyList<Density>;
|
||||
using R = dependencies::KeyList<MassConstraint>;
|
||||
using V = dependencies::KeyList<MassParameter>;
|
||||
using Constraints = pc::ConstraintCoordinates<R, V>;
|
||||
const auto field_choice = pc::make_preconditioner<Density>(inverse::Diagonal{});
|
||||
const auto choice = pc::with_constraints<Constraints>(field_choice);
|
||||
using Recipe = decltype(choice);
|
||||
auto fields = operators::linearized_block(matrix(1, 1, {2.0}));
|
||||
const auto context = dependencies::make_dependency_context(
|
||||
dependencies::borrow<pc::keys::Block<D, D>>(fields),
|
||||
dependencies::own<pc::keys::Block<D, V>>(operators::linearized_block(matrix(1, 1, {1.0}))),
|
||||
dependencies::own<pc::keys::Block<R, D>>(operators::linearized_block(matrix(1, 1, {3.0}))),
|
||||
dependencies::own<pc::keys::Block<R, V>>(operators::linearized_block(matrix(1, 1, {4.0}))));
|
||||
auto cache = dependencies::make_computation_cache<pc::Failure>(pc::make_preconditioner_graph(choice), context);
|
||||
REQUIRE(cache.try_prepare(context));
|
||||
auto old = cache.share<Recipe::Result>();
|
||||
const auto old_result = apply(*old, {7.0, 15.0});
|
||||
CHECK(old_result[0] == Catch::Approx(2.6));
|
||||
CHECK(old_result[1] == Catch::Approx(1.8));
|
||||
fields = operators::linearized_block(matrix(1, 1, {4.0}));
|
||||
auto refreshed = cache.try_prepare(context);
|
||||
REQUIRE(refreshed);
|
||||
CHECK(refreshed->status<Recipe::Result>() == dependencies::PreparationStatus::computed);
|
||||
const auto result = apply(cache.get<Recipe::Result>(), {7.0, 15.0});
|
||||
CHECK(4.0 * result[0] + result[1] == Catch::Approx(7.0));
|
||||
CHECK(3.0 * result[0] + 4.0 * result[1] == Catch::Approx(15.0));
|
||||
CHECK(apply(*old, {7.0, 15.0})[0] == Catch::Approx(old_result[0]));
|
||||
}
|
||||
|
||||
TEST_CASE("An incompatible physical representation returns a preparation error", "[unit][preconditioner][graph]")
|
||||
{
|
||||
const auto choice = pc::make_preconditioner<Density>(inverse::Diagonal{});
|
||||
const auto context = dependencies::make_dependency_context(pc::physical_block<Density, Density>(operators::LinearizedBlock(matrix(1, 1, {2.0}))));
|
||||
auto cache = dependencies::make_computation_cache<pc::Failure>(pc::make_preconditioner_graph(choice), context);
|
||||
const auto result = cache.try_prepare(context);
|
||||
REQUIRE_FALSE(result);
|
||||
CHECK(result.error().error.code == Code::PRECONDITIONER_INCOMPATIBLE_STRATEGY);
|
||||
}
|
||||
|
||||
TEST_CASE("MFEM adapters bind accepted Jacobians and reuse the existing normalization transform", "[unit][preconditioner][graph]")
|
||||
{
|
||||
namespace normalization = serif::solver::normalization;
|
||||
auto physical = matrix(2, 2, {2.0, 0.0, 0.0, 4.0});
|
||||
const auto inverse = pc::backend::mfem::try_dense_inverse(*physical);
|
||||
REQUIRE(inverse);
|
||||
pc::backend::mfem::Solver solver(*inverse, physical);
|
||||
mfem::Vector input(2), output(2), wrong_size(1);
|
||||
input = 2.0;
|
||||
CHECK_THROWS_WITH(solver.Mult(wrong_size, output), Catch::Matchers::ContainsSubstring("PRECONDITIONER_SIZE_MISMATCH"));
|
||||
CHECK_THROWS_WITH(solver.Mult(input, wrong_size), Catch::Matchers::ContainsSubstring("PRECONDITIONER_SIZE_MISMATCH"));
|
||||
solver.Mult(input, output);
|
||||
CHECK(output[0] == Catch::Approx(1.0));
|
||||
CHECK(output[1] == Catch::Approx(0.5));
|
||||
CHECK_NOTHROW(solver.SetOperator(*physical));
|
||||
const auto other = matrix(2, 2, {2.0, 0.0, 0.0, 4.0});
|
||||
CHECK_THROWS(solver.SetOperator(*other));
|
||||
const auto scaling = normalization::make_diagonal_normalization(std::vector<double>{3.0, 5.0}, std::vector<double>{7.0, 11.0});
|
||||
REQUIRE(scaling);
|
||||
normalization::backend::mfem::ScaledJacobianOperator jacobian(*physical, *scaling);
|
||||
normalization::backend::mfem::ScaledPreconditioner scaled(solver, jacobian);
|
||||
mfem::Vector state(2), residual(2), recovered(2);
|
||||
state[0] = 2.0;
|
||||
state[1] = -3.0;
|
||||
jacobian.Mult(state, residual);
|
||||
scaled.Mult(residual, recovered);
|
||||
CHECK(recovered[0] == Catch::Approx(state[0]));
|
||||
CHECK(recovered[1] == Catch::Approx(state[1]));
|
||||
}
|
||||
Reference in New Issue
Block a user