#include #include #include #include #include #include #include #include #include "serif/solver/preconditioner/preconditioner.hpp" #include "serif/utils/dependencies/computation_cache.hpp" namespace { namespace pc = serif::solver::preconditioner; namespace dependencies = serif::utils::dependencies; namespace physical = serif::operators::backend::mfem; namespace coupling = pc::strategies::coupling; using Inverse = pc::backend::mfem::Inverse; using Code = serif::utils::errors::SERiFErrorCode; struct Density { }; struct Temperature { }; struct HeatBalance { }; using DensityKeys = dependencies::KeyList; using HeatKeys = dependencies::KeyList; using TemperatureKeys = dependencies::KeyList; using DensityBlock = pc::keys::Block; using HeatBlock = pc::keys::Block; /** @brief Create independently owned diagonal or one-entry physical matrices. */ [[nodiscard]] std::shared_ptr diagonal(std::initializer_list values) { auto matrix = std::make_shared(values.size()); *matrix = 0.0; int index = 0; for (double value : values) { (*matrix)(index, index) = value; ++index; } return matrix; } /** @brief Apply a compiled inverse without leaving initialization to the implementation. */ [[nodiscard]] mfem::Vector apply(const Inverse& inverse, std::initializer_list coefficients) { mfem::Vector input(coefficients.size()); std::copy(coefficients.begin(), coefficients.end(), input.GetData()); mfem::Vector result(inverse.action->Height()); result = 17.0; inverse.action->Mult(input, result); return result; } /** @brief A custom strategy's reported success must still satisfy the physical block contract. */ struct CustomInverse final { std::expected result; /** @brief Return a chosen result so malformed successful declarations can be tested independently. */ [[nodiscard]] std::expected try_inverse(const physical::LinearizedBlock&) const { return result; } }; } // namespace TEST_CASE("Shared physical layouts reject incorrect constituent sizes even when packed totals agree", "[unit][preconditioner][graph]") { using Layout = serif::discretization::blocks::SystemLayout, dependencies::KeyList>; Layout layout{.state_sizes = {1, 2}, .residual_sizes = {2, 1}}; const auto choice = pc::couple(pc::make_preconditioner(pc::strategies::inverse::Diagonal{}), pc::make_preconditioner(pc::strategies::inverse::Diagonal{}), coupling::SecondThenFirst{}); const auto context = dependencies::make_dependency_context( dependencies::own(physical::linearized_block(diagonal({2.0, 4.0}))), dependencies::own(physical::linearized_block(diagonal({5.0}))), dependencies::own>(physical::linearized_block(std::make_shared<::mfem::DenseMatrix>(2, 1))), dependencies::borrow(layout)); auto cache = dependencies::make_computation_cache(pc::make_preconditioner_graph(choice, layout), context); REQUIRE(cache.try_prepare(context)); const auto accepted = cache.share(); SECTION("Unknown block counts are not interchangeable") { layout.state_sizes = {2, 1}; } SECTION("Equation block counts are checked independently") { layout.residual_sizes = {1, 2}; } SECTION("Matching wrong row and column partitions do not hide a physical mismatch") { layout.state_sizes = {2, 1}; layout.residual_sizes = {1, 2}; } const auto rejected = cache.try_prepare(context); REQUIRE_FALSE(rejected); CHECK(rejected.error().error.code == Code::PRECONDITIONER_SIZE_MISMATCH); CHECK_FALSE(cache.ready()); CHECK_THROWS(cache.share()); const auto old = apply(*accepted, {6.0, 8.0, 20.0}); CHECK(old[0] == Catch::Approx(4.0)); CHECK(old[1] == Catch::Approx(3.0)); CHECK(old[2] == Catch::Approx(2.0)); layout.state_sizes = {1, 2}; layout.residual_sizes = {2, 1}; const auto recovered = cache.try_prepare(context); REQUIRE(recovered); CHECK(recovered->status>() == dependencies::PreparationStatus::reused); CHECK(recovered->status>() == dependencies::PreparationStatus::reused); } TEST_CASE("Triangular preconditioner graphs require and track only their used cross coupling", "[unit][preconditioner][graph]") { using Required = pc::keys::Block; using Unused = pc::keys::Block; const auto choice = pc::couple(pc::make_preconditioner(pc::strategies::inverse::Diagonal{}), pc::make_preconditioner(pc::strategies::inverse::Diagonal{}), coupling::SecondThenFirst{}); const auto graph = pc::make_preconditioner_graph(choice); using Graph = decltype(graph); using Result = decltype(choice)::Result; static_assert(dependencies::ContainsKey); static_assert(!dependencies::ContainsKey); static_assert(dependencies::ContainsKey); static_assert(dependencies::ContainsKey); auto used = physical::linearized_block(diagonal({4.0})); const auto context = dependencies::make_dependency_context( dependencies::own(physical::linearized_block(diagonal({2.0}))), dependencies::own(physical::linearized_block(diagonal({3.0}))), dependencies::borrow(used)); auto cache = dependencies::make_computation_cache(graph, context); REQUIRE(cache.try_prepare(context)); const auto result = apply(cache.get(), {6.0, 12.0}); CHECK(result[0] == Catch::Approx(-5.0)); CHECK(result[1] == Catch::Approx(4.0)); auto ignored = physical::linearized_block(diagonal({99.0})); const auto extended = dependencies::make_dependency_context( dependencies::own(physical::linearized_block(diagonal({2.0}))), dependencies::own(physical::linearized_block(diagonal({3.0}))), dependencies::borrow(used), dependencies::borrow(ignored)); auto extended_cache = dependencies::make_computation_cache(graph, extended); REQUIRE(extended_cache.try_prepare(extended)); const auto accepted = extended_cache.share(); ignored = physical::linearized_block(diagonal({-13.0})); const auto reused = extended_cache.try_prepare(extended); REQUIRE(reused); CHECK(reused->computed_count() == 0); CHECK(extended_cache.share() == accepted); used = physical::linearized_block(diagonal({2.0})); const auto refreshed = extended_cache.try_prepare(extended); REQUIRE(refreshed); CHECK(refreshed->source_changed()); CHECK(refreshed->status() == dependencies::PreparationStatus::computed); CHECK(refreshed->status>() == dependencies::PreparationStatus::reused); CHECK(refreshed->status>() == dependencies::PreparationStatus::reused); const auto updated = apply(extended_cache.get(), {6.0, 12.0}); CHECK(updated[0] == Catch::Approx(-1.0)); CHECK(updated[1] == Catch::Approx(4.0)); CHECK(apply(*accepted, {6.0, 12.0})[0] == Catch::Approx(result[0])); CHECK(apply(*accepted, {6.0, 12.0})[1] == Catch::Approx(result[1])); } TEST_CASE("Custom inverse graph results must preserve the selected physical action contract", "[unit][preconditioner][graph]") { std::expected result = Inverse{.action = diagonal({0.5, 0.25})}; Code expected_code = Code::PRECONDITIONER_INVALID_OPERATOR; SECTION("Successful preparation cannot publish a null inverse") { result->action.reset(); } SECTION("An inverse must reverse the selected physical dimensions") { result->action = diagonal({0.5}); expected_code = Code::PRECONDITIONER_SIZE_MISMATCH; } SECTION("The declared application contract must be valid") { result->contract = static_cast(99); expected_code = Code::PRECONDITIONER_INVALID_OPTIONS; } SECTION("An originating custom failure propagates unchanged") { expected_code = Code::PRECONDITIONER_INVALID_CALIBRATION; result = std::unexpected(pc::Failure{expected_code, "Custom calibration failed once."}); } const auto choice = pc::make_preconditioner(CustomInverse{result}); const auto context = dependencies::make_dependency_context(dependencies::own(physical::linearized_block(diagonal({2.0, 4.0})))); auto cache = dependencies::make_computation_cache(pc::make_preconditioner_graph(choice), context); const auto rejected = cache.try_prepare(context); REQUIRE_FALSE(rejected); CHECK(rejected.error().error.code == expected_code); if (expected_code == Code::PRECONDITIONER_INVALID_CALIBRATION) { CHECK(rejected.error().error.message == "Custom calibration failed once."); } } TEST_CASE("Empty physical constraint lists reuse the field recipe and accepted inverse", "[unit][preconditioner][graph]") { const auto fields = pc::make_preconditioner(pc::strategies::inverse::Diagonal{}); using Empty = dependencies::KeyList<>; const auto choice = pc::with_constraints>(fields); static_assert(std::same_as); const auto context = dependencies::make_dependency_context(dependencies::own(physical::linearized_block(diagonal({2.0})))); auto cache = dependencies::make_computation_cache(pc::make_preconditioner_graph(choice), context); REQUIRE(cache.try_prepare(context)); CHECK(apply(cache.get(), {8.0})[0] == Catch::Approx(4.0)); }