#include #include #include #include #include #include #include #include #include #include #include import mean_field; #include "../../experiments/plateau_surface_border_schur.hpp" namespace surface_border_schur_test { namespace probe = experiment::plateau_diagnostics; using namespace mean_field; mfem::DenseMatrix Matrix(int size, std::initializer_list entries) { mfem::DenseMatrix result(size); auto entry = entries.begin(); for (int row = 0; row < size; ++row) for (int column = 0; column < size; ++column) result(row, column) = *entry++; return result; } mfem::Vector Vector(std::initializer_list values) { mfem::Vector result(values.size()); int i = 0; for (double value : values) result(i++) = value; return result; } double Difference(const mfem::Vector &left, const mfem::Vector &right) { mfem::Vector difference(left); difference -= right; return difference.Norml2(); } class Operation final : public mfem::Operator { public: explicit Operation(mfem::DenseMatrix matrix) : mfem::Operator(matrix.Height()), matrix(std::move(matrix)), residualScales(Height()), stateScales(Width()) { residualScales = 1.0; stateScales = 1.0; } void Mult(const mfem::Vector &input, mfem::Vector &output) const override { ++applications; matrix.Mult(input, output); } bool IsPrepared() const { return prepared; } const Operation &GetProblem() const { return *this; } std::uint64_t GetPreparationGeneration() const { return generation; } void DenormalizeResidual(const mfem::Vector &input, mfem::Vector &output) const { for (int i = 0; i < input.Size(); ++i) output(i) = input(i) * residualScales(i); } void NormalizeState(const mfem::Vector &input, mfem::Vector &output) const { for (int i = 0; i < input.Size(); ++i) output(i) = input(i) / stateScales(i); } mfem::DenseMatrix matrix; mfem::Vector residualScales, stateScales; bool prepared{true}; std::uint64_t generation{1}; mutable std::uint64_t applications{0}; }; class Inverse : public mfem::Solver { public: static constexpr auto applicationContract = preconditioning::ApplicationContract::stationary_linear; Inverse(const Operation &operation, mfem::DenseMatrix matrix) : mfem::Solver(matrix.Height()), operation(operation), matrix(std::move(matrix)), generation(operation.generation) {} void SetOperator(const mfem::Operator &) override {} void Mult(const mfem::Vector &input, mfem::Vector &output) const override { ++applications; matrix.Mult(input, output); if (nonfinite) output(0) = std::numeric_limits::infinity(); } bool IsCurrent() const { return generation == operation.generation; } const Operation &GetNormalizedJacobian() const { return operation; } const Operation &operation; mfem::DenseMatrix matrix; std::uint64_t generation; bool nonfinite{false}; mutable std::uint64_t applications{0}; }; class FlexibleInverse : public Inverse { public: using Inverse::Inverse; static constexpr auto applicationContract = preconditioning::ApplicationContract::flexible; }; static_assert(probe::SurfaceBorderSchurOperation); static_assert(probe::SurfaceBorderSchurInverse); static_assert(!probe::SurfaceBorderSchurInverse); static_assert(!probe::SurfaceBorderSchurOperation); using Schur = probe::PreparedSurfaceBorderSchur; static_assert(probe::SurfaceBorderSchurInverse); static_assert(!std::is_copy_constructible_v); // Fields occupy different equation/unknown positions. A contiguous or // same-offset assumption would silently factor the wrong block here. probe::SurfaceBorderSchurCoordinates Coordinates() { return {{1, 3}, {0, 2}}; } mfem::DenseMatrix CoupledMatrix() { return Matrix(4, {0.3, 4, -1, 1, 0, 2, 2, 0.4, 0.5, 2, 0.7, 3, 3, -0.2, 1, 0.9}); } mfem::DenseMatrix FieldInverse(bool exact) { mfem::DenseMatrix result(4); result = 0.0; result(1, 0) = exact ? 0.3 : 0.2; result(1, 2) = exact ? -0.1 : 0.0; result(3, 0) = exact ? -0.2 : 0.0; result(3, 2) = exact ? 0.4 : 0.2; // These coarse contributions must be excluded, not accidentally used // to complete the field response twice. result(0, 0) = 11; result(2, 2) = -17; result(1, 1) = 23; result(3, 3) = -31; return result; } namespace blocks = utils::blocks; struct StructureDescriptor { using BackendType = preconditioning::backend::Diagonal; using CorrectionBlocks = blocks::type_list; using ResidualBlocks = blocks::type_list; }; struct RootDescriptor { using StructureComponent = StructureDescriptor; using CorrectionBlocks = blocks::type_list; using ResidualBlocks = blocks::type_list; static constexpr std::size_t borderValueArity = 2, borderResidualArity = 2; }; struct CoordinateMap { using Range = preconditioning::EquilibriumCoordinateRange; std::array columns{{{5, 0, 2}, {1, 2, 3}, {0, 5, 1}, {4, 6, 1}, {7, 7, 1}}}; std::array rows{{{6, 0, 1}, {2, 1, 3}, {0, 4, 2}, {5, 6, 1}, {7, 7, 1}}}; const auto &GetCorrectionRanges() const { return columns; } const auto &GetResidualRanges() const { return rows; } void PackResidual(const mfem::Vector &input, mfem::Vector &output) const { for (const auto &range : rows) for (int i = 0; i < range.size; ++i) output(range.preconditionerOffset + i) = input(range.equilibriumOffset + i); } void UnpackCorrection(const mfem::Vector &input, mfem::Vector &output) const { for (const auto &range : columns) for (int i = 0; i < range.size; ++i) output(range.equilibriumOffset + i) = input(range.preconditionerOffset + i); } }; struct CoordinateBinding { RootDescriptor root; CoordinateMap coordinates; const auto &GetBlock() const { return root; } const auto &GetCoordinateMap() const { return coordinates; } }; class PackedStructure final : public mfem::Solver { public: PackedStructure() : mfem::Solver(6), matrix(6) { matrix = 0.0; for (int i = 0; i < 6; ++i) matrix(i, i) = i + 2.0; matrix(0, 5) = 0.4; matrix(4, 1) = -0.6; } void SetOperator(const mfem::Operator &) override {} void Mult(const mfem::Vector &input, mfem::Vector &output) const override { ++applications; matrix.Mult(input, output); } mfem::DenseMatrix matrix; mutable unsigned applications{0}; }; struct GroupedStructure { PackedStructure structure; const auto &GetStructurePreconditioner() const { return structure; } }; class PhysicalStructureBinding final : public mfem::Solver, public CoordinateBinding { public: static constexpr auto applicationContract = preconditioning::ApplicationContract::stationary_linear; explicit PhysicalStructureBinding(const Operation &operation) : mfem::Solver(operation.Width()), operation(operation), generation(operation.generation) {} void SetOperator(const mfem::Operator &) override {} void Mult(const mfem::Vector &, mfem::Vector &) const override { throw std::logic_error("The adapter must not apply the scalar-bordered inverse."); } const auto &GetProblem() const { return operation; } const auto &GetGroupedPreconditioner() const { return grouped; } bool IsCurrent() const { return generation == operation.generation; } const Operation &operation; std::uint64_t generation; GroupedStructure grouped; }; } TEST_CASE("Surface Border Schur Structure Adapter Preserves Nonunit Normalization And Typed Packing", "[solver][surface_border_schur][serial]") { using namespace surface_border_schur_test; mfem::DenseMatrix matrix(8); matrix = 0.0; Operation operation(std::move(matrix)); operation.residualScales = Vector({2, 3, 5, 7, 11, 13, 17, 19}); operation.stateScales = Vector({23, 29, 31, 37, 41, 43, 47, 53}); PhysicalStructureBinding physical(operation); probe::NormalizedUnborderedStructureInverse adapter(operation, physical); STATIC_CHECK(probe::SurfaceBorderSchurInverse); auto rhs = Vector({1, 2, 3, 4, 5, 6, 7, 8}); const auto expected = Vector({42.0 / 23, 112.0 / 29, 275.0 / 31, 3.0 / 37, 0, 240.4 / 43, 45.0 / 47, 0}); mfem::Vector observed(8); adapter.Mult(rhs, observed); CHECK(Difference(observed, expected) < 1.e-13); CHECK(physical.grouped.structure.applications == 1); adapter.Mult(rhs, rhs); CHECK(Difference(rhs, expected) < 1.e-13); physical.grouped.structure.iterative_mode = true; CHECK_FALSE(adapter.IsCurrent()); physical.grouped.structure.iterative_mode = false; ++operation.generation; physical.generation = operation.generation; CHECK_FALSE(adapter.IsCurrent()); CHECK_THROWS_AS(adapter.Mult(rhs, observed), std::logic_error); } TEST_CASE("Surface Border Schur Coordinates Include All Native Modes And Generated Central Border", "[solver][surface_border_schur][serial]") { using namespace surface_border_schur_test; const auto coordinates = probe::MakeSurfaceBorderSchurCoordinates(CoordinateBinding{}); CHECK(coordinates.correctionIndices == std::vector{1, 2, 3, 4, 7}); CHECK(coordinates.residualIndices == std::vector{2, 3, 4, 5, 7}); } TEST_CASE("Surface Border Schur With Exact Field Inverse Solves Full Nonsymmetric System", "[solver][surface_border_schur][serial]") { using namespace surface_border_schur_test; Operation operation(CoupledMatrix()); Inverse inverse(operation, FieldInverse(true)); Schur schur(operation, inverse, Coordinates(), MPI_COMM_WORLD); REQUIRE(schur.IsCurrent()); CHECK(schur.GetStatistics().setupOperatorApplications == 4); CHECK(schur.GetStatistics().setupInverseApplications == 2); const auto rhs = Vector({1, -2, 3, 4}); mfem::Vector action(4), expected(4), defect(4); mfem::DenseMatrixInverse direct(operation.matrix); direct.Mult(rhs, expected); schur.Mult(rhs, action); CHECK(Difference(action, expected) < 1.e-13); operation.Mult(action, defect); CHECK(Difference(defect, rhs) < 1.e-13); CHECK(schur.GetStatistics().operatorApplications == 1); CHECK(schur.GetStatistics().inverseApplications == 1); for (int column = 0; column < 2; ++column) { const auto &completed = schur.GetCompletedColumns()[column]; operation.Mult(completed, defect); CHECK(std::abs(defect(0)) < 1.e-13); CHECK(std::abs(defect(2)) < 1.e-13); CHECK(completed(Coordinates().correctionIndices[column]) == 1.0); } } TEST_CASE("Surface Border Schur With Approximate Fields Enforces Every Coarse Equation", "[solver][surface_border_schur][serial]") { using namespace surface_border_schur_test; Operation operation(CoupledMatrix()); Inverse inverse(operation, FieldInverse(false)); Schur schur(operation, inverse, Coordinates(), MPI_COMM_WORLD); REQUIRE(schur.IsCurrent()); const auto rhs = Vector({1, -2, 3, 4}); mfem::Vector action(4), image(4); schur.Mult(rhs, action); operation.Mult(action, image); for (int index : Coordinates().residualIndices) CHECK(std::abs(image(index) - rhs(index)) < 1.e-13); CHECK(Difference(image, rhs) > 1.e-3); // Verify S = D - A_CF B A_FC independently in this explicit packing. const auto &s = schur.GetSchurMatrix(); CHECK(s(0, 0) == Catch::Approx(-0.16)); CHECK(s(0, 1) == Catch::Approx(2.344)); CHECK(s(1, 0) == Catch::Approx(2.922)); CHECK(s(1, 1) == Catch::Approx(0.834)); } TEST_CASE("Surface Border Schur Is Stationary Linear And Supports Aliasing", "[solver][surface_border_schur][serial]") { using namespace surface_border_schur_test; Operation operation(CoupledMatrix()); Inverse inverse(operation, FieldInverse(false)); Schur schur(operation, inverse, Coordinates(), MPI_COMM_WORLD); const auto first = Vector({1, 2, -1, 3}), second = Vector({-2, 4, 0.5, 1}); mfem::Vector a(4), b(4), combined(first), expected(4), observed(4); schur.Mult(first, a); schur.Mult(second, b); combined *= 2.3; combined.Add(-0.7, second); expected = a; expected *= 2.3; expected.Add(-0.7, b); schur.Mult(combined, observed); CHECK(Difference(observed, expected) < 1.e-13); schur.Mult(combined, combined); CHECK(Difference(combined, expected) < 1.e-13); schur.Mult(first, observed); CHECK(Difference(observed, a) == 0.0); } TEST_CASE("Surface Border Schur Reports Singular And Ill Conditioned Coarse Blocks Without Regularization", "[solver][surface_border_schur][serial]") { using namespace surface_border_schur_test; Operation operation(Matrix(3, {1, 0, 0, 0, 0, 0, 0, 0, 1})); Inverse inverse(operation, Matrix(3, {1, 0, 0, 0, 0, 0, 0, 0, 0})); Schur singular(operation, inverse, {{1, 2}, {1, 2}}, MPI_COMM_WORLD); CHECK(singular.GetStatistics().status == probe::SurfaceBorderSchurStatus::singular); CHECK_FALSE(singular.IsCurrent()); auto rhs = Vector({1, 2, 3}); mfem::Vector action(3); CHECK_THROWS_AS(singular.Mult(rhs, action), std::logic_error); operation.matrix(1, 1) = 1.e-16; ++operation.generation; inverse.generation = operation.generation; singular.Refresh(); CHECK(singular.GetStatistics().status == probe::SurfaceBorderSchurStatus::ill_conditioned); CHECK(singular.GetStatistics().reciprocalCondition == Catch::Approx(1.e-16)); CHECK_FALSE(singular.IsCurrent()); CHECK_THROWS_AS(singular.SetOperator(operation), std::logic_error); CHECK(singular.GetSchurMatrix()(0, 0) == 1.e-16); } TEST_CASE("Surface Border Schur Handles Pivoting And Uniform Matrix Scale", "[solver][surface_border_schur][serial]") { using namespace surface_border_schur_test; for (double scale : {1.e-100, 1.0, 1.e100}) { auto matrix = Matrix(3, {2, 0, 0, 0, 0, 2, 0, 3, 1}); matrix *= scale; Operation operation(std::move(matrix)); auto base = Matrix(3, {0.5, 0, 0, 0, 0, 0, 0, 0, 0}); base *= 1.0 / scale; Inverse inverse(operation, std::move(base)); Schur schur(operation, inverse, {{1, 2}, {1, 2}}, MPI_COMM_WORLD); REQUIRE(schur.IsCurrent()); auto rhs = Vector({1, 2, 4}); rhs *= scale; mfem::Vector action(3); schur.Mult(rhs, action); CHECK(Difference(action, Vector({0.5, 1, 1})) < 1.e-13); CHECK(schur.GetStatistics().reciprocalCondition == Catch::Approx(0.5)); } } TEST_CASE("Surface Border Schur Requires Unbordered Fields For A Zero Scalar Diagonal", "[solver][surface_border_schur][serial]") { using namespace surface_border_schur_test; Operation operation(Matrix(2, {1, 1, 1, 0})); Inverse alreadyBordered(operation, Matrix(2, {0, 1, 1, -1})); Schur wrongRestriction(operation, alreadyBordered, {{1}, {1}}, MPI_COMM_WORLD); CHECK(wrongRestriction.GetStatistics().status == probe::SurfaceBorderSchurStatus::singular); Inverse unbordered(operation, Matrix(2, {1, 0, 0, 0})); Schur schur(operation, unbordered, {{1}, {1}}, MPI_COMM_WORLD); REQUIRE(schur.IsCurrent()); auto rhs = Vector({3, 2}); mfem::Vector action(2); schur.Mult(rhs, action); CHECK(Difference(action, Vector({2, 1})) < 1.e-13); } TEST_CASE("Surface Border Schur Rejects Invalid Partitions And Stale Or Nonfinite Dependencies", "[solver][surface_border_schur][serial]") { using namespace surface_border_schur_test; Operation operation(CoupledMatrix()); Inverse inverse(operation, FieldInverse(true)); const auto reject = [&](probe::SurfaceBorderSchurCoordinates coordinates) { CHECK_THROWS_AS(Schur(operation, inverse, std::move(coordinates), MPI_COMM_WORLD), std::invalid_argument); }; reject({{}, {}}); reject({{1}, {0, 2}}); reject({{1, 1}, {0, 2}}); reject({{1, 3}, {0, 0}}); reject({{1, 4}, {0, 2}}); reject({{0, 1, 2, 3}, {0, 1, 2, 3}}); CHECK_THROWS_AS(Schur(operation, inverse, Coordinates(), MPI_COMM_NULL), std::invalid_argument); Schur schur(operation, inverse, Coordinates(), MPI_COMM_WORLD); auto rhs = Vector({1, 2, 3, 4}); mfem::Vector action(4); Operation other(CoupledMatrix()); CHECK_THROWS_AS(schur.SetOperator(other), std::invalid_argument); CHECK_THROWS_AS(Schur(other, inverse, Coordinates(), MPI_COMM_WORLD), std::invalid_argument); ++operation.generation; CHECK_FALSE(schur.IsCurrent()); CHECK_THROWS_AS(schur.Mult(rhs, action), std::logic_error); inverse.generation = operation.generation; CHECK_FALSE(schur.IsCurrent()); schur.Refresh(); REQUIRE(schur.IsCurrent()); inverse.iterative_mode = true; CHECK_FALSE(schur.IsCurrent()); CHECK_THROWS_AS(schur.Mult(rhs, action), std::logic_error); inverse.iterative_mode = false; inverse.nonfinite = true; CHECK_THROWS_AS(schur.Mult(rhs, action), std::runtime_error); inverse.nonfinite = false; rhs(0) = std::numeric_limits::quiet_NaN(); CHECK_THROWS_AS(schur.Mult(rhs, action), std::runtime_error); }