#include #include #include #include #include #include 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 concept PreparesWithGeneratedRotation = requires( Problem &problem, const mfem::Vector &state, const mean_field::operators::StellarEquilibriumDependencies &dependencies ) { problem.Prepare(state, dependencies); }; template 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); STATIC_CHECK( std::same_as< typename std::remove_cvref_t::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; const double centralDensity = std::numbers::pi_v * 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; using Preconditioner = std::remove_cvref_t; STATIC_CHECK(Problem::hasFixedAngularMomentum); STATIC_CHECK_FALSE(Problem::hasFixedCentralDensity); STATIC_CHECK(PreparesWithGeneratedRotation); STATIC_CHECK_FALSE(PreparesWithPrescribedRotation); STATIC_CHECK(Problem::FormType::value_block_count == 7); STATIC_CHECK(Problem::FormType::residual_block_count == 7); STATIC_CHECK(preconditioning::PreconditionerComponent); 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().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); STATIC_CHECK(preconditioning::backend::ArnoldiAdmissible); 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); STATIC_CHECK_FALSE(preconditioning::backend::ArnoldiAdmissible); CHECK(preconditioner.structureComponent().materialSurfaceComponent().diagonalOptions().relativeFloor == 1.0e-10); CHECK( preconditioner.structureComponent().gravityComponent().potentialSchurBackend().application.maximumCycles == 20 ); }