Files
MeanField/tests/user-api/stellar_equilibrium.cpp

231 lines
9.8 KiB
C++

#include <concepts>
#include <numbers>
#include <type_traits>
#include <utility>
#include <catch2/catch_test_macros.hpp>
#include <mfem.hpp>
import mean_field;
import test_helpers;
namespace {
struct UserApiZeroMetric final { };
[[nodiscard]] mean_field::solver::nonlinear::MetricEvaluation getMetric(
const UserApiZeroMetric &,
const mfem::Vector &,
MPI_Comm
) {
return {.residualNorm = 0.0, .merit = 0.0};
}
[[nodiscard]] mean_field::fem::FEM makeFiniteElements() {
const mean_field::utils::Args arguments = test_utils::setup_args();
return mean_field::fem::setup_fem(arguments.mesh_file, arguments, 0);
}
template <typename Problem>
concept PreparesWithGeneratedRotation = requires(
Problem &problem,
const mfem::Vector &state,
const mean_field::operators::StellarEquilibriumDependencies &dependencies
) { problem.Prepare(state, dependencies); };
template <typename Problem>
concept PreparesWithPrescribedRotation = requires(
Problem &problem,
const mfem::Vector &state,
const mean_field::operators::StellarEquilibriumDependencies &dependencies,
const mean_field::physics::RigidRotation &rotation
) { problem.Prepare(state, dependencies, rotation); };
} // namespace
TEST_CASE(
"Simple User API Builds A Stellar Model With Its Default Preconditioner",
"[user-api][simple]"
) {
using namespace mean_field;
auto finiteElements = makeFiniteElements();
REQUIRE(finiteElements.okay());
auto model = model::StellarModel(
eos::Polytrope({.n = 1.0, .K = 0.25}), surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}),
integral::FixedTotalMass({.Mtotal = dimensions::MassValue{1.0}})
);
auto problem = equilibrium::discretize(model, std::move(finiteElements));
auto preconditioner = preconditioning::makePreconditioner(problem);
const auto &gravity = preconditioner.structureComponent().gravityComponent();
STATIC_CHECK(preconditioning::PreconditionerComponent<decltype(preconditioner)>);
STATIC_CHECK(
std::same_as<
typename std::remove_cvref_t<decltype(gravity)>::MassBackend, preconditioning::backend::MatrixFreeChebyshev>
);
CHECK(problem.StateSize() == problem.EquationSize());
CHECK(decltype(preconditioner)::borderValueArity == 1);
CHECK(gravity.massInverseBackend().order == 5);
CHECK(gravity.potentialSchurBackend().application.cycles == 3);
}
TEST_CASE(
"Complete User API Builds A Context And Evaluates A Borrowing Newton Solver",
"[user-api][solver][context][report]"
) {
using namespace mean_field;
auto finiteElements = makeFiniteElements();
REQUIRE(finiteElements.okay());
constexpr double radius = utils::RADIUS;
const double polytropicConstant = 2.0 * utils::G * radius * radius / std::numbers::pi_v<double>;
const double centralDensity = std::numbers::pi_v<double> * utils::MASS / (4.0 * radius * radius * radius);
auto stellarModel = model::StellarModel(
eos::Polytrope({.n = 1.0, .K = polytropicConstant}),
surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}),
integral::FixedTotalMass({.Mtotal = dimensions::MassValue{utils::MASS}}),
constraint::FixedCentralDensity({.RhoC = dimensions::DensityValue{centralDensity}})
);
auto discretization = equilibrium::makeStellarDiscretization(
std::move(finiteElements), normalization::PhysicalRieszDiagonal{dimensions::LengthValue{radius}, utils::G}
);
auto context = solver::makeContext(
std::move(stellarModel), std::move(discretization), preconditioning::makePreconditioner(),
solver::linear::FGMRES({.restartLength = 20, .printLevel = -1})
);
int beforeCalls = 0;
int afterCalls = 0;
auto observer = solver::nonlinear::makeObserver(
[&beforeCalls](const solver::nonlinear::BeforeIteration &) { ++beforeCalls; },
[&afterCalls](const solver::nonlinear::AfterIteration &) { ++afterCalls; }
);
auto newton = solver::nonlinear::Newton(solver::nonlinear::NewtonOptions{}, UserApiZeroMetric{});
auto equilibriumSolver = solver::make(context, newton, observer);
auto report = equilibriumSolver.evaluate();
REQUIRE(report.converged());
CHECK(report.completedNonlinearIterations() == 0);
CHECK(beforeCalls == 0);
CHECK(afterCalls == 0);
auto structure = report.structureView();
REQUIRE(structure.valid());
CHECK_FALSE(structure.state().empty());
CHECK(context.hasActiveSolver());
}
TEST_CASE(
"Fixed Angular Momentum User API Generates Rotation And Its Composable Solver Border",
"[user-api][fixed-angular-momentum][type]"
) {
using namespace mean_field;
auto finiteElements = makeFiniteElements();
REQUIRE(finiteElements.okay());
auto model = model::StellarModel(
eos::Polytrope({.n = 1.0, .K = 0.25}), surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}),
integral::FixedTotalMass({.Mtotal = dimensions::MassValue{1.0}}),
integral::FixedAngularMomentum({.Jtotal = dimensions::AngularMomentumValue{0.2}, .axis = {0.0, 0.0, 2.0}})
);
auto problem = equilibrium::discretize(model, std::move(finiteElements));
auto preconditioner = preconditioning::makePreconditioner(problem);
using Problem = std::remove_cvref_t<decltype(problem)>;
using Preconditioner = std::remove_cvref_t<decltype(preconditioner)>;
STATIC_CHECK(Problem::hasFixedAngularMomentum);
STATIC_CHECK_FALSE(Problem::hasFixedCentralDensity);
STATIC_CHECK(PreparesWithGeneratedRotation<Problem>);
STATIC_CHECK_FALSE(PreparesWithPrescribedRotation<Problem>);
STATIC_CHECK(Problem::FormType::value_block_count == 7);
STATIC_CHECK(Problem::FormType::residual_block_count == 7);
STATIC_CHECK(preconditioning::PreconditionerComponent<Preconditioner>);
STATIC_CHECK(Preconditioner::borderValueArity == 2);
STATIC_CHECK(Preconditioner::borderResidualArity == 2);
CHECK(problem.StateSize() == problem.EquationSize());
CHECK(problem.StateSize() == problem.GetPhysicalOperator().Width() + 1);
REQUIRE(problem.GetManifest().constraints().size() == 3);
CHECK(
problem.GetManifest().template specification<models::FixedAngularMomentum>().stableId == "FixedAngularMomentum"
);
CHECK(problem.GetManifest().valueBlocks().back().symbol == "Omega");
CHECK(problem.GetManifest().residualBlocks().back().symbol == "R_J");
}
TEST_CASE(
"Intermediate User API Selects A Coupled Stellar Factorization",
"[user-api][intermediate]"
) {
using namespace mean_field;
auto finiteElements = makeFiniteElements();
REQUIRE(finiteElements.okay());
auto model = model::StellarModel(
eos::Polytrope({.n = 3.0, .K = 0.25}), surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}),
integral::FixedTotalMass({.Mtotal = dimensions::MassValue{1.0}}),
constraint::FixedCentralDensity({.RhoC = dimensions::DensityValue{1.0}})
);
auto problem = equilibrium::discretize(model, std::move(finiteElements));
auto material = preconditioning::materialSurfaceBlock(problem);
auto gravity = preconditioning::GravityFieldBlock(
preconditioning::backend::Diagonal{},
preconditioning::backend::HypreBoomerAMG(preconditioning::backend::FixedCycles{.cycles = 2}),
preconditioning::GravityApproximateLDU{}
);
auto structure = preconditioning::stellarStructureBlock(
problem, material, gravity, preconditioning::ApproximateStellarBlockLDU{}
);
auto preconditioner = preconditioning::specificationBorderBlock(problem, structure);
STATIC_CHECK(preconditioning::PreconditionerComponent<decltype(preconditioner)>);
STATIC_CHECK(preconditioning::backend::ArnoldiAdmissible<typename decltype(preconditioner)::BackendType>);
CHECK(decltype(preconditioner)::borderValueArity == 2);
}
TEST_CASE(
"Advanced User API Composes Explicit Blocks Backends And Application Modes",
"[user-api][advanced]"
) {
using namespace mean_field;
auto finiteElements = makeFiniteElements();
REQUIRE(finiteElements.okay());
auto model = model::StellarModel(
eos::Polytrope({.n = 3.0, .K = 0.25}), surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}),
integral::FixedTotalMass({.Mtotal = dimensions::MassValue{1.0}}),
constraint::FixedCentralDensity({.RhoC = dimensions::DensityValue{1.0}})
);
auto problem = equilibrium::discretize(model, std::move(finiteElements));
auto material = preconditioning::materialSurfaceBlock(
problem, preconditioning::backend::Diagonal{}, preconditioning::backend::Diagonal{},
preconditioning::MaterialThenSurfaceTriangular{}, {.relativeFloor = 1.0e-10, .absoluteFloor = 1.0e-13}
);
auto gravity = preconditioning::GravityFieldBlock(
preconditioning::backend::Diagonal{},
preconditioning::backend::HypreBoomerAMG(
preconditioning::backend::SolveToTolerance{.relativeTolerance = 1.0e-8, .maximumCycles = 20}
),
preconditioning::GravityUpperTriangular{}
);
auto structure = preconditioning::stellarStructureBlock(
problem, material, gravity, preconditioning::MaterialThenGravityTriangular{}
);
auto preconditioner =
preconditioning::specificationBorderBlock(problem, structure, preconditioning::backend::DenseDirect{});
STATIC_CHECK(preconditioning::PreconditionerComponent<decltype(preconditioner)>);
STATIC_CHECK_FALSE(preconditioning::backend::ArnoldiAdmissible<typename decltype(preconditioner)::BackendType>);
CHECK(preconditioner.structureComponent().materialSurfaceComponent().diagonalOptions().relativeFloor == 1.0e-10);
CHECK(
preconditioner.structureComponent().gravityComponent().potentialSchurBackend().application.maximumCycles == 20
);
}