#include #include #include #include #include #include #include #include #include #include #include #include import mean_field; import test_helpers; namespace { bool vector_is_finite(const mfem::Vector &vector) { for (int index = 0; index < vector.Size(); ++index) { if (!std::isfinite(vector(index))) { return false; } } return true; } mfem::Vector make_deterministic_vector( const int size, const double phase ) { mfem::Vector vector(size); for (int index = 0; index < size; ++index) { const double coordinate = static_cast(index + 1); vector(index) = std::sin(phase + 0.017 * coordinate) + 0.25 * std::cos(0.031 * coordinate); } return vector; } void require_all_ranks( const bool localCondition, const MPI_Comm communicator, const char *description ) { int rank = 0; int size = 0; MPI_Comm_rank(communicator, &rank); MPI_Comm_size(communicator, &size); const int localFailure = localCondition ? size : rank; int firstFailure = size; const int result = MPI_Allreduce(&localFailure, &firstFailure, 1, MPI_INT, MPI_MIN, communicator); REQUIRE(result == MPI_SUCCESS); CAPTURE(description, localCondition, firstFailure); REQUIRE(firstFailure == size); } double global_dot( const mfem::Vector &left, const mfem::Vector &right, const MPI_Comm communicator ) { require_all_ranks(left.Size() == right.Size(), communicator, "global dot-product vector sizes"); const double local = left * right; double global = 0.0; REQUIRE(MPI_Allreduce(&local, &global, 1, MPI_DOUBLE, MPI_SUM, communicator) == MPI_SUCCESS); return global; } double global_norm( const mfem::Vector &vector, const MPI_Comm communicator ) { return std::sqrt(global_dot(vector, vector, communicator)); } [[nodiscard]] mean_field::operators::StellarEquilibriumDependencies make_stellar_dependencies(const std::uint64_t revision = 1) { return { .discretization = {.identity = 16101, .revision = 1}, .density = {.identity = 16103, .revision = revision}, .surfaceDeformation = {.identity = 16111, .revision = revision}, .gravityGradient = {.identity = 16127, .revision = revision}, .gravityPotential = {.identity = 16139, .revision = revision}, .enthalpy = {.identity = 16141, .revision = revision}, .bernoulliConstant = {.identity = 16183, .revision = revision}, .rotation = {.identity = 16187, .revision = revision}, .targetMass = {.identity = 16189, .revision = 1} }; } class FixedCentralDensityPhysicalCoreStub final : public mfem::Operator { public: FixedCentralDensityPhysicalCoreStub() : mfem::Operator(0) { } void Mult( const mfem::Vector &, mfem::Vector & ) const override { } const mean_field::operators::StellarEquilibriumLayout &GetLayout() const; mean_field::operators::PreparedStellarEquilibriumReport Prepare( const mfem::Vector &, const mean_field::operators::StellarEquilibriumDependencies &, const mean_field::physics::RigidRotation & ); void BuildResidual(mfem::Vector &) const; bool IsPrepared() const noexcept; mean_field::operators::RootConstraintReport GetFixedMassReport() const; const mean_field::operators::StellarEquilibriumDependencies &GetDependencies() const; const mean_field::operators::StellarEquilibriumDependencyStamp &GetGeneratedDisplacementDependency() const; const mean_field::operators::PreparedPressureSurfaceConstraint &GetSurfaceConstraintOperator() const; }; static_assert(mean_field::operators::PreparedStellarEquilibriumPhysicalCore); struct FixedCentralDensityStateView final { const mfem::Vector &enthalpy; const mfem::Vector &phase; const mfem::Vector &block(const mean_field::utils::blocks::enthalpy::specific &) const noexcept { return enthalpy; } const mfem::Vector & block(const mean_field::utils::blocks::fixed_central_density::central_value &) const noexcept { return phase; } }; void check_rank_consistent_scalar( const double value, const MPI_Comm communicator, const double relativeTolerance = 2.0e-13 ) { double minimum = 0.0; double maximum = 0.0; REQUIRE(MPI_Allreduce(&value, &minimum, 1, MPI_DOUBLE, MPI_MIN, communicator) == MPI_SUCCESS); REQUIRE(MPI_Allreduce(&value, &maximum, 1, MPI_DOUBLE, MPI_MAX, communicator) == MPI_SUCCESS); CAPTURE(value, minimum, maximum); CHECK(std::isfinite(minimum)); CHECK(std::isfinite(maximum)); CHECK(std::abs(maximum - minimum) <= relativeTolerance * std::max({1.0, std::abs(minimum), std::abs(maximum)})); } class DistributedDiagonalOperator final : public mfem::Operator { public: DistributedDiagonalOperator() : mfem::Operator(2) { } void Mult( const mfem::Vector &input, mfem::Vector &output ) const override { output.SetSize(2); output(0) = input(0); output(1) = 2.0 * input(1); } }; class DistributedIdentityInverse final : public mfem::Solver { public: static constexpr mean_field::preconditioning::ApplicationContract applicationContract = mean_field::preconditioning::ApplicationContract::flexible; DistributedIdentityInverse() : mfem::Solver(2) { } void SetOperator(const mfem::Operator &operation) override { if (operation.Height() != 2 || operation.Width() != 2) { throw std::invalid_argument("The distributed identity inverse requires a two-by-two operator."); } } void Mult( const mfem::Vector &input, mfem::Vector &output ) const override { output = input; } }; } // namespace TEST_CASE( "MPI MFEM FGMRES Reports Global Warm-Start And Restart Diagnostics Consistently", "[mpi][distributed][solver][fgmres]" ) { using namespace mean_field; const MPI_Comm communicator = MPI_COMM_WORLD; int rank = 0; REQUIRE(MPI_Comm_rank(communicator, &rank) == MPI_SUCCESS); DistributedDiagonalOperator operation; DistributedIdentityInverse inverse; auto backend = solver::linear::prepareLinearBackend( solver::linear::FGMRES({.restartLength = 1, .printLevel = -1}), operation, inverse, communicator ); require_all_ranks(backend.IsReady(), communicator, "distributed FGMRES backend readiness"); mfem::Vector rightHandSide(2); rightHandSide(0) = 1.0 + static_cast(rank); rightHandSide(1) = 2.0 + 0.5 * static_cast(rank); mfem::Vector correction(2); correction(0) = 0.125 * static_cast(rank + 1); correction(1) = -0.25 * static_cast(rank + 1); mfem::Vector initialAction(2); operation.Mult(correction, initialAction); mfem::Vector initialResidual(rightHandSide); initialResidual -= initialAction; const double expectedInitialResidualNorm = global_norm(initialResidual, communicator); const solver::LinearSolveControl control{ .relativeTolerance = 1.0e-10, .absoluteTolerance = 1.0e-13, .maximumIterations = 100 }; const auto report = backend.Solve(rightHandSide, correction, control); const auto checkConsistentInteger = [&](const int value) { int minimum = 0; int maximum = 0; REQUIRE(MPI_Allreduce(&value, &minimum, 1, MPI_INT, MPI_MIN, communicator) == MPI_SUCCESS); REQUIRE(MPI_Allreduce(&value, &maximum, 1, MPI_INT, MPI_MAX, communicator) == MPI_SUCCESS); CHECK(minimum == maximum); }; checkConsistentInteger(static_cast(report.status)); checkConsistentInteger(report.iterations); checkConsistentInteger(report.restarts); check_rank_consistent_scalar(report.initialResidualNorm, communicator); check_rank_consistent_scalar(report.reportedResidualNorm, communicator); check_rank_consistent_scalar(report.trueResidualNorm, communicator); check_rank_consistent_scalar(report.relativeTrueResidualNorm, communicator); require_all_ranks(report.Converged(), communicator, "distributed FGMRES convergence"); mfem::Vector finalAction(2); operation.Mult(correction, finalAction); mfem::Vector trueResidual(rightHandSide); trueResidual -= finalAction; const double expectedTrueResidualNorm = global_norm(trueResidual, communicator); const double rightHandSideNorm = global_norm(rightHandSide, communicator); CHECK(report.status == solver::LinearSolveStatus::converged); CHECK(report.iterations > 1); CHECK(report.iterations <= control.maximumIterations); CHECK(report.restarts == report.iterations - 1); CHECK( std::abs(report.initialResidualNorm - expectedInitialResidualNorm) <= 2.0e-13 * std::max(1.0, expectedInitialResidualNorm) ); CHECK( std::abs(report.trueResidualNorm - expectedTrueResidualNorm) <= 2.0e-13 * std::max(1.0, expectedTrueResidualNorm) ); CHECK(report.trueResidualNorm <= control.ConvergenceThreshold(rightHandSideNorm)); CHECK( std::abs(report.relativeTrueResidualNorm - report.trueResidualNorm / rightHandSideNorm) <= 2.0e-13 * std::max(1.0, report.relativeTrueResidualNorm) ); mfem::Vector invalidRightHandSide(rightHandSide); if (rank == 0) { invalidRightHandSide(0) = std::numeric_limits::quiet_NaN(); } mfem::Vector rejectedCorrection(correction); bool collectivelyRejected = false; try { (void)backend.Solve(invalidRightHandSide, rejectedCorrection, control); } catch (const std::invalid_argument &) { collectivelyRejected = true; } require_all_ranks(collectivelyRejected, communicator, "collective non-finite FGMRES input rejection"); auto rankDependentBackend = solver::linear::prepareLinearBackend( solver::linear::FGMRES({.restartLength = rank + 1, .printLevel = -1}), operation, inverse, communicator ); mfem::Vector rankDependentCorrection(2); rankDependentCorrection = 0.0; bool rankDependentBackendRejected = false; try { (void)rankDependentBackend.Solve(rightHandSide, rankDependentCorrection, control); } catch (const std::invalid_argument &) { rankDependentBackendRejected = true; } require_all_ranks(rankDependentBackendRejected, communicator, "rank-dependent FGMRES option rejection"); auto rankDependentControl = control; rankDependentControl.maximumIterations = control.maximumIterations + rank; rankDependentCorrection = 0.0; bool rankDependentControlRejected = false; try { (void)backend.Solve(rightHandSide, rankDependentCorrection, rankDependentControl); } catch (const std::invalid_argument &) { rankDependentControlRejected = true; } require_all_ranks(rankDependentControlRejected, communicator, "rank-dependent FGMRES solve-control rejection"); } TEST_CASE( "MPI Observer Exceptions Cause Rank-Coordinated Solver Shutdown", "[mpi][distributed][solver][newton][observer][exception-safety]" ) { using namespace mean_field; const MPI_Comm communicator = MPI_COMM_WORLD; int rank = 0; int size = 0; REQUIRE(MPI_Comm_rank(communicator, &rank) == MPI_SUCCESS); REQUIRE(MPI_Comm_size(communicator, &size) == MPI_SUCCESS); REQUIRE(size >= 2); auto observer = solver::nonlinear::makeObserver( [rank](const solver::nonlinear::BeforeIteration &) { if (rank == 1) { throw std::runtime_error("rank-local before observer failure"); } }, [rank](const solver::nonlinear::AfterLineSearchTrial &) { if (rank == 1) { throw std::runtime_error("rank-local trial observer failure"); } }, [rank](const solver::nonlinear::AfterIteration &) { if (rank == 1) { throw std::runtime_error("rank-local after observer failure"); } } ); const auto requireCoordinatedEscape = [&](auto &&invoke, const char *description) { bool caught = false; try { invoke(); } catch (const std::runtime_error &) { caught = true; } require_all_ranks(caught, communicator, description); const int localHandshake = rank + 1; int globalHandshake = 0; REQUIRE(MPI_Allreduce(&localHandshake, &globalHandshake, 1, MPI_INT, MPI_SUM, communicator) == MPI_SUCCESS); CHECK(globalHandshake == size * (size + 1) / 2); }; requireCoordinatedEscape( [&] { solver::nonlinear::detail::InvokeBeforeIteration( observer, solver::nonlinear::BeforeIteration{.communicator = communicator} ); }, "before observer exception escape" ); requireCoordinatedEscape( [&] { solver::nonlinear::detail::InvokeAfterLineSearchTrial( observer, solver::nonlinear::AfterLineSearchTrial{.communicator = communicator} ); }, "line-search observer exception escape" ); requireCoordinatedEscape( [&] { solver::nonlinear::detail::InvokeAfterIteration( observer, solver::nonlinear::AfterIteration{.communicator = communicator} ); }, "after observer exception escape" ); } TEST_CASE( "MPI Runtime Preserves World And Split Communicator Membership", "[mpi][distributed][unit]" ) { int rank = 0; int size = 1; MPI_Comm_rank(MPI_COMM_WORLD, &rank); MPI_Comm_size(MPI_COMM_WORLD, &size); std::vector ranks(static_cast(size), -1); MPI_Allgather(&rank, 1, MPI_INT, ranks.data(), 1, MPI_INT, MPI_COMM_WORLD); CHECK(size >= 2); for (int expected = 0; expected < size; ++expected) { CHECK(ranks[expected] == expected); } MPI_Comm parity_communicator = MPI_COMM_NULL; MPI_Comm_split(MPI_COMM_WORLD, rank % 2, rank, &parity_communicator); int parity_size = 0; MPI_Comm_size(parity_communicator, &parity_size); const int expected_parity_size = (size + 1 - rank % 2) / 2; CHECK(parity_size == expected_parity_size); MPI_Comm_free(&parity_communicator); } TEST_CASE( "MPI FEM Setup Partitions Every Element Exactly Once", "[mpi][distributed][mesh][integration]" ) { const mean_field::utils::Args args = test_utils::setup_args(); const mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); const long long local_elements = f.mesh->GetNE(); long long global_elements = 0; long long minimum_elements = 0; MPI_Allreduce(&local_elements, &global_elements, 1, MPI_LONG_LONG, MPI_SUM, f.mesh->GetComm()); MPI_Allreduce(&local_elements, &minimum_elements, 1, MPI_LONG_LONG, MPI_MIN, f.mesh->GetComm()); CHECK(global_elements == f.smesh.mesh->GetNE()); CHECK(minimum_elements > 0); CHECK(f.logicalReferenceMesh->GetNE() == f.mesh->GetNE()); } TEST_CASE( "MPI Fixed Angular Momentum Produces One Consistent Global Invariant Row", "[mpi][distributed][fixed-angular-momentum][physics][jacobian]" ) { using namespace mean_field; const auto args = test_utils::setup_args(); fem::FEM finiteElements = fem::setup_fem(args.mesh_file, args, 0); require_all_ranks(finiteElements.okay(), MPI_COMM_WORLD, "fixed-angular-momentum FEM setup"); const MPI_Comm communicator = finiteElements.mesh->GetComm(); mfem::ParGridFunction densityField(finiteElements.densityFes.get()); mfem::ConstantCoefficient densityCoefficient(1.23); densityField.ProjectCoefficient(densityCoefficient); mfem::Vector densityTrue; densityField.GetTrueDofs(densityTrue); mfem::Vector displacementTrue(finiteElements.displacementFes->GetTrueVSize()); displacementTrue = 0.0; finiteElements.displacement->SetFromTrueDofs(displacementTrue); mfem::Vector gravityGradientTrue(finiteElements.gravityFluxFes->GetTrueVSize()); mfem::Vector gravityPotentialTrue(finiteElements.gravityPotentialFes->GetTrueVSize()); gravityGradientTrue = 0.0; gravityPotentialTrue = 0.0; operators::context::gravity_field::GravityFieldLinearizationContext gravityContext( finiteElements, *finiteElements.domainMapperStateless ); gravityContext.Prepare( {.density = gravityContext.GetDensityMap().gather(densityTrue), .displacement = gravityContext.GetDisplacementMap().gather(displacementTrue), .gravity_gradient = gravityContext.GetGravityGradientMap().gather(gravityGradientTrue), .gravity_potential = gravityContext.GetGravityPotentialMap().gather(gravityPotentialTrue)}, {.discretization = {.value = 2}, .displacement = {.value = 3}, .density = {.value = 5}, .gravity_gradient = {.value = 7}, .gravity_potential = {.value = 11}} ); constexpr double targetAngularMomentum = 0.37; constexpr double angularVelocity = 0.61; operators::PreparedAngularMomentumOperator invariant( finiteElements, *finiteElements.domainMapperStateless, gravityContext, models::compileConstraint( integral::FixedAngularMomentum({.Jtotal = dimensions::AngularMomentumValue{targetAngularMomentum}}) ) ); const operators::AngularMomentumDependencies dependencies{ .discretization = {.identity = 101, .revision = 2}, .density = {.identity = 103, .revision = 5}, .displacement = {.identity = 107, .revision = 3}, .rotation = {.identity = 109, .revision = 13} }; const auto preparation = invariant.Prepare(angularVelocity, dependencies); CHECK(preparation.rebuiltStaticPlan); CHECK(preparation.refreshedGeometry); CHECK(preparation.refreshedDensity); CHECK(preparation.updatedAngularVelocity); CHECK(preparation.assembledResidual); const double independentMoment = analysis::get_moment_of_inertia(finiteElements, densityField); const double preparedMoment = invariant.GetMomentOfInertia(); const double comparisonScale = std::max({std::abs(independentMoment), std::abs(preparedMoment), 1.0e-300}); CHECK(std::abs(preparedMoment - independentMoment) / comparisonScale <= 3.0e-13); double minimumMoment = 0.0; double maximumMoment = 0.0; MPI_Allreduce(&preparedMoment, &minimumMoment, 1, MPI_DOUBLE, MPI_MIN, finiteElements.mesh->GetComm()); MPI_Allreduce(&preparedMoment, &maximumMoment, 1, MPI_DOUBLE, MPI_MAX, finiteElements.mesh->GetComm()); CHECK(std::abs(maximumMoment - minimumMoment) / comparisonScale <= 2.0e-15); mfem::Vector residual; invariant.BuildResidual(residual); require_all_ranks(residual.Size() == 1, communicator, "fixed-angular-momentum residual size"); CHECK( std::abs(residual(0) - (angularVelocity * independentMoment - targetAngularMomentum)) / std::max({std::abs(residual(0)), std::abs(angularVelocity * independentMoment), 1.0}) <= 3.0e-13 ); mfem::Vector densityAction; invariant.ApplyDensityJacobianAction(gravityContext.GetDensityMap().gather(densityTrue), densityAction); require_all_ranks(densityAction.Size() == 1, communicator, "fixed-angular-momentum density action size"); CHECK(std::abs(densityAction(0) - angularVelocity * preparedMoment) / comparisonScale <= 3.0e-13); constexpr double angularVelocityVariation = -0.29; mfem::Vector angularVelocityAction; invariant.ApplyAngularVelocityJacobianAction(angularVelocityVariation, angularVelocityAction); require_all_ranks(angularVelocityAction.Size() == 1, communicator, "fixed-angular-momentum rotation action size"); CHECK(std::abs(angularVelocityAction(0) - angularVelocityVariation * preparedMoment) / comparisonScale <= 2.0e-15); int rank = 0; REQUIRE(MPI_Comm_rank(communicator, &rank) == MPI_SUCCESS); auto rejectedDependencies = dependencies; ++rejectedDependencies.rotation.revision; const double rankLocalAngularVelocity = rank == 0 ? std::numeric_limits::quiet_NaN() : angularVelocity; const auto synchronizedRejection = invariant.TryPrepare(rankLocalAngularVelocity, rejectedDependencies); require_all_ranks( !synchronizedRejection.has_value(), communicator, "fixed-angular-momentum synchronized trial rejection" ); CHECK( synchronizedRejection.error().reason == operators::AngularMomentumPreparationRejectionReason::non_finite_angular_velocity ); CHECK_FALSE(invariant.IsPrepared()); ++rejectedDependencies.rotation.revision; const auto recovered = invariant.TryPrepare(angularVelocity, rejectedDependencies); require_all_ranks(recovered.has_value(), communicator, "fixed-angular-momentum recovery after rejection"); CHECK(invariant.IsPrepared()); } TEST_CASE( "MPI Fixed Central Density Coordinates Reject Collectively Without Unwinding", "[mpi][distributed][fixed-central-density][candidate-rejection]" ) { using namespace mean_field; const utils::Args arguments = test_utils::setup_args(); fem::FEM finiteElements = fem::setup_fem(arguments.mesh_file, arguments, 0); require_all_ranks(finiteElements.okay(), MPI_COMM_WORLD, "fixed-central-density FEM setup"); const MPI_Comm communicator = finiteElements.mesh->GetComm(); require_all_ranks( finiteElements.domainMapperStateless != nullptr, communicator, "fixed-central-density mapper setup" ); 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}}), constraint::FixedCentralDensity({.RhoC = dimensions::DensityValue{1.0}}) ); FixedCentralDensityPhysicalCoreStub physicalCore; using Runtime = operators::detail::FixedCentralDensityRuntime>; Runtime runtime(finiteElements, *finiteElements.domainMapperStateless, physicalCore, model); mfem::Vector enthalpy(runtime.constraint().GetCenterDof().field_size()); enthalpy = 1.0; mfem::Vector phase(1); phase = 0.03; const FixedCentralDensityStateView state{enthalpy, phase}; const auto initial = runtime.TryPrepareAfterPhysical(state, make_stellar_dependencies(1), physicalCore); require_all_ranks(initial.has_value(), communicator, "initial fixed-central-density preparation"); int rank = 0; REQUIRE(MPI_Comm_rank(communicator, &rank) == MPI_SUCCESS); phase(0) = rank == 0 ? 0.04 : 0.03; const auto inconsistent = runtime.TryPrepareAfterPhysical(state, make_stellar_dependencies(2), physicalCore); require_all_ranks(!inconsistent.has_value(), communicator, "fixed-central-density inconsistent scalar rejection"); CHECK(inconsistent.error().reason == operators::StellarEquilibriumPreparationRejectionReason::inadmissible_physics); CHECK(inconsistent.error().stage == operators::StellarEquilibriumPreparationStage::model_specification); require_all_ranks(!runtime.IsPrepared(), communicator, "fixed-central-density inconsistent preparation state"); phase(0) = rank == 0 ? std::numeric_limits::quiet_NaN() : 0.03; const auto rejected = runtime.TryPrepareAfterPhysical(state, make_stellar_dependencies(3), physicalCore); require_all_ranks(!rejected.has_value(), communicator, "fixed-central-density synchronized trial rejection"); CHECK(rejected.error().reason == operators::StellarEquilibriumPreparationRejectionReason::non_finite_physics); CHECK(rejected.error().stage == operators::StellarEquilibriumPreparationStage::model_specification); require_all_ranks(!runtime.IsPrepared(), communicator, "fixed-central-density rejected preparation state"); phase = 0.03; const auto recovered = runtime.TryPrepareAfterPhysical(state, make_stellar_dependencies(4), physicalCore); require_all_ranks(recovered.has_value(), communicator, "fixed-central-density recovery after rejection"); require_all_ranks(runtime.IsPrepared(), communicator, "fixed-central-density recovered preparation state"); } TEST_CASE( "MPI Material Surface Regularization Rejects Rank-Local Non-Finite Data Collectively", "[mpi][distributed][preconditioner][material-surface][exception-safety]" ) { using namespace mean_field; const MPI_Comm communicator = MPI_COMM_WORLD; int rank = 0; REQUIRE(MPI_Comm_rank(communicator, &rank) == MPI_SUCCESS); mfem::Vector diagonal(1); diagonal(0) = rank == 0 ? std::numeric_limits::quiet_NaN() : 1.0; bool rejectedCollectively = false; try { (void)preconditioning::detail::regularizeMaterialSurfaceDiagonal( diagonal, preconditioning::MaterialSurfaceDiagonalOptions{}, communicator ); } catch (const std::invalid_argument &) { rejectedCollectively = true; } require_all_ranks(rejectedCollectively, communicator, "material-surface non-finite diagonal collective rejection"); diagonal = 0.0; const auto recovered = preconditioning::detail::regularizeMaterialSurfaceDiagonal( diagonal, preconditioning::MaterialSurfaceDiagonalOptions{}, communicator ); CHECK(std::isfinite(diagonal(0))); CHECK(diagonal(0) == recovered.appliedFloor); std::uint64_t communicatorSize = 0; int size = 0; REQUIRE(MPI_Comm_size(communicator, &size) == MPI_SUCCESS); communicatorSize = static_cast(size); CHECK(recovered.regularizedEntries == communicatorSize); } TEST_CASE( "MPI Density Volume Context Forwards The Prepared Global Mass Integral", "[mpi][distributed][integral-context][physics-extension]" ) { using namespace mean_field; const utils::Args arguments = test_utils::setup_args(); fem::FEM finiteElements = fem::setup_fem(arguments.mesh_file, arguments, 0); require_all_ranks(finiteElements.okay(), MPI_COMM_WORLD, "density-volume context FEM setup"); const MPI_Comm communicator = finiteElements.mesh->GetComm(); constexpr double densityValue = 1.23; mfem::ParGridFunction densityField(finiteElements.densityFes.get()); mfem::ConstantCoefficient densityCoefficient(densityValue); densityField.ProjectCoefficient(densityCoefficient); mfem::Vector densityTrue; densityField.GetTrueDofs(densityTrue); mfem::Vector displacementTrue(finiteElements.displacementFes->GetTrueVSize()); displacementTrue = 0.0; finiteElements.displacement->SetFromTrueDofs(displacementTrue); mfem::Vector gravityGradientTrue(finiteElements.gravityFluxFes->GetTrueVSize()); mfem::Vector gravityPotentialTrue(finiteElements.gravityPotentialFes->GetTrueVSize()); gravityGradientTrue = 0.0; gravityPotentialTrue = 0.0; operators::context::gravity_field::GravityFieldLinearizationContext gravityContext( finiteElements, *finiteElements.domainMapperStateless ); const mfem::Vector reducedDensity = gravityContext.GetDensityMap().gather(densityTrue); gravityContext.Prepare( {.density = reducedDensity, .displacement = gravityContext.GetDisplacementMap().gather(displacementTrue), .gravity_gradient = gravityContext.GetGravityGradientMap().gather(gravityGradientTrue), .gravity_potential = gravityContext.GetGravityPotentialMap().gather(gravityPotentialTrue)}, {.discretization = {.value = 2}, .displacement = {.value = 3}, .density = {.value = 5}, .gravity_gradient = {.value = 7}, .gravity_potential = {.value = 11}} ); operators::PreparedMassNormalizationOperator massIntegral( finiteElements, *finiteElements.domainMapperStateless, gravityContext ); massIntegral.Prepare( models::compileConstraint(integral::FixedTotalMass({.Mtotal = dimensions::MassValue{1.0}})), {.discretization = {.identity = 101, .revision = 2}, .density = {.identity = 103, .revision = 5}, .displacement = {.identity = 107, .revision = 3}, .targetMass = {.identity = 109, .revision = 1}} ); class DistributedMassIntegralCore final { public: explicit DistributedMassIntegralCore(const operators::PreparedMassNormalizationOperator &mass) noexcept : m_mass(&mass) { } [[nodiscard]] double ApplyDensityVolumeIntegralDensityAction(const mfem::Vector &direction) const { mfem::Vector action; m_mass->ApplyDensityJacobianAction(direction, action); return action(0); } [[nodiscard]] double ApplyDensityVolumeIntegralSurfaceShapeAction(const mfem::Vector &) const noexcept { return 0.0; } private: const operators::PreparedMassNormalizationOperator *m_mass; }; const DistributedMassIntegralCore testCore{massIntegral}; const stellar::DensityVolumeIntegralContext densityIntegral{testCore}; const double integratedMass = densityIntegral.integrateDensity(reducedDensity).value(); const double linearizedDensityMass = densityIntegral.linearizeDensityIntegral(reducedDensity).value(); const double preparedMass = massIntegral.GetCurrentMass(); const double independentMass = densityValue * analysis::get_mesh_volume(finiteElements); check_rank_consistent_scalar(integratedMass, communicator); check_rank_consistent_scalar(linearizedDensityMass, communicator); check_rank_consistent_scalar(preparedMass, communicator); check_rank_consistent_scalar(independentMass, communicator); const double massScale = std::max({1.0, std::abs(integratedMass), std::abs(independentMass)}); CHECK(std::abs(integratedMass - independentMass) <= 3.0e-12 * massScale); CHECK(std::abs(integratedMass - preparedMass) <= 3.0e-13 * massScale); CHECK(std::abs(linearizedDensityMass - integratedMass) <= 3.0e-13 * massScale); } TEST_CASE( "Single-Rank MPI Assembled Variadic Stellar Root Normalizes And Applies Its Inferred Preconditioner", "[mpi][single-rank][stellar-equilibrium][normalization][preconditioning][integration]" ) { using namespace mean_field; const utils::Args arguments = test_utils::setup_args(); fem::FEM finiteElements = fem::setup_fem(arguments.mesh_file, arguments, 0); require_all_ranks(finiteElements.okay(), MPI_COMM_WORLD, "variadic stellar-root FEM setup"); const MPI_Comm communicator = finiteElements.mesh->GetComm(); constexpr double radius = utils::RADIUS; constexpr double mass = utils::MASS; const double polytropicConstant = 2.0 * utils::G * radius * radius / std::numbers::pi_v; const double centralDensity = std::numbers::pi_v * mass / (4.0 * radius * radius * radius); auto model = model::StellarModel( eos::Polytrope({.n = 1.0, .K = polytropicConstant}), surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}), integral::FixedTotalMass({.Mtotal = dimensions::MassValue{mass}}), integral::FixedAngularMomentum({.Jtotal = dimensions::AngularMomentumValue{0.05}, .axis = {0.0, 0.0, 1.0}}), constraint::FixedCentralDensity({.RhoC = dimensions::DensityValue{centralDensity}}) ); auto problem = equilibrium::discretize( model, equilibrium::makeStellarDiscretization( std::move(finiteElements), normalization::PhysicalRieszDiagonal{dimensions::LengthValue{radius}, utils::G} ) ); auto projected = seed::makeProjectedEquilibriumState( problem, seed::LaneEmden({.centralDensity = dimensions::DensityValue{centralDensity}, .radialSampleCount = 512}) ); auto normalized = normalization::makeNormalizedStellarEquilibriumOperator(problem); using Problem = std::remove_cvref_t; using Form = typename Problem::FormType; constexpr auto massResidualBlock = utils::blocks::get_residual_block
(utils::blocks::fixed_total_mass_constraint.mass_normalization_term); constexpr auto angularMomentumResidualBlock = utils::blocks::get_residual_block(utils::blocks::fixed_angular_momentum_constraint.angular_velocity_term); constexpr auto centralDensityResidualBlock = utils::blocks::get_residual_block(utils::blocks::fixed_central_density_phase.central_value_term); require_all_ranks( problem.StateSize() == problem.EquationSize(), communicator, "variadic stellar-root square layout" ); mfem::Vector normalizedState; normalized.NormalizeState(projected.values, normalizedState); const auto preparation = normalized.Prepare(normalizedState, make_stellar_dependencies(1)); require_all_ranks( preparation.DidAnyWork() && preparation.generatedPhysicalControl && preparation.template specification().generatedRotation && problem.IsPrepared() && normalized.IsPrepared(), communicator, "initial variadic stellar-root preparation" ); // The astronomy-facing integral handle must delegate to the same mapped // physical-volume quadrature and collective reduction as the prepared // mass equation, without exposing the FEM/core objects to extension // physics. Linearity in density gives an independent check of both // public operations on every rank. const auto physicalState = problem.GetManifest().stateView(projected.values); const auto physicalDensity = physicalState.block(utils::blocks::density_field.mass_term); const stellar::DensityVolumeIntegralContext densityIntegral{ problem.GetPhysicalOperator() }; const double integratedMass = densityIntegral.integrateDensity(physicalDensity).value(); const double linearizedMass = densityIntegral.linearizeDensityIntegral(physicalDensity).value(); const double preparedMass = problem.GetPhysicalOperator().GetFixedMassReport().achieved; check_rank_consistent_scalar(integratedMass, communicator); check_rank_consistent_scalar(linearizedMass, communicator); const double massComparisonScale = std::max({std::abs(integratedMass), std::abs(preparedMass), 1.0e-300}); CHECK(std::abs(integratedMass - preparedMass) / massComparisonScale <= 3.0e-13); CHECK(std::abs(linearizedMass - preparedMass) / massComparisonScale <= 3.0e-13); mfem::Vector normalizedResidual; normalized.BuildResidual(normalizedResidual); require_all_ranks( normalizedResidual.Size() == problem.EquationSize(), communicator, "normalized variadic residual size" ); auto residualView = problem.GetManifest().residualView(normalizedResidual); const auto &layout = problem.GetManifest().layout(); const auto &residualFactors = normalized.GetNormalization().ResidualFactors(); const auto massReport = problem.GetPreparedOperator().GetFixedMassReport(); const auto angularMomentumReport = problem.GetPreparedOperator().GetAngularMomentumReport(); const auto centralDensityReport = problem.GetPreparedOperator().GetCentralDensityReport(); const auto checkReportedScalarResidual = [&](const mfem::Vector &block, const double expected) { require_all_ranks(block.Size() == 1, communicator, "reported scalar residual block size"); const double actual = block(0); CAPTURE(actual, expected); CHECK(std::isfinite(expected)); CHECK(std::abs(actual - expected) <= 5.0e-13 * std::max({1.0, std::abs(actual), std::abs(expected)})); check_rank_consistent_scalar(actual, communicator); }; checkReportedScalarResidual( residualView.block(utils::blocks::fixed_total_mass_constraint.mass_normalization_term), massReport.dimensionalResidual * residualFactors(layout.offset(massResidualBlock)) ); checkReportedScalarResidual( residualView.block(utils::blocks::fixed_angular_momentum_constraint.angular_velocity_term), angularMomentumReport.dimensionalResidual * residualFactors(layout.offset(angularMomentumResidualBlock)) ); checkReportedScalarResidual( residualView.block(utils::blocks::fixed_central_density_phase.central_value_term), centralDensityReport.enthalpyResidual * residualFactors(layout.offset(centralDensityResidualBlock)) ); mfem::Vector normalizedDirection = make_deterministic_vector(problem.StateSize(), 0.37); const auto directionState = problem.GetManifest().stateView(normalizedDirection); mfem::Vector massDirection = directionState.block(utils::blocks::fixed_total_mass_constraint.mass_normalization_term); mfem::Vector angularVelocityDirection = directionState.block(utils::blocks::fixed_angular_momentum_constraint.angular_velocity_term); mfem::Vector phaseDirection = directionState.block(utils::blocks::fixed_central_density_phase.central_value_term); require_all_ranks( massDirection.Size() == 1 && angularVelocityDirection.Size() == 1 && phaseDirection.Size() == 1, communicator, "generated scalar direction block sizes" ); massDirection(0) = 0.17; angularVelocityDirection(0) = -0.23; phaseDirection(0) = 0.31; massDirection.SyncAliasMemory(normalizedDirection); angularVelocityDirection.SyncAliasMemory(normalizedDirection); phaseDirection.SyncAliasMemory(normalizedDirection); normalizedDirection /= global_norm(normalizedDirection, communicator); mfem::Vector normalizedAction; normalized.Mult(normalizedDirection, normalizedAction); require_all_ranks( normalizedAction.Size() == problem.EquationSize(), communicator, "normalized variadic Jacobian-action size" ); /* Different dependency revisions are intentional: the state changes in * each difference evaluation, so the distributed physical contexts must * be rebuilt even though all persistent identities remain the same. */ constexpr double differenceStep = 1.0e-5; mfem::Vector plusState(normalizedState); plusState.Add(differenceStep, normalizedDirection); const auto plusPreparation = normalized.Prepare(plusState, make_stellar_dependencies(2)); require_all_ranks(plusPreparation.DidAnyWork(), communicator, "positive finite-difference preparation"); mfem::Vector plusResidual; normalized.BuildResidual(plusResidual); mfem::Vector minusState(normalizedState); minusState.Add(-differenceStep, normalizedDirection); const auto minusPreparation = normalized.Prepare(minusState, make_stellar_dependencies(3)); require_all_ranks(minusPreparation.DidAnyWork(), communicator, "negative finite-difference preparation"); mfem::Vector minusResidual; normalized.BuildResidual(minusResidual); mfem::Vector finiteDifference(plusResidual); finiteDifference -= minusResidual; finiteDifference /= 2.0 * differenceStep; const auto restoredPreparation = normalized.Prepare(normalizedState, make_stellar_dependencies(4)); require_all_ranks( restoredPreparation.DidAnyWork() && normalized.IsPrepared(), communicator, "restored finite-difference preparation" ); mfem::Vector restoredResidual; normalized.BuildResidual(restoredResidual); mfem::Vector restoredResidualDifference(restoredResidual); restoredResidualDifference -= normalizedResidual; const double restoredResidualError = global_norm(restoredResidualDifference, communicator) / std::max( {global_norm(restoredResidual, communicator), global_norm(normalizedResidual, communicator), std::numeric_limits::epsilon()} ); CAPTURE(restoredResidualError); CHECK(restoredResidualError <= 2.0e-12); mfem::Vector finiteDifferenceError(normalizedAction); finiteDifferenceError -= finiteDifference; const double actionNorm = global_norm(normalizedAction, communicator); const double finiteDifferenceNorm = global_norm(finiteDifference, communicator); const double completeDifferenceError = global_norm(finiteDifferenceError, communicator) / std::max({actionNorm, finiteDifferenceNorm, std::numeric_limits::epsilon()}); CAPTURE(actionNorm, finiteDifferenceNorm, completeDifferenceError); CHECK(actionNorm > std::numeric_limits::min()); CHECK(finiteDifferenceNorm > std::numeric_limits::min()); CHECK(completeDifferenceError <= 8.0e-5); const int locallyFinite = vector_is_finite(normalizedResidual) && vector_is_finite(normalizedAction) && vector_is_finite(finiteDifference) ? 1 : 0; int globallyFinite = 0; REQUIRE(MPI_Allreduce(&locallyFinite, &globallyFinite, 1, MPI_INT, MPI_MIN, communicator) == MPI_SUCCESS); CHECK(globallyFinite == 1); CHECK(global_norm(normalizedResidual, communicator) > 0.0); CHECK(global_norm(normalizedAction, communicator) > 0.0); auto actionView = problem.GetManifest().residualView(normalizedAction); auto finiteDifferenceView = problem.GetManifest().residualView(finiteDifference); const auto checkGlobalRow = [&](const auto &term, const char *rowName) { const mfem::Vector residualBlock = residualView.block(term); const mfem::Vector actionBlock = actionView.block(term); const mfem::Vector differenceBlock = finiteDifferenceView.block(term); require_all_ranks( residualBlock.Size() == 1 && actionBlock.Size() == 1 && differenceBlock.Size() == 1, communicator, "global scalar residual/Jacobian block sizes" ); check_rank_consistent_scalar(residualBlock(0), communicator); check_rank_consistent_scalar(actionBlock(0), communicator); check_rank_consistent_scalar(differenceBlock(0), communicator); const double rowMagnitude = std::max(std::abs(actionBlock(0)), std::abs(differenceBlock(0))); const double rowDifferenceError = std::abs(actionBlock(0) - differenceBlock(0)) / std::max(rowMagnitude, std::numeric_limits::epsilon()); CAPTURE(rowName, actionBlock(0), differenceBlock(0), rowMagnitude, rowDifferenceError); CHECK(rowMagnitude > 1.0e-10); CHECK(rowDifferenceError <= 8.0e-5); }; checkGlobalRow(utils::blocks::fixed_total_mass_constraint.mass_normalization_term, "fixed-total-mass"); checkGlobalRow(utils::blocks::fixed_angular_momentum_constraint.angular_velocity_term, "fixed-angular-momentum"); checkGlobalRow(utils::blocks::fixed_central_density_phase.central_value_term, "fixed-central-density phase"); auto component = preconditioning::makePreconditioner(problem); STATIC_CHECK(decltype(component)::borderValueArity == 3); STATIC_CHECK(decltype(component)::borderResidualArity == 3); auto physicalInverse = preconditioning::prepare(problem, component); require_all_ranks(physicalInverse.IsCurrent(), communicator, "prepared physical preconditioner currentness"); auto scaledInverse = normalized.MakeScaledPreconditioner(physicalInverse); require_all_ranks(scaledInverse.IsCurrent(), communicator, "prepared normalized preconditioner currentness"); mfem::Vector normalizedRightHandSide = make_deterministic_vector(problem.EquationSize(), 0.73); auto rightHandSideView = problem.GetManifest().residualView(normalizedRightHandSide); mfem::Vector massRightHandSide = rightHandSideView.block(utils::blocks::fixed_total_mass_constraint.mass_normalization_term); mfem::Vector angularMomentumRightHandSide = rightHandSideView.block(utils::blocks::fixed_angular_momentum_constraint.angular_velocity_term); mfem::Vector phaseRightHandSide = rightHandSideView.block(utils::blocks::fixed_central_density_phase.central_value_term); massRightHandSide(0) = 0.11; angularMomentumRightHandSide(0) = -0.19; phaseRightHandSide(0) = 0.29; massRightHandSide.SyncAliasMemory(normalizedRightHandSide); angularMomentumRightHandSide.SyncAliasMemory(normalizedRightHandSide); phaseRightHandSide.SyncAliasMemory(normalizedRightHandSide); normalizedRightHandSide /= global_norm(normalizedRightHandSide, communicator); mfem::Vector firstCorrection(scaledInverse.Height()); mfem::Vector repeatedCorrection(scaledInverse.Height()); firstCorrection = 0.0; repeatedCorrection = 0.0; scaledInverse.Mult(normalizedRightHandSide, firstCorrection); scaledInverse.Mult(normalizedRightHandSide, repeatedCorrection); mfem::Vector repeatDifference(repeatedCorrection); repeatDifference -= firstCorrection; const double correctionNorm = global_norm(firstCorrection, communicator); const double repeatError = global_norm(repeatDifference, communicator) / std::max(correctionNorm, std::numeric_limits::epsilon()); CAPTURE(correctionNorm, repeatError); const int locallyFiniteCorrections = vector_is_finite(firstCorrection) && vector_is_finite(repeatedCorrection) ? 1 : 0; int globallyFiniteCorrections = 0; REQUIRE( MPI_Allreduce(&locallyFiniteCorrections, &globallyFiniteCorrections, 1, MPI_INT, MPI_MIN, communicator) == MPI_SUCCESS ); CHECK(globallyFiniteCorrections == 1); CHECK(std::isfinite(correctionNorm)); CHECK(correctionNorm > 0.0); CHECK(repeatError <= 2.0e-12); require_all_ranks( physicalInverse.IsCurrent() && scaledInverse.IsCurrent(), communicator, "preconditioner currentness after repeated application" ); const mfem::Vector &readOnlyCorrection = firstCorrection; const auto correctionView = problem.GetManifest().stateView(readOnlyCorrection); const double massCorrection = correctionView.block(utils::blocks::fixed_total_mass_constraint.mass_normalization_term)(0); const double angularVelocityCorrection = correctionView.block(utils::blocks::fixed_angular_momentum_constraint.angular_velocity_term)(0); const double phaseCorrection = correctionView.block(utils::blocks::fixed_central_density_phase.central_value_term)(0); CAPTURE(massCorrection, angularVelocityCorrection, phaseCorrection); check_rank_consistent_scalar(massCorrection, communicator); check_rank_consistent_scalar(angularVelocityCorrection, communicator); check_rank_consistent_scalar(phaseCorrection, communicator); /* A Newton iteration reparses the same normalized state under fresh * dependency revisions. Every rank must observe the stale inverse, and * refresh must reconstruct the inferred border actions and Schur data. */ const auto secondPreparation = normalized.Prepare(normalizedState, make_stellar_dependencies(5)); require_all_ranks( secondPreparation.DidAnyWork() && normalized.IsPrepared(), communicator, "second variadic stellar-root preparation" ); mfem::Vector secondPreparedResidual; normalized.BuildResidual(secondPreparedResidual); mfem::Vector secondPreparedResidualDifference(secondPreparedResidual); secondPreparedResidualDifference -= normalizedResidual; const double secondPreparedResidualError = global_norm(secondPreparedResidualDifference, communicator) / std::max( {global_norm(secondPreparedResidual, communicator), global_norm(normalizedResidual, communicator), std::numeric_limits::epsilon()} ); CAPTURE(secondPreparedResidualError); CHECK(secondPreparedResidualError <= 2.0e-12); require_all_ranks( !physicalInverse.IsCurrent() && !scaledInverse.IsCurrent(), communicator, "preconditioners become stale together" ); CHECK_THROWS_AS(scaledInverse.Mult(normalizedRightHandSide, repeatedCorrection), std::logic_error); const auto refresh = physicalInverse.Refresh(); CHECK(refresh.specificationActionsRefreshed); CHECK(refresh.rebuiltSchurComplement); CHECK(refresh.DidAnyWork()); require_all_ranks( physicalInverse.IsCurrent() && scaledInverse.IsCurrent(), communicator, "refreshed preconditioner currentness" ); const auto noOpRefresh = physicalInverse.Refresh(); CHECK_FALSE(noOpRefresh.DidAnyWork()); mfem::Vector refreshedCorrection(scaledInverse.Height()); refreshedCorrection = 0.0; scaledInverse.Mult(normalizedRightHandSide, refreshedCorrection); mfem::Vector refreshDifference(refreshedCorrection); refreshDifference -= firstCorrection; const double refreshError = global_norm(refreshDifference, communicator) / std::max(correctionNorm, std::numeric_limits::epsilon()); const int locallyFiniteRefresh = vector_is_finite(refreshedCorrection) ? 1 : 0; int globallyFiniteRefresh = 0; REQUIRE( MPI_Allreduce(&locallyFiniteRefresh, &globallyFiniteRefresh, 1, MPI_INT, MPI_MIN, communicator) == MPI_SUCCESS ); CAPTURE(refreshError); CHECK(globallyFiniteRefresh == 1); CHECK(refreshError <= 2.0e-10); } TEST_CASE( "MPI Prepared Gravity Operators Preserve Global Algebraic Identities", "[mpi][distributed][gravity][operators][unit]" ) { const auto args = test_utils::setup_args(); mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); using GeometryContext = mean_field::operators::context::gravity_field::GravityFieldGeometryContext; GeometryContext geometry_context(f, *f.domainMapperStateless); mfem::Vector displacement_true(f.displacementFes->GetTrueVSize()); displacement_true = 0.0; const mfem::Vector displacement = geometry_context.GetDisplacementMap().gather(displacement_true); geometry_context.PreparePrimal(displacement, {0}, {0}); const mfem::Operator &mass = geometry_context.GetMassOperator(); const mfem::Vector first = make_deterministic_vector(mass.Width(), 0.17); const mfem::Vector second = make_deterministic_vector(mass.Width(), 0.83); mfem::Vector combination(first); combination *= 1.7; combination.Add(-0.4, second); mfem::Vector first_action; mfem::Vector second_action; mfem::Vector combination_action; mass.Mult(first, first_action); mass.Mult(second, second_action); mass.Mult(combination, combination_action); mfem::Vector expected_combination(first_action); expected_combination *= 1.7; expected_combination.Add(-0.4, second_action); mfem::Vector linearity_difference(combination_action); linearity_difference -= expected_combination; const MPI_Comm communicator = f.mesh->GetComm(); const double symmetry_scale = std::max( {std::abs(global_dot(first, second_action, communicator)), std::abs(global_dot(second, first_action, communicator)), std::numeric_limits::epsilon()} ); const double symmetry_error = std::abs(global_dot(first, second_action, communicator) - global_dot(second, first_action, communicator)) / symmetry_scale; const double linearity_error = global_norm(linearity_difference, communicator) / std::max(global_norm(expected_combination, communicator), std::numeric_limits::epsilon()); CHECK(symmetry_error <= 2.0e-12); CHECK(linearity_error <= 2.0e-12); const mfem::Operator &divergence = geometry_context.GetDivergenceOperator(); const mfem::Operator &transpose_divergence = geometry_context.GetTransposeDivergenceOperator(); const mfem::Vector flux = make_deterministic_vector(divergence.Width(), 0.41); const mfem::Vector potential = make_deterministic_vector(divergence.Height(), 0.67); mfem::Vector divergence_action; mfem::Vector transpose_action; divergence.Mult(flux, divergence_action); transpose_divergence.Mult(potential, transpose_action); const double forward_product = global_dot(potential, divergence_action, communicator); const double transpose_product = global_dot(flux, transpose_action, communicator); const double adjoint_scale = std::max({std::abs(forward_product), std::abs(transpose_product), std::numeric_limits::epsilon()}); const double adjoint_error = std::abs(forward_product - transpose_product) / adjoint_scale; CHECK(adjoint_error <= 2.0e-12); } TEST_CASE( "MPI Coupled Gravity LDU Is Stationary Linear And Does Not Reprepare Geometry", "[mpi][distributed][gravity][preconditioning][integration]" ) { const auto args = test_utils::setup_args(); mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); using GeometryContext = mean_field::operators::context::gravity_field::GravityFieldGeometryContext; GeometryContext geometryContext(f, *f.domainMapperStateless); mfem::Vector displacementTrue(f.displacementFes->GetTrueVSize()); displacementTrue = 0.0; const mfem::Vector displacement = geometryContext.GetDisplacementMap().gather(displacementTrue); geometryContext.PreparePrimal(displacement, {.value = 1}, {.value = 1}); namespace backend = mean_field::preconditioning::backend; namespace preconditioning = mean_field::preconditioning; const auto block = preconditioning::GravityFieldBlock( backend::Diagonal{}, backend::HypreBoomerAMG{backend::FixedCycles{.cycles = 1}}, preconditioning::GravityApproximateLDU{} ); auto prepared = preconditioning::prepare(f, geometryContext, block); const mfem::Vector first = make_deterministic_vector(prepared.Width(), 0.23); const mfem::Vector second = make_deterministic_vector(prepared.Width(), 0.79); mfem::Vector combined(first); combined *= 1.3; combined.Add(-0.45, second); mfem::Vector firstAction(prepared.Height()); mfem::Vector secondAction(prepared.Height()); mfem::Vector combinedAction(prepared.Height()); mfem::Vector repeatedAction(prepared.Height()); firstAction = 0.0; secondAction = 0.0; combinedAction = 0.0; repeatedAction = 0.0; const std::uint64_t massPreparations = geometryContext.GetMassOperator().GetPreparationCount(); const std::uint64_t sourcePreparations = geometryContext.GetSourceOperator().GetPreparationCount(); double *const combinedStorage = combinedAction.GetData(); prepared.Mult(first, firstAction); prepared.Mult(second, secondAction); prepared.Mult(combined, combinedAction); prepared.Mult(first, repeatedAction); mfem::Vector expectedCombined(firstAction); expectedCombined *= 1.3; expectedCombined.Add(-0.45, secondAction); const MPI_Comm communicator = f.mesh->GetComm(); mfem::Vector linearityDifference(combinedAction); linearityDifference -= expectedCombined; mfem::Vector determinismDifference(repeatedAction); determinismDifference -= firstAction; const double linearityError = global_norm(linearityDifference, communicator) / std::max(global_norm(expectedCombined, communicator), std::numeric_limits::epsilon()); CHECK(vector_is_finite(combinedAction)); CHECK(linearityError <= 5.0e-12); CHECK(global_norm(determinismDifference, communicator) <= 5.0e-14); CHECK(combinedAction.GetData() == combinedStorage); CHECK(geometryContext.GetMassOperator().GetPreparationCount() == massPreparations); CHECK(geometryContext.GetSourceOperator().GetPreparationCount() == sourcePreparations); CHECK(prepared.GetFactorization().GetStatistics().applications == 4); } TEST_CASE( "MPI Gravity Analysis And Solve Produce Finite Distributed Fields", "[mpi][distributed][gravity][integration]" ) { auto args = test_utils::setup_args(); mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); *f.displacement = 0.0; using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema; mfem::Vector attribute_density(f.smesh.mesh->attributes.Max()); attribute_density = 0.0; for (int index = 0; index < f.smesh.mesh->attributes.Size(); ++index) { const int attribute = f.smesh.mesh->attributes[index]; if (DomainSchema::template attribute_belongs_to(attribute)) { attribute_density(attribute - 1) = 1.0; } } mfem::PWConstCoefficient density_coefficient(attribute_density); mfem::ParGridFunction density(f.densityFes.get()); density.ProjectCoefficient(density_coefficient); mean_field::analysis::conserve_mass(f, density, mean_field::utils::MASS); const double integrated_mass = mean_field::analysis::domain_integrate_grid_function( f, density, mean_field::utils::DOMAINS::STELLAR, mean_field::mapping::COORDINATE_SPACE::PHYSICAL ); f.com = mean_field::analysis::get_com(f, density); f.Q = mean_field::physics::compute_quadrupole_moment_tensor(f, density, f.com); const mean_field::physics::GravitySolution solution = mean_field::physics::solve_gravity_field( f, mean_field::physics::GravitySolveOptions{ .relativeTolerance = 1.0e-12, .absoluteTolerance = 1.0e-15, .maximumIterations = 1000 }, density, *f.displacement ); mfem::Vector flux_true; mfem::Vector potential_true; solution.gradPhi.GetTrueDofs(flux_true); solution.phi.GetTrueDofs(potential_true); const int local_finite = vector_is_finite(flux_true) && vector_is_finite(potential_true) ? 1 : 0; int globally_finite = 0; MPI_Allreduce(&local_finite, &globally_finite, 1, MPI_INT, MPI_MIN, f.mesh->GetComm()); const double local_norms[2]{flux_true * flux_true, potential_true * potential_true}; double global_norms[2]{}; MPI_Allreduce(local_norms, global_norms, 2, MPI_DOUBLE, MPI_SUM, f.mesh->GetComm()); CHECK(globally_finite == 1); CHECK(std::abs(integrated_mass - mean_field::utils::MASS) <= 1.0e-12 * mean_field::utils::MASS); CHECK(global_norms[0] > std::numeric_limits::min()); CHECK(global_norms[1] > std::numeric_limits::min()); }