Files
MeanField/tests/preconditioning/spherical_harmonic_surface.cpp

183 lines
8.5 KiB
C++
Raw Normal View History

#include <algorithm>
#include <cmath>
#include <cstdint>
#include <stdexcept>
#include <type_traits>
#include <utility>
#include <catch2/catch_approx.hpp>
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_string.hpp>
#include <mfem.hpp>
#include <mpi.h>
import mean_field;
import test_helpers;
namespace spherical_harmonic_preconditioner_test {
using namespace mean_field;
[[nodiscard]] operators::StellarEquilibriumDependencies dependencies() {
return {
.discretization = {.identity = 9201, .revision = 1},
.density = {.identity = 9203, .revision = 1},
.surfaceDeformation = {.identity = 9209, .revision = 1},
.gravityGradient = {.identity = 9221, .revision = 1},
.gravityPotential = {.identity = 9227, .revision = 1},
.enthalpy = {.identity = 9239, .revision = 1},
.bernoulliConstant = {.identity = 9241, .revision = 1},
.rotation = {.identity = 9257, .revision = 1},
.targetMass = {.identity = 9277, .revision = 1}
};
}
void checkClose(
const mfem::Vector &actual,
const mfem::Vector &expected
) {
REQUIRE(actual.Size() == expected.Size());
mfem::Vector difference(actual);
difference -= expected;
const double error = difference.Norml2();
const double scale = std::max(actual.Norml2(), expected.Norml2());
REQUIRE(std::isfinite(error));
REQUIRE(std::isfinite(scale));
INFO("inverse error = " << error << ", scale = " << scale);
CHECK(error <= 2.e-12 + 3.e-11 * scale);
}
// Construct the advertised triangular surrogate's RHS from a known
// correction. This checks the inverse's parameter ordering and material
// coupling, without asking an approximate inverse to invert the full J.
template <typename Prepared>
void checkTriangularInverse(
const Prepared &inverse,
const int modes
) {
const auto &couplings = inverse.GetCoupledOperator();
const auto &offsets = couplings.GetOffsets();
REQUIRE(offsets[2] - offsets[1] == modes);
REQUIRE(inverse.Width() == offsets[3]);
REQUIRE(inverse.Height() == inverse.Width());
REQUIRE(inverse.GetSurfaceDiagonal().Size() == modes);
mfem::Vector expected(inverse.Height()), rhs(inverse.Width()), actual(inverse.Height());
for (int i = 0; i < expected.Size(); ++i)
expected(i) = 0.02 * std::sin(0.31 * (i + 1));
const mfem::Vector density(expected.GetData(), offsets[1]);
const mfem::Vector shape(expected.GetData() + offsets[1], modes);
const mfem::Vector enthalpy(expected.GetData() + offsets[2], offsets[3] - offsets[2]);
mfem::Vector densityCoupling(density.Size()), enthalpyCoupling(enthalpy.Size()), cross(density.Size());
couplings.ApplySurfaceToMaterial(shape, densityCoupling, enthalpyCoupling);
couplings.ApplyEnthalpyToDensity(enthalpy, cross);
for (int i = 0; i < density.Size(); ++i)
rhs(i) = inverse.GetDensityDiagonal()(i) * density(i) + densityCoupling(i) + cross(i);
for (int i = 0; i < modes; ++i)
rhs(offsets[1] + i) = inverse.GetSurfaceDiagonal()(i) * shape(i);
for (int i = 0; i < enthalpy.Size(); ++i)
rhs(offsets[2] + i) = inverse.GetEnthalpyDiagonal()(i) * enthalpy(i) + enthalpyCoupling(i);
actual = 0.0;
inverse.Mult(rhs, actual);
checkClose(actual, expected);
const mfem::Vector actualShape(actual.GetData() + offsets[1], modes);
checkClose(actualShape, shape);
rhs = 0.0;
actual = 1.0;
inverse.Mult(rhs, actual);
CHECK(actual.Norml2() == 0.0);
CHECK(inverse.IsCurrent());
}
} // namespace spherical_harmonic_preconditioner_test
TEST_CASE(
"Harmonic Surface Mass And Calibrated Inverses Use Native Parameters And Reject Nodal H1",
"[preconditioning][material_surface][spherical_harmonic][integration][serial]"
) {
using namespace mean_field;
using namespace spherical_harmonic_preconditioner_test;
namespace backend = preconditioning::backend;
int ranks = 0;
MPI_Comm_size(MPI_COMM_WORLD, &ranks);
if (ranks != 1)
SKIP("The current radial domain extension supports one MPI rank.");
const auto args = test_utils::setup_args();
auto finiteElements = fem::setup_fem(args.mesh_file, args, 0);
REQUIRE(finiteElements.okay());
const auto stellarModel = model::StellarModel(
eos::Polytrope({.n = 1.5, .K = 0.25}), surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}),
integral::FixedTotalMass({.Mtotal = dimensions::MassValue{1.0}})
);
// A restricted azimuthal set also rules out using (L+1)^2 as the block
// length when the actual mode catalog has fewer independent coefficients.
const deformation::SphericalHarmonicSurface prescription{{.maximumDegree = 2, .maximumAzimuthalOrder = 1}};
constexpr int modes = 7;
auto problem = equilibrium::discretize(
stellarModel,
equilibrium::makeStellarDiscretization(std::move(finiteElements), normalization::Unnormalized{}, prescription)
);
mfem::Vector state(problem.StateSize());
state = 0.0;
const auto view = problem.GetManifest().stateView(state);
view.block(utils::blocks::density_field.mass_term) = 1.0;
view.block(utils::blocks::enthalpy_field.specific_term) = 0.7;
view.block(utils::blocks::fixed_total_mass_constraint.mass_normalization_term) = 0.8;
auto shape = view.block(utils::blocks::surface_deformation_field.parameters_term);
REQUIRE(shape.Size() == modes);
for (int i = 0; i < modes; ++i)
shape(i) = 0.001 * std::sin(0.7 * (i + 1));
mfem::Vector omega(3), center(3);
omega = 0.0;
center = 0.0;
const auto preparation = problem.TryPrepare(state, dependencies(), physics::RigidRotation{omega, center});
REQUIRE(preparation.has_value());
const auto &physical = problem.GetPhysicalOperator();
REQUIRE(physical.IsPrepared());
REQUIRE_FALSE(physical.GetDomainDeformation().hasNodalRadialParameters());
const auto block = preconditioning::materialSurfaceBlock(problem);
STATIC_CHECK(
std::is_same_v<
typename std::remove_cvref_t<decltype(block)>::SurfaceSurrogate, preconditioning::SurfaceMassDiagonal>
);
auto prepared = preconditioning::prepare(problem, block);
CHECK(prepared.GetStatistics().surfaceRieszAssemblies == 1);
CHECK(prepared.GetStatistics().surfaceJacobianProbes == 0);
CHECK(prepared.GetSurfaceDiagonalQuality().minimumAbsoluteEntryBeforeRegularization > 0.0);
checkTriangularInverse(prepared, modes);
const auto unchanged = prepared.Refresh(physical);
CHECK_FALSE(unchanged.DidAnyWork());
const auto calibratedBlock = preconditioning::materialSurfaceBlock(
problem, backend::Diagonal{}, backend::Diagonal{}, preconditioning::SurfaceThenMaterialTriangular{},
{.surfaceCalibration = {
.target = preconditioning::SurfaceRieszCalibrationTarget::surface_jacobian,
.probeCount = 3,
.objective = preconditioning::SurfaceRieszCalibrationObjective::operator_action
}}
);
auto calibrated = preconditioning::prepare(problem, calibratedBlock);
const auto &calibration = calibrated.GetSurfaceCalibration();
REQUIRE(calibration.WasCalibrated());
REQUIRE(std::isfinite(calibration.scale));
REQUIRE(calibration.scale != 0.0);
CHECK(calibration.probeCount == 3);
CHECK(calibrated.GetStatistics().surfaceJacobianProbes == 3);
for (int i = 0; i < modes; ++i)
CHECK(
calibrated.GetSurfaceDiagonal()(i) ==
Catch::Approx(calibration.scale * prepared.GetSurfaceDiagonal()(i)).epsilon(2.e-13)
);
checkTriangularInverse(calibrated, modes);
using FixedAMG = backend::HypreBoomerAMG<backend::FixedCycles>;
const auto h1 = preconditioning::materialSurfaceBlock(
problem, backend::Diagonal{}, FixedAMG{backend::FixedCycles{.cycles = 1}},
preconditioning::ApproximateMaterialSurfaceLDU{}, preconditioning::SurfaceH1MassStiffness{}
);
CHECK_THROWS_WITH(
preconditioning::prepare(problem, h1), Catch::Matchers::ContainsSubstring("requires nodal radial parameters")
);
CHECK(prepared.IsCurrent());
CHECK(calibrated.IsCurrent());
}