Files
MeanField/tests/solver/plateau_surface_border_schur.cpp

428 lines
19 KiB
C++

#include <array>
#include <cmath>
#include <cstdint>
#include <initializer_list>
#include <limits>
#include <type_traits>
#include <vector>
#include <catch2/catch_approx.hpp>
#include <catch2/catch_test_macros.hpp>
#include <mfem.hpp>
#include <mpi.h>
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<double> 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<double> 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<double>::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<Operation>);
static_assert(probe::SurfaceBorderSchurInverse<Inverse>);
static_assert(!probe::SurfaceBorderSchurInverse<FlexibleInverse>);
static_assert(!probe::SurfaceBorderSchurOperation<mfem::Operator>);
using Schur = probe::PreparedSurfaceBorderSchur<Operation, Inverse>;
static_assert(probe::SurfaceBorderSchurInverse<Schur>);
static_assert(!std::is_copy_constructible_v<Schur>);
// 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<blocks::density::mass::value,
blocks::surface_deformation::parameters::value, blocks::enthalpy::specific::value>;
using ResidualBlocks = blocks::type_list<blocks::enthalpy::specific::residual,
blocks::surface_deformation::shape_equilibrium::residual, blocks::density::mass::residual>;
};
struct RootDescriptor {
using StructureComponent = StructureDescriptor;
using CorrectionBlocks = blocks::type_list<blocks::density::mass::value,
blocks::surface_deformation::parameters::value, blocks::enthalpy::specific::value,
blocks::fixed_total_mass::mass_normalization::value,
blocks::fixed_central_density::central_value::value>;
using ResidualBlocks = blocks::type_list<blocks::enthalpy::specific::residual,
blocks::surface_deformation::shape_equilibrium::residual, blocks::density::mass::residual,
blocks::fixed_total_mass::mass_normalization::residual,
blocks::fixed_central_density::central_value::residual>;
static constexpr std::size_t borderValueArity = 2, borderResidualArity = 2;
};
struct CoordinateMap {
using Range = preconditioning::EquilibriumCoordinateRange;
std::array<Range, 5> columns{{{5, 0, 2}, {1, 2, 3}, {0, 5, 1}, {4, 6, 1}, {7, 7, 1}}};
std::array<Range, 5> 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<decltype(adapter)>);
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<int>{1, 2, 3, 4, 7});
CHECK(coordinates.residualIndices == std::vector<int>{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<double>::quiet_NaN();
CHECK_THROWS_AS(schur.Mult(rhs, action), std::runtime_error);
}