Files
MeanField/tests/solver/stellar_equilibrium_architecture.cpp

1050 lines
48 KiB
C++

#include <array>
#include <cmath>
#include <concepts>
#include <cstdint>
#include <limits>
#include <memory>
#include <numbers>
#include <optional>
#include <span>
#include <stdexcept>
#include <type_traits>
#include <utility>
#include <variant>
#include <catch2/catch_test_macros.hpp>
#include <mfem.hpp>
#include <mpi.h>
import mean_field;
import test_helpers;
namespace solver_architecture_test {
struct MissingBackendApplicationContract { };
struct NonConstantBackendApplicationContract { };
struct WrongTypeBackendApplicationContract { };
struct OutOfDomainBackendApplicationContract { };
} // namespace solver_architecture_test
namespace mean_field::preconditioning::backend {
template <> struct Traits<solver_architecture_test::MissingBackendApplicationContract> {
static constexpr bool registered = true;
};
template <> struct Traits<solver_architecture_test::NonConstantBackendApplicationContract> {
static constexpr bool registered = true;
inline static ApplicationContract applicationContract = ApplicationContract::stationary_linear;
};
template <> struct Traits<solver_architecture_test::WrongTypeBackendApplicationContract> {
static constexpr bool registered = true;
static constexpr int applicationContract = 0;
};
template <> struct Traits<solver_architecture_test::OutOfDomainBackendApplicationContract> {
static constexpr bool registered = true;
static constexpr ApplicationContract applicationContract = static_cast<ApplicationContract>(127);
};
} // namespace mean_field::preconditioning::backend
namespace solver_architecture_test {
struct LifetimeProbe final {
const void *operatorIdentity{nullptr};
const void *preconditionerIdentity{nullptr};
const void *problemIdentity{nullptr};
const void *mapperIdentity{nullptr};
MPI_Comm communicator{MPI_COMM_NULL};
int rightHandSideWorkspaceSize{0};
int correctionWorkspaceSize{0};
bool backendDestroyed{false};
bool dependenciesAliveAtBackendDestruction{false};
bool customPreconditionerOwnsMarker{false};
bool activeRotationIsFinite{false};
std::array<double, 3> activeAngularVelocity{};
std::array<double, 3> activeRotationCenter{};
};
struct FlexibleBackend final : mean_field::solver::LinearBackendConfigurationTag {
static constexpr mean_field::preconditioning::ApplicationContract supportedPreconditionerContract =
mean_field::preconditioning::ApplicationContract::flexible;
std::shared_ptr<LifetimeProbe> probe;
bool duplicateCommunicator{false};
explicit FlexibleBackend(
std::shared_ptr<LifetimeProbe> lifetimeProbe = nullptr,
const bool ownsCommunicatorDuplicate = false
)
: probe(std::move(lifetimeProbe)),
duplicateCommunicator(ownsCommunicatorDuplicate) {
}
};
struct StationaryBackend final : mean_field::solver::LinearBackendConfigurationTag {
static constexpr mean_field::preconditioning::ApplicationContract supportedPreconditionerContract =
mean_field::preconditioning::ApplicationContract::stationary_linear;
};
struct NonStaticBackendConfiguration final : mean_field::solver::LinearBackendConfigurationTag {
mean_field::preconditioning::ApplicationContract supportedPreconditionerContract{
mean_field::preconditioning::ApplicationContract::stationary_linear
};
};
struct NonConstantBackendConfiguration final : mean_field::solver::LinearBackendConfigurationTag {
inline static mean_field::preconditioning::ApplicationContract supportedPreconditionerContract =
mean_field::preconditioning::ApplicationContract::stationary_linear;
};
struct OutOfDomainBackendConfiguration final : mean_field::solver::LinearBackendConfigurationTag {
static constexpr mean_field::preconditioning::ApplicationContract supportedPreconditionerContract =
static_cast<mean_field::preconditioning::ApplicationContract>(127);
};
class IdentityOperator final : public mfem::Operator {
public:
explicit IdentityOperator(const int size) : mfem::Operator(size) {
}
void Mult(
const mfem::Vector &input,
mfem::Vector &output
) const override {
output = input;
}
};
template <mean_field::preconditioning::ApplicationContract Contract>
class ContractInverse final : public mfem::Solver {
public:
static constexpr mean_field::preconditioning::ApplicationContract applicationContract = Contract;
explicit ContractInverse(const int size) : mfem::Solver(size) {
}
void SetOperator(const mfem::Operator &operation) override {
if (operation.Height() != Height() || operation.Width() != Width()) {
throw std::invalid_argument("The test inverse received an incompatible operator.");
}
}
void Mult(
const mfem::Vector &input,
mfem::Vector &output
) const override {
output = input;
}
};
using StationaryInverse = ContractInverse<mean_field::preconditioning::ApplicationContract::stationary_linear>;
using FlexibleInverse = ContractInverse<mean_field::preconditioning::ApplicationContract::flexible>;
struct NonStaticContract final {
mean_field::preconditioning::ApplicationContract applicationContract{
mean_field::preconditioning::ApplicationContract::stationary_linear
};
};
struct NonConstantContract final {
static mean_field::preconditioning::ApplicationContract applicationContract;
};
struct WrongTypeContract final {
static constexpr int applicationContract = 0;
};
struct OutOfDomainContract final {
static constexpr mean_field::preconditioning::ApplicationContract applicationContract =
static_cast<mean_field::preconditioning::ApplicationContract>(127);
};
struct ConflictingContract final {
static constexpr mean_field::preconditioning::ApplicationContract applicationContract =
mean_field::preconditioning::ApplicationContract::flexible;
using BackendType = mean_field::preconditioning::backend::Identity;
};
template <typename Backend> struct BackendContractCarrier final {
using BackendType = Backend;
};
struct NonStaticContractWithValidBackend final {
mean_field::preconditioning::ApplicationContract applicationContract{
mean_field::preconditioning::ApplicationContract::stationary_linear
};
using BackendType = mean_field::preconditioning::backend::Identity;
};
struct NonConstantContractWithValidBackend final {
inline static mean_field::preconditioning::ApplicationContract applicationContract =
mean_field::preconditioning::ApplicationContract::stationary_linear;
using BackendType = mean_field::preconditioning::backend::Identity;
};
template <typename Operator, typename Preconditioner> class PreparedBackend final {
public:
PreparedBackend(
const Operator &operation,
Preconditioner &preconditioner,
const MPI_Comm communicator,
std::shared_ptr<LifetimeProbe> probe,
const bool duplicateCommunicator
)
: m_operation(&operation),
m_preconditioner(&preconditioner),
m_communicator(communicator),
m_rightHandSideWorkspace(operation.Height()),
m_correctionWorkspace(operation.Width()),
m_probe(std::move(probe)) {
m_rightHandSideWorkspace = 0.0;
m_correctionWorkspace = 0.0;
if (m_probe != nullptr) {
m_probe->operatorIdentity = m_operation;
m_probe->preconditionerIdentity = m_preconditioner;
m_probe->communicator = m_communicator;
m_probe->rightHandSideWorkspaceSize = m_rightHandSideWorkspace.Size();
m_probe->correctionWorkspaceSize = m_correctionWorkspace.Size();
if constexpr (requires { preconditioner.GetPhysicalInverse().ownsMarker(); }) {
m_probe->customPreconditionerOwnsMarker = preconditioner.GetPhysicalInverse().ownsMarker();
}
if constexpr (requires { operation.GetProblem(); }) {
const auto &problem = operation.GetProblem();
m_probe->problemIdentity = std::addressof(problem);
m_probe->mapperIdentity = std::addressof(problem.GetDiscretization().domainMapper());
const auto rotation = problem.GetPreparedOperator().GetRotation();
m_probe->activeRotationIsFinite = true;
for (std::size_t component = 0; component < m_probe->activeAngularVelocity.size(); ++component) {
m_probe->activeAngularVelocity[component] =
rotation.angular_velocity()(static_cast<int>(component));
m_probe->activeRotationCenter[component] = rotation.center()(static_cast<int>(component));
m_probe->activeRotationIsFinite = m_probe->activeRotationIsFinite &&
std::isfinite(m_probe->activeAngularVelocity[component]) &&
std::isfinite(m_probe->activeRotationCenter[component]);
}
}
}
if (duplicateCommunicator) {
if (communicator == MPI_COMM_NULL || MPI_Comm_dup(communicator, &m_communicator) != MPI_SUCCESS) {
throw std::runtime_error("The test backend could not duplicate its communicator.");
}
m_ownsCommunicator = true;
if (m_probe != nullptr) {
m_probe->communicator = m_communicator;
}
}
}
PreparedBackend(const PreparedBackend &) = delete;
PreparedBackend &operator=(const PreparedBackend &) = delete;
PreparedBackend(PreparedBackend &&) = delete;
PreparedBackend &operator=(PreparedBackend &&) = delete;
~PreparedBackend() {
if (m_probe != nullptr) {
m_probe->backendDestroyed = true;
if constexpr (requires { m_preconditioner->IsCurrent(); }) {
m_probe->dependenciesAliveAtBackendDestruction =
m_operation != nullptr && m_preconditioner != nullptr && m_preconditioner->IsCurrent();
} else {
m_probe->dependenciesAliveAtBackendDestruction =
m_operation != nullptr && m_preconditioner != nullptr;
}
}
if (m_ownsCommunicator && m_communicator != MPI_COMM_NULL) {
MPI_Comm_free(&m_communicator);
}
}
[[nodiscard]] const Operator &GetOperator() const noexcept {
return *m_operation;
}
[[nodiscard]] const Preconditioner &GetPreconditioner() const noexcept {
return *m_preconditioner;
}
[[nodiscard]] MPI_Comm GetCommunicator() const noexcept {
return m_communicator;
}
[[nodiscard]] bool IsReady() const noexcept {
return m_operation != nullptr && m_preconditioner != nullptr && m_communicator != MPI_COMM_NULL &&
m_rightHandSideWorkspace.Size() == m_operation->Height() &&
m_correctionWorkspace.Size() == m_operation->Width();
}
[[nodiscard]] int RightHandSideSize() const noexcept {
return m_operation->Height();
}
[[nodiscard]] int CorrectionSize() const noexcept {
return m_operation->Width();
}
[[nodiscard]] mean_field::solver::LinearSolveReport Solve(
const mfem::Vector &rightHandSide,
mfem::Vector &correction,
const mean_field::solver::LinearSolveControl &control
) {
if (rightHandSide.Size() != RightHandSideSize() || correction.Size() != CorrectionSize()) {
throw std::invalid_argument("The test backend requires compatible preallocated vectors.");
}
control.Validate();
m_operation->Mult(correction, m_correctionWorkspace);
m_rightHandSideWorkspace = rightHandSide;
m_rightHandSideWorkspace -= m_correctionWorkspace;
const double localNorm = m_rightHandSideWorkspace.Norml2();
const double localNormSquared = localNorm * localNorm;
double globalNormSquared = 0.0;
if (MPI_Allreduce(&localNormSquared, &globalNormSquared, 1, MPI_DOUBLE, MPI_SUM, m_communicator) !=
MPI_SUCCESS) {
throw std::runtime_error("The test backend could not reduce its initial residual norm.");
}
m_correctionWorkspace = rightHandSide;
correction = m_correctionWorkspace;
return {
.status = mean_field::solver::LinearSolveStatus::converged,
.control = control,
.iterations = 0,
.restarts = 0,
.initialResidualNorm = std::sqrt(globalNormSquared),
.reportedResidualNorm = 0.0,
.trueResidualNorm = 0.0,
.relativeTrueResidualNorm = 0.0,
.operatorApplications = 0,
.inversePreconditionerApplications = 0,
.solveSeconds = 0.0
};
}
private:
const Operator *m_operation;
Preconditioner *m_preconditioner;
MPI_Comm m_communicator;
mfem::Vector m_rightHandSideWorkspace;
mfem::Vector m_correctionWorkspace;
std::shared_ptr<LifetimeProbe> m_probe;
bool m_ownsCommunicator{false};
};
template <
typename Operator,
typename Preconditioner>
[[nodiscard]] auto prepareLinearBackend(
FlexibleBackend configuration,
const Operator &operation,
Preconditioner &preconditioner,
const MPI_Comm communicator
) {
return PreparedBackend<Operator, Preconditioner>{
operation, preconditioner, communicator, std::move(configuration.probe), configuration.duplicateCommunicator
};
}
template <
typename Operator,
typename Preconditioner>
[[nodiscard]] auto prepareLinearBackend(
StationaryBackend,
const Operator &operation,
Preconditioner &preconditioner,
const MPI_Comm communicator
) {
return PreparedBackend<Operator, Preconditioner>{operation, preconditioner, communicator, nullptr, false};
}
struct BorrowingBackend final : mean_field::solver::LinearBackendConfigurationTag {
static constexpr mean_field::preconditioning::ApplicationContract supportedPreconditionerContract =
mean_field::preconditioning::ApplicationContract::flexible;
};
template <
typename Operator,
typename Preconditioner>
[[nodiscard]] auto prepareLinearBackend(
BorrowingBackend,
const Operator &,
Preconditioner &,
MPI_Comm
)
-> PreparedBackend<
Operator,
Preconditioner> &;
struct OwningPreconditionerPrescription final : mean_field::preconditioning::StellarPreconditionerPrescriptionTag {
OwningPreconditionerPrescription() : marker(std::make_unique<int>(37)) {
}
OwningPreconditionerPrescription(const OwningPreconditionerPrescription &) = delete;
OwningPreconditionerPrescription &operator=(const OwningPreconditionerPrescription &) = delete;
OwningPreconditionerPrescription(OwningPreconditionerPrescription &&) noexcept = default;
OwningPreconditionerPrescription &operator=(OwningPreconditionerPrescription &&) = delete;
std::unique_ptr<int> marker;
};
template <typename Problem> class OwningPreparedInverse final : public mfem::Solver {
public:
static constexpr mean_field::preconditioning::ApplicationContract applicationContract =
mean_field::preconditioning::ApplicationContract::flexible;
OwningPreparedInverse(
const Problem &problem,
std::unique_ptr<int> marker
)
: mfem::Solver(
problem.StateSize(),
problem.EquationSize()
),
m_problem(&problem),
m_preparationGeneration(problem.GetPreparationGeneration()),
m_marker(std::move(marker)) {
if (m_marker == nullptr) {
throw std::invalid_argument("The test preconditioner requires its owned marker.");
}
}
OwningPreparedInverse(const OwningPreparedInverse &) = delete;
OwningPreparedInverse &operator=(const OwningPreparedInverse &) = delete;
OwningPreparedInverse(OwningPreparedInverse &&) = delete;
OwningPreparedInverse &operator=(OwningPreparedInverse &&) = delete;
void SetOperator(const mfem::Operator &operation) override {
if (std::addressof(operation) != std::addressof(m_problem->GetLinearizationOperator())) {
throw std::invalid_argument("The test preconditioner cannot be rebound to another problem.");
}
}
void Mult(
const mfem::Vector &input,
mfem::Vector &output
) const override {
output = input;
}
[[nodiscard]] const Problem &GetProblem() const noexcept {
return *m_problem;
}
[[nodiscard]] bool IsCurrent() const noexcept {
return m_problem != nullptr && m_problem->IsPrepared() &&
m_preparationGeneration == m_problem->GetPreparationGeneration();
}
void Refresh() {
if (m_problem == nullptr || !m_problem->IsPrepared()) {
throw std::logic_error("The test preconditioner cannot refresh from an unprepared problem.");
}
m_preparationGeneration = m_problem->GetPreparationGeneration();
}
[[nodiscard]] bool ownsMarker() const noexcept {
return m_marker != nullptr && *m_marker == 37;
}
private:
const Problem *m_problem;
std::uint64_t m_preparationGeneration;
std::unique_ptr<int> m_marker;
};
template <typename Problem>
[[nodiscard]] auto prepareStellarPreconditioner(
OwningPreconditionerPrescription prescription,
const Problem &problem
) {
return OwningPreparedInverse<Problem>{problem, std::move(prescription.marker)};
}
struct BorrowingPreconditionerPrescription final
: mean_field::preconditioning::StellarPreconditionerPrescriptionTag { };
template <typename Problem>
[[nodiscard]] auto prepareStellarPreconditioner(
BorrowingPreconditionerPrescription,
const Problem &
) -> OwningPreparedInverse<Problem> &;
[[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);
}
[[nodiscard]] auto makeFixedCentralDensityModel() {
using namespace mean_field;
constexpr double stellarRadius = utils::RADIUS;
constexpr double targetMass = utils::MASS;
const double polytropicConstant = 2.0 * utils::G * stellarRadius * stellarRadius / std::numbers::pi_v<double>;
const double centralDensity =
std::numbers::pi_v<double> * targetMass / (4.0 * stellarRadius * stellarRadius * stellarRadius);
return model::StellarModel(
eos::Polytrope({.n = 1.0, .K = polytropicConstant}),
surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}),
integral::FixedTotalMass({.Mtotal = dimensions::MassValue{targetMass}}),
constraint::FixedCentralDensity({.RhoC = dimensions::DensityValue{centralDensity}})
);
}
[[nodiscard]] auto makeFixedCentralDensityAngularMomentumModel() {
using namespace mean_field;
constexpr double stellarRadius = utils::RADIUS;
constexpr double targetMass = utils::MASS;
const double polytropicConstant = 2.0 * utils::G * stellarRadius * stellarRadius / std::numbers::pi_v<double>;
const double centralDensity =
std::numbers::pi_v<double> * targetMass / (4.0 * stellarRadius * stellarRadius * stellarRadius);
return model::StellarModel(
eos::Polytrope({.n = 1.0, .K = polytropicConstant}),
surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}),
integral::FixedTotalMass({.Mtotal = dimensions::MassValue{targetMass}}),
integral::FixedAngularMomentum({.Jtotal = dimensions::AngularMomentumValue{0.05}}),
constraint::FixedCentralDensity({.RhoC = dimensions::DensityValue{centralDensity}})
);
}
[[nodiscard]] auto makeMassOnlyModel() {
using namespace mean_field;
constexpr double stellarRadius = utils::RADIUS;
constexpr double targetMass = utils::MASS;
const double polytropicConstant = 2.0 * utils::G * stellarRadius * stellarRadius / std::numbers::pi_v<double>;
return model::StellarModel(
eos::Polytrope({.n = 1.0, .K = polytropicConstant}),
surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}),
integral::FixedTotalMass({.Mtotal = dimensions::MassValue{targetMass}})
);
}
[[nodiscard]] mean_field::dimensions::DensityValue matchingCentralDensity() {
constexpr double stellarRadius = mean_field::utils::RADIUS;
constexpr double targetMass = mean_field::utils::MASS;
return mean_field::dimensions::DensityValue{
std::numbers::pi_v<double> * targetMass / (4.0 * stellarRadius * stellarRadius * stellarRadius)
};
}
[[nodiscard]] mean_field::physics::RigidRotation makeRotation(const double angularSpeed = 0.0) {
mfem::Vector angularVelocity(3);
mfem::Vector center(3);
angularVelocity = 0.0;
center = 0.0;
angularVelocity(2) = angularSpeed;
center(0) = 0.125;
center(1) = -0.25;
center(2) = 0.5;
return mean_field::physics::RigidRotation{angularVelocity, center};
}
struct ImmovableRadialSeed final {
ImmovableRadialSeed() = default;
ImmovableRadialSeed(const ImmovableRadialSeed &) = delete;
ImmovableRadialSeed(ImmovableRadialSeed &&) = delete;
ImmovableRadialSeed &operator=(const ImmovableRadialSeed &) = delete;
ImmovableRadialSeed &operator=(ImmovableRadialSeed &&) = delete;
};
template <typename Model>
[[nodiscard]] mean_field::seed::RadialProfile generateRadialProfile(
const Model &,
const ImmovableRadialSeed &
);
template <typename Candidate>
concept HasEvaluate = requires(Candidate &candidate) { candidate.evaluate(); };
template <typename Candidate>
concept ExposesFiniteElementModel = requires(const Candidate &candidate) { candidate.finiteElementModel(); };
template <typename Candidate>
concept ExposesStructureManifest = requires(const Candidate &candidate) { candidate.manifest(); };
template <typename Candidate>
concept ExposesMutableMFEMState = requires(const Candidate &candidate) {
{ candidate.state() } -> std::same_as<const mfem::Vector &>;
};
template <typename Candidate>
concept ExposesStructureStateView = requires(const Candidate &candidate) { candidate.stateView(); };
template <typename Candidate>
concept ExposesOwnedProblem = requires(const Candidate &candidate) { candidate.problem(); } ||
requires(const Candidate &candidate) { candidate.GetProblem(); };
template <typename Candidate>
concept ReadsCommunicatorFromRvalue = requires(Candidate &&candidate) { std::move(candidate).communicator(); };
template <typename Candidate>
concept ReadsProblemCommunicatorFromRvalue =
requires(Candidate &&candidate) { std::move(candidate).GetCommunicator(); };
template <typename Structure, typename Prescription, typename Backend>
concept RestartableFromRvalue = requires(Structure &&structure, Prescription prescription, Backend backend) {
mean_field::solver::make(std::move(structure), std::move(prescription), std::move(backend));
};
template <typename Structure, typename Prescription, typename Backend>
concept RestartableFromLvalue = requires(Structure &structure, Prescription prescription, Backend backend) {
mean_field::solver::make(structure, std::move(prescription), std::move(backend));
};
template <typename Model, typename Discretization, typename Prescription, typename Backend, typename InitialState>
concept HighLevelSeedAccepted = requires(
Model model,
Discretization discretization,
Prescription prescription,
Backend backend,
InitialState initialState
) {
mean_field::solver::makeContext(
std::move(model), std::move(discretization), std::move(prescription), std::move(backend),
std::move(initialState)
);
};
template <typename Model, typename Discretization, typename Prescription, typename Backend, typename InitialState>
concept ExplicitSeedOverloadsAccepted = requires(
Model model,
Discretization discretization,
Prescription prescription,
Backend backend,
InitialState initialState,
mean_field::seed::StellarEquilibriumProjectionOptions options,
mean_field::physics::RigidRotation rotation
) {
mean_field::solver::makeContext(
std::move(model), std::move(discretization), std::move(prescription), std::move(backend),
std::move(initialState)
);
mean_field::solver::makeContext(
std::move(model), std::move(discretization), std::move(prescription), std::move(backend),
std::move(initialState), std::move(options)
);
mean_field::solver::makeContext(
std::move(model), std::move(discretization), std::move(prescription), std::move(backend),
std::move(initialState), std::move(rotation)
);
mean_field::solver::makeContext(
std::move(model), std::move(discretization), std::move(prescription), std::move(backend),
std::move(initialState), std::move(options), std::move(rotation)
);
};
template <typename Model, typename Discretization, typename Prescription, typename Backend>
concept DefaultSeedWithRotationAccepted = requires(
Model model,
Discretization discretization,
Prescription prescription,
Backend backend,
mean_field::physics::RigidRotation rotation,
mean_field::seed::StellarEquilibriumProjectionOptions options
) {
mean_field::solver::makeContext(
std::move(model), std::move(discretization), std::move(prescription), std::move(backend),
std::move(rotation)
);
mean_field::solver::makeContext(
std::move(model), std::move(discretization), std::move(prescription), std::move(backend),
std::move(options), std::move(rotation)
);
};
} // namespace solver_architecture_test
TEST_CASE(
"Linear Backend Boundary Is Generic And Contract-Aware",
"[solver][architecture][linear]"
) {
using namespace mean_field;
using namespace solver_architecture_test;
STATIC_CHECK(solver::LinearBackendConfiguration<FlexibleBackend>);
STATIC_CHECK(solver::LinearBackendConfiguration<StationaryBackend>);
STATIC_CHECK_FALSE(solver::LinearBackendConfiguration<NonStaticBackendConfiguration>);
STATIC_CHECK_FALSE(solver::LinearBackendConfiguration<NonConstantBackendConfiguration>);
STATIC_CHECK_FALSE(solver::LinearBackendConfiguration<OutOfDomainBackendConfiguration>);
STATIC_CHECK(solver::LinearPreconditionerApplicationContractAvailable<StationaryInverse>);
STATIC_CHECK(solver::LinearPreconditionerApplicationContractAvailable<FlexibleInverse>);
STATIC_CHECK_FALSE(solver::LinearPreconditionerApplicationContractAvailable<NonStaticContract>);
STATIC_CHECK_FALSE(solver::LinearPreconditionerApplicationContractAvailable<NonConstantContract>);
STATIC_CHECK_FALSE(solver::LinearPreconditionerApplicationContractAvailable<WrongTypeContract>);
STATIC_CHECK_FALSE(solver::LinearPreconditionerApplicationContractAvailable<OutOfDomainContract>);
STATIC_CHECK_FALSE(solver::LinearPreconditionerApplicationContractAvailable<ConflictingContract>);
STATIC_CHECK_FALSE(solver::LinearPreconditionerApplicationContractAvailable<NonStaticContractWithValidBackend>);
STATIC_CHECK_FALSE(solver::LinearPreconditionerApplicationContractAvailable<NonConstantContractWithValidBackend>);
STATIC_CHECK_FALSE(
solver::LinearPreconditionerApplicationContractAvailable<
BackendContractCarrier<MissingBackendApplicationContract>>
);
STATIC_CHECK_FALSE(
solver::LinearPreconditionerApplicationContractAvailable<
BackendContractCarrier<NonConstantBackendApplicationContract>>
);
STATIC_CHECK_FALSE(
solver::LinearPreconditionerApplicationContractAvailable<
BackendContractCarrier<WrongTypeBackendApplicationContract>>
);
STATIC_CHECK_FALSE(
solver::LinearPreconditionerApplicationContractAvailable<
BackendContractCarrier<OutOfDomainBackendApplicationContract>>
);
STATIC_CHECK(solver::LinearBackendPreconditionerCompatible<FlexibleBackend, FlexibleInverse>);
STATIC_CHECK(solver::LinearBackendPreconditionerCompatible<StationaryBackend, StationaryInverse>);
STATIC_CHECK_FALSE(solver::LinearBackendPreconditionerCompatible<StationaryBackend, FlexibleInverse>);
using DefaultModel = std::remove_cvref_t<decltype(makeFixedCentralDensityModel())>;
using DefaultProblem = equilibrium::StellarEquilibriumProblem<DefaultModel, equilibrium::StellarDiscretization>;
using DefaultPhysicalInverse =
preconditioning::PreparedStellarInverseType<preconditioning::DefaultStellarPreconditioner, DefaultProblem>;
using DefaultNormalizedInverse =
normalization::NormalizedStellarPreconditioner<DefaultProblem, DefaultPhysicalInverse>;
STATIC_CHECK(solver::LinearPreconditionerApplicationContractAvailable<DefaultPhysicalInverse>);
STATIC_CHECK(
solver::linearPreconditionerApplicationContract<DefaultPhysicalInverse> ==
preconditioning::ApplicationContract::stationary_linear
);
STATIC_CHECK(solver::LinearPreconditionerApplicationContractAvailable<DefaultNormalizedInverse>);
STATIC_CHECK(
solver::linearPreconditionerApplicationContract<DefaultNormalizedInverse> ==
preconditioning::ApplicationContract::stationary_linear
);
STATIC_CHECK(solver::LinearBackendPreconditionerCompatible<StationaryBackend, DefaultNormalizedInverse>);
STATIC_CHECK(
solver::StellarEquilibriumContextConfiguration<
DefaultModel, equilibrium::StellarDiscretization, preconditioning::DefaultStellarPreconditioner,
StationaryBackend>
);
STATIC_CHECK(solver::LinearBackendConfiguration<BorrowingBackend>);
STATIC_CHECK_FALSE(solver::LinearBackendRuntimeAvailableFor<BorrowingBackend, IdentityOperator, FlexibleInverse>);
IdentityOperator operation(4);
FlexibleInverse inverse(4);
auto prepared = prepareLinearBackend(FlexibleBackend{}, operation, inverse, MPI_COMM_WORLD);
using Prepared = std::remove_cvref_t<decltype(prepared)>;
STATIC_CHECK(solver::PreparedLinearBackendFor<Prepared, IdentityOperator, FlexibleInverse>);
STATIC_CHECK_FALSE(std::copy_constructible<Prepared>);
STATIC_CHECK_FALSE(std::move_constructible<Prepared>);
auto nullPrepared = prepareLinearBackend(FlexibleBackend{}, operation, inverse, MPI_COMM_NULL);
CHECK_FALSE(nullPrepared.IsReady());
mfem::Vector rightHandSide(4);
mfem::Vector correction(4);
rightHandSide = 2.0;
correction = -1.0;
const solver::LinearSolveControl control{
.relativeTolerance = 2.0e-7, .absoluteTolerance = 3.0e-12, .maximumIterations = 17
};
CHECK(control.ConvergenceThreshold(4.0) == 8.0e-7);
CHECK(control.ConvergenceThreshold(0.0) == control.absoluteTolerance);
CHECK_THROWS_AS(solver::LinearSolveControl{.relativeTolerance = -1.0}.Validate(), std::invalid_argument);
CHECK_THROWS_AS(
solver::LinearSolveControl{.relativeTolerance = std::numeric_limits<double>::quiet_NaN()}.Validate(),
std::invalid_argument
);
CHECK_THROWS_AS(solver::LinearSolveControl{.absoluteTolerance = -1.0}.Validate(), std::invalid_argument);
CHECK_THROWS_AS(
solver::LinearSolveControl{.absoluteTolerance = std::numeric_limits<double>::quiet_NaN()}.Validate(),
std::invalid_argument
);
CHECK_THROWS_AS(
solver::LinearSolveControl{.absoluteTolerance = std::numeric_limits<double>::infinity()}.Validate(),
std::invalid_argument
);
CHECK_THROWS_AS(solver::LinearSolveControl{.maximumIterations = 0}.Validate(), std::invalid_argument);
CHECK_THROWS_AS(control.ConvergenceThreshold(-1.0), std::invalid_argument);
CHECK_THROWS_AS(control.ConvergenceThreshold(std::numeric_limits<double>::quiet_NaN()), std::invalid_argument);
CHECK_THROWS_AS(control.ConvergenceThreshold(std::numeric_limits<double>::infinity()), std::invalid_argument);
CHECK_THROWS_AS(
solver::LinearSolveControl{.relativeTolerance = std::numeric_limits<double>::max()}.ConvergenceThreshold(2.0),
std::invalid_argument
);
const auto report = prepared.Solve(rightHandSide, correction, control);
CHECK(report.Converged());
CHECK(report.control.relativeTolerance == control.relativeTolerance);
CHECK(report.control.absoluteTolerance == control.absoluteTolerance);
CHECK(report.control.maximumIterations == control.maximumIterations);
CHECK(report.initialResidualNorm == 6.0);
correction -= rightHandSide;
CHECK(correction.Norml2() == 0.0);
}
TEST_CASE(
"Solver Assembly Owns Stable Prepared Runtime Before Evaluation",
"[solver][architecture][ownership]"
) {
using namespace mean_field;
using namespace solver_architecture_test;
auto finiteElements = makeFiniteElements();
REQUIRE(finiteElements.okay());
const void *mapperIdentity = finiteElements.domainMapperStateless.get();
const MPI_Comm expectedCommunicator = finiteElements.mesh->GetComm();
auto discretization = equilibrium::makeStellarDiscretization(
std::move(finiteElements),
normalization::PhysicalRieszDiagonal{dimensions::LengthValue{utils::RADIUS}, utils::G}
);
using Discretization = std::remove_cvref_t<decltype(discretization)>;
STATIC_CHECK(std::move_constructible<Discretization>);
STATIC_CHECK_FALSE(std::copy_constructible<Discretization>);
STATIC_CHECK_FALSE(std::is_move_assignable_v<Discretization>);
STATIC_CHECK_FALSE(std::constructible_from<Discretization, fem::FEM &>);
STATIC_CHECK_FALSE(ReadsCommunicatorFromRvalue<Discretization>);
STATIC_CHECK(
std::same_as<typename Discretization::NormalizationPrescriptionType, normalization::PhysicalRieszDiagonal<>>
);
auto probe = std::make_shared<LifetimeProbe>();
{
auto model = makeFixedCentralDensityAngularMomentumModel();
using Model = std::remove_cvref_t<decltype(model)>;
using Problem = equilibrium::StellarEquilibriumProblem<Model, Discretization>;
STATIC_CHECK(Problem::generatedRotationProviderCount == 1);
STATIC_CHECK_FALSE(ReadsProblemCommunicatorFromRvalue<Problem>);
auto context = solver::makeContext(
std::move(model), std::move(discretization), preconditioning::makePreconditioner(),
FlexibleBackend{probe, true}
);
using Context = std::remove_cvref_t<decltype(context)>;
STATIC_CHECK_FALSE(std::move_constructible<Context>);
STATIC_CHECK_FALSE(std::copy_constructible<Context>);
STATIC_CHECK_FALSE(HasEvaluate<Context>);
REQUIRE(context.isReady());
CHECK_FALSE(context.hasActiveSolver());
{
auto borrowingSolver = solver::make(context, solver::nonlinear::Newton{});
STATIC_CHECK_FALSE(std::move_constructible<std::remove_cvref_t<decltype(borrowingSolver)>>);
STATIC_CHECK_FALSE(std::copy_constructible<std::remove_cvref_t<decltype(borrowingSolver)>>);
STATIC_CHECK(HasEvaluate<std::remove_cvref_t<decltype(borrowingSolver)>>);
REQUIRE(borrowingSolver.isReady());
CHECK(context.hasActiveSolver());
}
CHECK_FALSE(context.hasActiveSolver());
CHECK_FALSE(discretization.isCurrent());
STATIC_CHECK_FALSE(ExposesFiniteElementModel<Discretization>);
CHECK_THROWS_AS(discretization.communicator(), std::logic_error);
CHECK(probe->mapperIdentity == mapperIdentity);
CHECK(probe->rightHandSideWorkspaceSize > 0);
CHECK(probe->correctionWorkspaceSize > 0);
CHECK(probe->rightHandSideWorkspaceSize == probe->correctionWorkspaceSize);
CHECK(probe->communicator != MPI_COMM_NULL);
int communicatorComparison = MPI_UNEQUAL;
REQUIRE(MPI_Comm_compare(probe->communicator, expectedCommunicator, &communicatorComparison) == MPI_SUCCESS);
CHECK((communicatorComparison == MPI_CONGRUENT || communicatorComparison == MPI_IDENT));
CHECK(probe->operatorIdentity != nullptr);
CHECK(probe->preconditionerIdentity != nullptr);
CHECK(probe->problemIdentity != nullptr);
CHECK(probe->activeRotationIsFinite);
CHECK(probe->activeAngularVelocity[0] == 0.0);
CHECK(probe->activeAngularVelocity[1] == 0.0);
CHECK(probe->activeAngularVelocity[2] > 0.0);
CHECK((probe->activeRotationCenter == std::array{0.0, 0.0, 0.0}));
CHECK_FALSE(probe->backendDestroyed);
REQUIRE(context.isReady());
}
CHECK(probe->backendDestroyed);
CHECK(probe->dependenciesAliveAtBackendDestruction);
}
TEST_CASE(
"Active Rotation Is Unavailable Before Problem Preparation",
"[solver][architecture][rotation]"
) {
using namespace mean_field;
using namespace solver_architecture_test;
auto finiteElements = makeFiniteElements();
auto problem = equilibrium::discretize(
makeFixedCentralDensityModel(), equilibrium::StellarDiscretization{std::move(finiteElements)}
);
CHECK_FALSE(problem.IsPrepared());
CHECK_THROWS(problem.GetPreparedOperator().GetRotation());
}
TEST_CASE(
"ADL Preparation Boundaries Reject Borrowed Runtime Results",
"[solver][architecture][ownership][customization]"
) {
using namespace mean_field;
using namespace solver_architecture_test;
using Model = std::remove_cvref_t<decltype(makeFixedCentralDensityModel())>;
using Discretization = equilibrium::StellarDiscretization;
using Problem = equilibrium::StellarEquilibriumProblem<Model, Discretization>;
STATIC_CHECK(preconditioning::StellarPreconditionerPrescription<OwningPreconditionerPrescription>);
STATIC_CHECK(preconditioning::PreparedStellarInverseFor<OwningPreparedInverse<Problem>, Problem>);
STATIC_CHECK(preconditioning::StellarPreconditionerRuntimeAvailableFor<OwningPreconditionerPrescription, Problem>);
STATIC_CHECK_FALSE(
solver::StellarEquilibriumContextConfiguration<
Model, Discretization, OwningPreconditionerPrescription, StationaryBackend>
);
STATIC_CHECK(preconditioning::StellarPreconditionerPrescription<BorrowingPreconditionerPrescription>);
STATIC_CHECK_FALSE(
preconditioning::StellarPreconditionerRuntimeAvailableFor<BorrowingPreconditionerPrescription, Problem>
);
}
TEST_CASE(
"Explicit Seed And Prescribed Rotation Assemble Without A Model Default",
"[solver][architecture][seed]"
) {
using namespace mean_field;
using namespace solver_architecture_test;
using MassOnlyModel = std::remove_cvref_t<decltype(makeMassOnlyModel())>;
using FixedDensityModel = std::remove_cvref_t<decltype(makeFixedCentralDensityModel())>;
STATIC_CHECK_FALSE(solver::DefaultStellarEquilibriumInitialStateAvailableFor<MassOnlyModel>);
STATIC_CHECK(solver::DefaultStellarEquilibriumInitialStateAvailableFor<FixedDensityModel>);
auto finiteElements = makeFiniteElements();
auto probe = std::make_shared<LifetimeProbe>();
auto context = solver::makeContext(
makeMassOnlyModel(), equilibrium::StellarDiscretization{std::move(finiteElements)},
OwningPreconditionerPrescription{}, FlexibleBackend{probe},
seed::LaneEmden({.centralDensity = matchingCentralDensity(), .radialSampleCount = 64}),
seed::StellarEquilibriumProjectionOptions{}, makeRotation(0.03)
);
REQUIRE(context.isReady());
CHECK(probe->customPreconditionerOwnsMarker);
CHECK(probe->activeRotationIsFinite);
CHECK((probe->activeAngularVelocity == std::array{0.0, 0.0, 0.03}));
CHECK((probe->activeRotationCenter == std::array{0.125, -0.25, 0.5}));
}
TEST_CASE(
"Only Target-Projected Seed Strategies Enter High-Level Solver Assembly",
"[solver][architecture][seed][contract]"
) {
using namespace mean_field;
using namespace solver_architecture_test;
using Model = std::remove_cvref_t<decltype(makeFixedCentralDensityModel())>;
using Discretization = equilibrium::StellarDiscretization;
using Prescription = preconditioning::DefaultStellarPreconditioner;
using Projected = seed::ProjectedEquilibriumState<Model>;
STATIC_CHECK_FALSE(solver::StellarEquilibriumInitialStateFor<Projected, Model>);
STATIC_CHECK(seed::RadialSeedStrategyFor<ImmovableRadialSeed, Model>);
STATIC_CHECK_FALSE(std::move_constructible<ImmovableRadialSeed>);
STATIC_CHECK_FALSE(solver::StellarEquilibriumInitialStateFor<ImmovableRadialSeed, Model>);
STATIC_CHECK_FALSE(HighLevelSeedAccepted<Model, Discretization, Prescription, FlexibleBackend, Projected>);
STATIC_CHECK(ExplicitSeedOverloadsAccepted<Model, Discretization, Prescription, FlexibleBackend, seed::LaneEmden>);
STATIC_CHECK_FALSE(
HighLevelSeedAccepted<Model, Discretization, Prescription, FlexibleBackend, ImmovableRadialSeed>
);
STATIC_CHECK(DefaultSeedWithRotationAccepted<Model, Discretization, Prescription, FlexibleBackend>);
using GeneratedRotationModel = std::remove_cvref_t<decltype(makeFixedCentralDensityAngularMomentumModel())>;
STATIC_CHECK_FALSE(
DefaultSeedWithRotationAccepted<GeneratedRotationModel, Discretization, Prescription, FlexibleBackend>
);
}
TEST_CASE(
"Result Views Are Read-Only And Capture Targets Are Opaque Owning Values",
"[solver][architecture][ownership][view][capture]"
) {
using namespace mean_field;
using namespace solver_architecture_test;
using Model = std::remove_cvref_t<decltype(makeFixedCentralDensityModel())>;
using Discretization = equilibrium::StellarDiscretization;
using Problem = equilibrium::StellarEquilibriumProblem<Model, Discretization>;
using Structure = equilibrium::StellarStructure<Problem>;
using Checkpoint = equilibrium::StellarCheckpoint<Problem>;
using StructureView = equilibrium::StellarStructureView<Problem>;
using CheckpointView = equilibrium::StellarCheckpointView<Problem>;
using Prescription = preconditioning::DefaultStellarPreconditioner;
STATIC_CHECK(std::move_constructible<Structure>);
STATIC_CHECK(std::is_nothrow_move_constructible_v<Structure>);
STATIC_CHECK_FALSE(std::copy_constructible<Structure>);
STATIC_CHECK_FALSE(std::is_move_assignable_v<Structure>);
STATIC_CHECK_FALSE(std::default_initializable<Structure>);
STATIC_CHECK_FALSE(std::default_initializable<Checkpoint>);
STATIC_CHECK(std::copy_constructible<StructureView>);
STATIC_CHECK(std::copy_constructible<CheckpointView>);
STATIC_CHECK(std::same_as<decltype(std::declval<const StructureView &>().state()), std::span<const mfem::real_t>>);
STATIC_CHECK(
std::same_as<
decltype(std::declval<const StructureView &>().stateDescriptors()),
std::span<const operators::RootBlockDescriptor>>
);
STATIC_CHECK(std::same_as<decltype(std::declval<const StructureView &>().model()), const Model &>);
STATIC_CHECK(std::same_as<decltype(std::declval<const StructureView &>().communicator()), MPI_Comm>);
STATIC_CHECK_FALSE(ReadsCommunicatorFromRvalue<StructureView>);
STATIC_CHECK(std::same_as<decltype(std::declval<const StructureView &>().valid()), bool>);
STATIC_CHECK(
std::same_as<
decltype(std::declval<const StructureView &>().prescribedRotation()), std::optional<physics::RigidRotation>>
);
STATIC_CHECK(std::same_as<decltype(std::declval<const StructureView &>().rotation()), physics::RigidRotation>);
STATIC_CHECK(std::same_as<decltype(std::declval<const StructureView &>().capture()), Structure>);
STATIC_CHECK(std::same_as<decltype(std::declval<const CheckpointView &>().capture()), Checkpoint>);
STATIC_CHECK_FALSE(ExposesStructureManifest<StructureView>);
STATIC_CHECK_FALSE(ExposesMutableMFEMState<StructureView>);
STATIC_CHECK_FALSE(ExposesStructureStateView<StructureView>);
STATIC_CHECK_FALSE(ExposesFiniteElementModel<StructureView>);
STATIC_CHECK_FALSE(ExposesOwnedProblem<StructureView>);
STATIC_CHECK_FALSE(RestartableFromRvalue<Structure, Prescription, FlexibleBackend>);
STATIC_CHECK_FALSE(RestartableFromLvalue<Structure, Prescription, FlexibleBackend>);
}
TEST_CASE(
"Projection Options Reach Seed Projection Before Runtime Preparation",
"[solver][architecture][seed][options]"
) {
using namespace mean_field;
using namespace solver_architecture_test;
auto finiteElements = makeFiniteElements();
auto probe = std::make_shared<LifetimeProbe>();
seed::StellarEquilibriumProjectionOptions options;
options.surfaceRadiusRelativeTolerance = std::numeric_limits<double>::quiet_NaN();
auto assemble = [&] {
return solver::makeContext(
makeFixedCentralDensityModel(), equilibrium::StellarDiscretization{std::move(finiteElements)},
preconditioning::makePreconditioner(), FlexibleBackend{probe}, options
);
};
REQUIRE_THROWS_AS(assemble(), std::invalid_argument);
CHECK(probe->operatorIdentity == nullptr);
CHECK(probe->preconditionerIdentity == nullptr);
}
TEST_CASE(
"Every Nonlinear Failure Tag Is Preserved By The Evaluation Report",
"[solver][architecture][result]"
) {
using namespace mean_field;
CHECK(solver::StellarEquilibriumFailureReport{}.reason == solver::StellarEquilibriumFailureReason::unspecified);
const std::array reasons{
solver::StellarEquilibriumFailureReason::inadmissible_state,
solver::StellarEquilibriumFailureReason::non_finite_state,
solver::StellarEquilibriumFailureReason::non_finite_residual,
solver::StellarEquilibriumFailureReason::linear_solve_failure,
solver::StellarEquilibriumFailureReason::globalization_failure,
solver::StellarEquilibriumFailureReason::stagnation,
solver::StellarEquilibriumFailureReason::iteration_limit
};
for (const auto reason : reasons) {
const solver::StellarEquilibriumFailureReport report{
.reason = reason,
.message = "retained",
.completedNonlinearIterations = 3,
.initialResidualNorm = 4.0,
.finalResidualNorm = 2.0
};
CHECK(report.reason == reason);
CHECK(report.message == "retained");
CHECK(report.completedNonlinearIterations == 3);
CHECK(report.initialResidualNorm == 4.0);
CHECK(report.finalResidualNorm == 2.0);
}
}