#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include 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 { static constexpr bool registered = true; }; template <> struct Traits { static constexpr bool registered = true; inline static ApplicationContract applicationContract = ApplicationContract::stationary_linear; }; template <> struct Traits { static constexpr bool registered = true; static constexpr int applicationContract = 0; }; template <> struct Traits { static constexpr bool registered = true; static constexpr ApplicationContract applicationContract = static_cast(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 activeAngularVelocity{}; std::array activeRotationCenter{}; }; struct FlexibleBackend final : mean_field::solver::LinearBackendConfigurationTag { static constexpr mean_field::preconditioning::ApplicationContract supportedPreconditionerContract = mean_field::preconditioning::ApplicationContract::flexible; std::shared_ptr probe; bool duplicateCommunicator{false}; explicit FlexibleBackend( std::shared_ptr 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(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 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; using FlexibleInverse = ContractInverse; 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(127); }; struct ConflictingContract final { static constexpr mean_field::preconditioning::ApplicationContract applicationContract = mean_field::preconditioning::ApplicationContract::flexible; using BackendType = mean_field::preconditioning::backend::Identity; }; template 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 class PreparedBackend final { public: PreparedBackend( const Operator &operation, Preconditioner &preconditioner, const MPI_Comm communicator, std::shared_ptr 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(component)); m_probe->activeRotationCenter[component] = rotation.center()(static_cast(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 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{ 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{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(37)) { } OwningPreconditionerPrescription(const OwningPreconditionerPrescription &) = delete; OwningPreconditionerPrescription &operator=(const OwningPreconditionerPrescription &) = delete; OwningPreconditionerPrescription(OwningPreconditionerPrescription &&) noexcept = default; OwningPreconditionerPrescription &operator=(OwningPreconditionerPrescription &&) = delete; std::unique_ptr marker; }; template 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 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 m_marker; }; template [[nodiscard]] auto prepareStellarPreconditioner( OwningPreconditionerPrescription prescription, const Problem &problem ) { return OwningPreparedInverse{problem, std::move(prescription.marker)}; } struct BorrowingPreconditionerPrescription final : mean_field::preconditioning::StellarPreconditionerPrescriptionTag { }; template [[nodiscard]] auto prepareStellarPreconditioner( BorrowingPreconditionerPrescription, const Problem & ) -> OwningPreparedInverse &; [[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; const double centralDensity = std::numbers::pi_v * 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; const double centralDensity = std::numbers::pi_v * 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; 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 * 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 [[nodiscard]] mean_field::seed::RadialProfile generateRadialProfile( const Model &, const ImmovableRadialSeed & ); template concept HasEvaluate = requires(Candidate &candidate) { candidate.evaluate(); }; template concept ExposesFiniteElementModel = requires(const Candidate &candidate) { candidate.finiteElementModel(); }; template concept ExposesStructureManifest = requires(const Candidate &candidate) { candidate.manifest(); }; template concept ExposesMutableMFEMState = requires(const Candidate &candidate) { { candidate.state() } -> std::same_as; }; template concept ExposesStructureStateView = requires(const Candidate &candidate) { candidate.stateView(); }; template concept ExposesOwnedProblem = requires(const Candidate &candidate) { candidate.problem(); } || requires(const Candidate &candidate) { candidate.GetProblem(); }; template concept ReadsCommunicatorFromRvalue = requires(Candidate &&candidate) { std::move(candidate).communicator(); }; template concept ReadsProblemCommunicatorFromRvalue = requires(Candidate &&candidate) { std::move(candidate).GetCommunicator(); }; template concept RestartableFromRvalue = requires(Structure &&structure, Prescription prescription, Backend backend) { mean_field::solver::make(std::move(structure), std::move(prescription), std::move(backend)); }; template concept RestartableFromLvalue = requires(Structure &structure, Prescription prescription, Backend backend) { mean_field::solver::make(structure, std::move(prescription), std::move(backend)); }; template 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 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 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); STATIC_CHECK(solver::LinearBackendConfiguration); STATIC_CHECK_FALSE(solver::LinearBackendConfiguration); STATIC_CHECK_FALSE(solver::LinearBackendConfiguration); STATIC_CHECK_FALSE(solver::LinearBackendConfiguration); STATIC_CHECK(solver::LinearPreconditionerApplicationContractAvailable); STATIC_CHECK(solver::LinearPreconditionerApplicationContractAvailable); STATIC_CHECK_FALSE(solver::LinearPreconditionerApplicationContractAvailable); STATIC_CHECK_FALSE(solver::LinearPreconditionerApplicationContractAvailable); STATIC_CHECK_FALSE(solver::LinearPreconditionerApplicationContractAvailable); STATIC_CHECK_FALSE(solver::LinearPreconditionerApplicationContractAvailable); STATIC_CHECK_FALSE(solver::LinearPreconditionerApplicationContractAvailable); STATIC_CHECK_FALSE(solver::LinearPreconditionerApplicationContractAvailable); STATIC_CHECK_FALSE(solver::LinearPreconditionerApplicationContractAvailable); STATIC_CHECK_FALSE( solver::LinearPreconditionerApplicationContractAvailable< BackendContractCarrier> ); STATIC_CHECK_FALSE( solver::LinearPreconditionerApplicationContractAvailable< BackendContractCarrier> ); STATIC_CHECK_FALSE( solver::LinearPreconditionerApplicationContractAvailable< BackendContractCarrier> ); STATIC_CHECK_FALSE( solver::LinearPreconditionerApplicationContractAvailable< BackendContractCarrier> ); STATIC_CHECK(solver::LinearBackendPreconditionerCompatible); STATIC_CHECK(solver::LinearBackendPreconditionerCompatible); STATIC_CHECK_FALSE(solver::LinearBackendPreconditionerCompatible); using DefaultModel = std::remove_cvref_t; using DefaultProblem = equilibrium::StellarEquilibriumProblem; using DefaultPhysicalInverse = preconditioning::PreparedStellarInverseType; using DefaultNormalizedInverse = normalization::NormalizedStellarPreconditioner; STATIC_CHECK(solver::LinearPreconditionerApplicationContractAvailable); STATIC_CHECK( solver::linearPreconditionerApplicationContract == preconditioning::ApplicationContract::stationary_linear ); STATIC_CHECK(solver::LinearPreconditionerApplicationContractAvailable); STATIC_CHECK( solver::linearPreconditionerApplicationContract == preconditioning::ApplicationContract::stationary_linear ); STATIC_CHECK(solver::LinearBackendPreconditionerCompatible); STATIC_CHECK( solver::StellarEquilibriumContextConfiguration< DefaultModel, equilibrium::StellarDiscretization, preconditioning::DefaultStellarPreconditioner, StationaryBackend> ); STATIC_CHECK(solver::LinearBackendConfiguration); STATIC_CHECK_FALSE(solver::LinearBackendRuntimeAvailableFor); IdentityOperator operation(4); FlexibleInverse inverse(4); auto prepared = prepareLinearBackend(FlexibleBackend{}, operation, inverse, MPI_COMM_WORLD); using Prepared = std::remove_cvref_t; STATIC_CHECK(solver::PreparedLinearBackendFor); STATIC_CHECK_FALSE(std::copy_constructible); STATIC_CHECK_FALSE(std::move_constructible); 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::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::quiet_NaN()}.Validate(), std::invalid_argument ); CHECK_THROWS_AS( solver::LinearSolveControl{.absoluteTolerance = std::numeric_limits::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::quiet_NaN()), std::invalid_argument); CHECK_THROWS_AS(control.ConvergenceThreshold(std::numeric_limits::infinity()), std::invalid_argument); CHECK_THROWS_AS( solver::LinearSolveControl{.relativeTolerance = std::numeric_limits::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; STATIC_CHECK(std::move_constructible); STATIC_CHECK_FALSE(std::copy_constructible); STATIC_CHECK_FALSE(std::is_move_assignable_v); STATIC_CHECK_FALSE(std::constructible_from); STATIC_CHECK_FALSE(ReadsCommunicatorFromRvalue); STATIC_CHECK( std::same_as> ); auto probe = std::make_shared(); { auto model = makeFixedCentralDensityAngularMomentumModel(); using Model = std::remove_cvref_t; using Problem = equilibrium::StellarEquilibriumProblem; STATIC_CHECK(Problem::generatedRotationProviderCount == 1); STATIC_CHECK_FALSE(ReadsProblemCommunicatorFromRvalue); auto context = solver::makeContext( std::move(model), std::move(discretization), preconditioning::makePreconditioner(), FlexibleBackend{probe, true} ); using Context = std::remove_cvref_t; STATIC_CHECK_FALSE(std::move_constructible); STATIC_CHECK_FALSE(std::copy_constructible); STATIC_CHECK_FALSE(HasEvaluate); REQUIRE(context.isReady()); CHECK_FALSE(context.hasActiveSolver()); { auto borrowingSolver = solver::make(context, solver::nonlinear::Newton{}); STATIC_CHECK_FALSE(std::move_constructible>); STATIC_CHECK_FALSE(std::copy_constructible>); STATIC_CHECK(HasEvaluate>); REQUIRE(borrowingSolver.isReady()); CHECK(context.hasActiveSolver()); } CHECK_FALSE(context.hasActiveSolver()); CHECK_FALSE(discretization.isCurrent()); STATIC_CHECK_FALSE(ExposesFiniteElementModel); 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; using Discretization = equilibrium::StellarDiscretization; using Problem = equilibrium::StellarEquilibriumProblem; STATIC_CHECK(preconditioning::StellarPreconditionerPrescription); STATIC_CHECK(preconditioning::PreparedStellarInverseFor, Problem>); STATIC_CHECK(preconditioning::StellarPreconditionerRuntimeAvailableFor); STATIC_CHECK_FALSE( solver::StellarEquilibriumContextConfiguration< Model, Discretization, OwningPreconditionerPrescription, StationaryBackend> ); STATIC_CHECK(preconditioning::StellarPreconditionerPrescription); STATIC_CHECK_FALSE( preconditioning::StellarPreconditionerRuntimeAvailableFor ); } 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; using FixedDensityModel = std::remove_cvref_t; STATIC_CHECK_FALSE(solver::DefaultStellarEquilibriumInitialStateAvailableFor); STATIC_CHECK(solver::DefaultStellarEquilibriumInitialStateAvailableFor); auto finiteElements = makeFiniteElements(); auto probe = std::make_shared(); 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; using Discretization = equilibrium::StellarDiscretization; using Prescription = preconditioning::DefaultStellarPreconditioner; using Projected = seed::ProjectedEquilibriumState; STATIC_CHECK_FALSE(solver::StellarEquilibriumInitialStateFor); STATIC_CHECK(seed::RadialSeedStrategyFor); STATIC_CHECK_FALSE(std::move_constructible); STATIC_CHECK_FALSE(solver::StellarEquilibriumInitialStateFor); STATIC_CHECK_FALSE(HighLevelSeedAccepted); STATIC_CHECK(ExplicitSeedOverloadsAccepted); STATIC_CHECK_FALSE( HighLevelSeedAccepted ); STATIC_CHECK(DefaultSeedWithRotationAccepted); using GeneratedRotationModel = std::remove_cvref_t; STATIC_CHECK_FALSE( DefaultSeedWithRotationAccepted ); } 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; using Discretization = equilibrium::StellarDiscretization; using Problem = equilibrium::StellarEquilibriumProblem; using Structure = equilibrium::StellarStructure; using Checkpoint = equilibrium::StellarCheckpoint; using StructureView = equilibrium::StellarStructureView; using CheckpointView = equilibrium::StellarCheckpointView; using Prescription = preconditioning::DefaultStellarPreconditioner; STATIC_CHECK(std::move_constructible); STATIC_CHECK(std::is_nothrow_move_constructible_v); STATIC_CHECK_FALSE(std::copy_constructible); STATIC_CHECK_FALSE(std::is_move_assignable_v); STATIC_CHECK_FALSE(std::default_initializable); STATIC_CHECK_FALSE(std::default_initializable); STATIC_CHECK(std::copy_constructible); STATIC_CHECK(std::copy_constructible); STATIC_CHECK(std::same_as().state()), std::span>); STATIC_CHECK( std::same_as< decltype(std::declval().stateDescriptors()), std::span> ); STATIC_CHECK(std::same_as().model()), const Model &>); STATIC_CHECK(std::same_as().communicator()), MPI_Comm>); STATIC_CHECK_FALSE(ReadsCommunicatorFromRvalue); STATIC_CHECK(std::same_as().valid()), bool>); STATIC_CHECK( std::same_as< decltype(std::declval().prescribedRotation()), std::optional> ); STATIC_CHECK(std::same_as().rotation()), physics::RigidRotation>); STATIC_CHECK(std::same_as().capture()), Structure>); STATIC_CHECK(std::same_as().capture()), Checkpoint>); STATIC_CHECK_FALSE(ExposesStructureManifest); STATIC_CHECK_FALSE(ExposesMutableMFEMState); STATIC_CHECK_FALSE(ExposesStructureStateView); STATIC_CHECK_FALSE(ExposesFiniteElementModel); STATIC_CHECK_FALSE(ExposesOwnedProblem); STATIC_CHECK_FALSE(RestartableFromRvalue); STATIC_CHECK_FALSE(RestartableFromLvalue); } 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(); seed::StellarEquilibriumProjectionOptions options; options.surfaceRadiusRelativeTolerance = std::numeric_limits::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); } }