#include #include #include #include #include #include #include #include import mean_field; import test_helpers; namespace prepared_pressure_force_test_utils { using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema; using CoupledForm = mean_field::utils::blocks::barotropic_equilibrium_form; struct Maps final { mean_field::field::FieldDofMap density; mean_field::field::FieldDofMap displacement; mean_field::field::FieldDofMap gravityFlux; mean_field::field::FieldDofMap gravityPotential; mean_field::field::FieldDofMap enthalpy; explicit Maps(const mean_field::fem::FEM &f) : density( mean_field::field::make_field_dof_map< mean_field::field::Density, DomainSchema>(*f.densityFes) ), displacement( mean_field::field::make_field_dof_map< mean_field::field::Displacement, DomainSchema>(*f.displacementFes) ), gravityFlux( mean_field::field::make_field_dof_map< mean_field::field::Gravity, DomainSchema>(*f.gravityFluxFes) ), gravityPotential( mean_field::field::make_field_dof_map< mean_field::field::Gravity, DomainSchema>(*f.gravityPotentialFes) ), enthalpy( mean_field::field::make_field_dof_map< mean_field::field::Enthalpy, DomainSchema>(*f.enthalpyFes) ) { } }; [[nodiscard]] mfem::Vector make_affine_displacement( const mean_field::fem::FEM &f, const double scale ) { mfem::ParGridFunction field(f.displacementFes.get()); mfem::VectorFunctionCoefficient coefficient( f.mesh->Dimension(), [scale](const mfem::Vector &position, mfem::Vector &value) { value.SetSize(position.Size()); for (int dimension = 0; dimension < position.Size(); ++dimension) { value(dimension) = scale * position(dimension); } } ); field.ProjectCoefficient(coefficient); mfem::Vector result; field.GetTrueDofs(result); return result; } [[nodiscard]] mfem::Vector make_positive_enthalpy_true( const mean_field::fem::FEM &f, const double phase ) { mfem::Vector enthalpy(f.enthalpyFes->GetTrueVSize()); for (int index = 0; index < enthalpy.Size(); ++index) { const double position = static_cast(index + 1); enthalpy(index) = 0.93 + 0.09 * std::sin(0.23 * position + phase) + 0.04 * std::cos(0.17 * position - 0.5 * phase); } return enthalpy; } [[nodiscard]] mfem::Vector make_enthalpy_direction_true( const mean_field::fem::FEM &f, const double phase ) { mfem::Vector direction(f.enthalpyFes->GetTrueVSize()); for (int index = 0; index < direction.Size(); ++index) { const double position = static_cast(index + 1); direction(index) = 0.27 * std::sin(0.19 * position + phase) + 0.14 * std::cos(0.13 * position - 0.5 * phase); } return direction; } [[nodiscard]] mfem::Vector make_displacement_direction_true( const mean_field::fem::FEM &f, const double phase ) { MFEM_VERIFY( f.mesh->Dimension() == 3, "The prepared pressure-force test requires a " "three-dimensional mesh." ); mfem::ParGridFunction directionField(f.displacementFes.get()); mfem::VectorFunctionCoefficient directionCoefficient( 3, [phase](const mfem::Vector &position, mfem::Vector &value) { const double x = position(0); const double y = position(1); const double z = position(2); value.SetSize(3); value(0) = 0.019 * x + 0.011 * y * z - 0.006 * z * z + 0.004 * phase * y; value(1) = -0.016 * y + 0.008 * x * z + 0.005 * x * x - 0.003 * phase * z; value(2) = 0.013 * z - 0.010 * x * y + 0.006 * y * y + 0.004 * phase * x; } ); directionField.ProjectCoefficient(directionCoefficient); mfem::Vector directionTrue; directionField.GetTrueDofs(directionTrue); return directionTrue; } [[nodiscard]] double relative_difference( const mfem::Vector &left, const mfem::Vector &right, const MPI_Comm communicator ) { MFEM_VERIFY( left.Size() == right.Size(), "Cannot compare prepared pressure-force vectors with " "different sizes." ); mfem::Vector difference(left); difference -= right; const double scale = std::max( {gravity_prepared_test_utils::global_norm(left, communicator), gravity_prepared_test_utils::global_norm(right, communicator), 100.0 * std::numeric_limits::epsilon()} ); return gravity_prepared_test_utils::global_norm(difference, communicator) / scale; } [[nodiscard]] mean_field::operators::context::pressure_force::PressureForceDependencies make_dependencies() { return { .discretization = {.identity = 1201, .revision = 3}, .enthalpy = {.identity = 1213, .revision = 5}, .displacement = {.identity = 1217, .revision = 7} }; } constexpr auto densityValue = mean_field::utils::blocks::get_value_block(mean_field::utils::blocks::density_field.mass_term); constexpr auto displacementValue = mean_field::utils::blocks::get_value_block( mean_field::utils::blocks::displacement_field.geometry_term ); constexpr auto gravityGradientValue = mean_field::utils::blocks::get_value_block(mean_field::utils::blocks::gravity_field.gradient_term); constexpr auto gravityPotentialValue = mean_field::utils::blocks::get_value_block(mean_field::utils::blocks::gravity_field.poisson_term); constexpr auto enthalpyValue = mean_field::utils::blocks::get_value_block( mean_field::utils::blocks::enthalpy_field.specific_term ); constexpr auto barotropicConstantValue = mean_field::utils::blocks::get_value_block( mean_field::utils::blocks::barotropic_constant_field.mass_normalization_term ); constexpr auto gravityGradientResidual = mean_field::utils::blocks::get_residual_block( mean_field::utils::blocks::gravity_field.gradient_term ); constexpr auto gravityPotentialResidual = mean_field::utils::blocks::get_residual_block( mean_field::utils::blocks::gravity_field.poisson_term ); constexpr auto densityResidual = mean_field::utils::blocks::get_residual_block(mean_field::utils::blocks::density_field.mass_term); constexpr auto displacementResidual = mean_field::utils::blocks::get_residual_block( mean_field::utils::blocks::displacement_field.geometry_term ); constexpr auto enthalpyResidual = mean_field::utils::blocks::get_residual_block( mean_field::utils::blocks::enthalpy_field.specific_term ); constexpr auto massResidual = mean_field::utils::blocks::get_residual_block( mean_field::utils::blocks::barotropic_constant_field.mass_normalization_term ); [[nodiscard]] mean_field::operators::BarotropicEquilibriumLayout make_coupled_layout(const Maps &maps) { const std::array valueSizes{ maps.density.reduced_size(), maps.displacement.reduced_size(), maps.gravityFlux.reduced_size(), maps.gravityPotential.reduced_size(), maps.enthalpy.reduced_size(), 1 }; const std::array residualSizes{ maps.gravityFlux.reduced_size(), maps.gravityPotential.reduced_size(), maps.density.reduced_size(), maps.displacement.reduced_size(), maps.enthalpy.reduced_size(), 1 }; return {valueSizes, residualSizes}; } template [[nodiscard]] mfem::Vector copy_residual_block( const mfem::Vector &action, const mean_field::operators::BarotropicEquilibriumLayout &layout, const mean_field::utils::blocks::residual_block block ) { mfem::Vector result(layout.size(block)); const int offset = layout.offset(block); for (int entry = 0; entry < result.Size(); ++entry) { result(entry) = action(offset + entry); } return result; } } // namespace prepared_pressure_force_test_utils TEST_CASE( "Prepared Pressure Force Uses FieldDof Supported Dimensions And Owns Its " "Context", tags::barotrope &tags::pressure &tags::prepared &tags::field &tags::unit ) { mean_field::utils::Args args = test_utils::setup_args(); mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); REQUIRE(f.okay()); const prepared_pressure_force_test_utils::Maps maps(f); const mean_field::eos::Polytrope equationOfState(3.0, 0.25); mean_field::operators::PreparedPressureForceOperator preparedOperator(f, *f.domainMapperStateless, equationOfState); REQUIRE(maps.enthalpy.reduced_size() < maps.enthalpy.full_size()); CHECK(maps.displacement.is_identity()); CHECK(preparedOperator.GetEnthalpySize() == maps.enthalpy.reduced_size()); CHECK(preparedOperator.GetDisplacementSize() == maps.displacement.reduced_size()); CHECK( &preparedOperator.GetContext().GetPreparationStatistics() == &preparedOperator.GetContextPreparationStatistics() ); } TEST_CASE( "Prepared Pressure Force Reports Expected EOS Rejections Without Unwinding", tags::barotrope &tags::pressure &tags::prepared &tags::unit ) { mean_field::utils::Args args = test_utils::setup_args(); mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); REQUIRE(f.okay()); const prepared_pressure_force_test_utils::Maps maps(f); const mean_field::eos::Polytrope equationOfState(3.0, 0.25); mean_field::operators::PreparedPressureForceOperator preparedOperator(f, *f.domainMapperStateless, equationOfState); mfem::Vector enthalpy(maps.enthalpy.reduced_size()); mfem::Vector displacement(maps.displacement.reduced_size()); enthalpy = -1.0; displacement = 0.0; auto dependencies = prepared_pressure_force_test_utils::make_dependencies(); const auto outsideDomain = preparedOperator.TryPrepare({.enthalpy = enthalpy, .displacement = displacement}, dependencies); REQUIRE_FALSE(outsideDomain.has_value()); CHECK( outsideDomain.error().reason == mean_field::operators::PressureForcePreparationRejectionReason::equation_of_state ); CHECK(outsideDomain.error().equationOfStateCode == mean_field::eos::EvaluationErrorCode::outside_domain); CHECK_FALSE(preparedOperator.IsPrepared()); try { (void)preparedOperator.Prepare({.enthalpy = enthalpy, .displacement = displacement}, dependencies); FAIL("The compatibility Prepare overload accepted an out-of-domain EOS input."); } catch (const mean_field::eos::EvaluationError &error) { CHECK(error.code() == mean_field::eos::EvaluationErrorCode::outside_domain); } enthalpy = std::numeric_limits::max(); ++dependencies.enthalpy.revision; const auto rejected = preparedOperator.TryPrepare({.enthalpy = enthalpy, .displacement = displacement}, dependencies); REQUIRE_FALSE(rejected.has_value()); CHECK(rejected.error().reason == mean_field::operators::PressureForcePreparationRejectionReason::equation_of_state); CHECK(rejected.error().equationOfStateCode == mean_field::eos::EvaluationErrorCode::nonfinite_result); CHECK_FALSE(preparedOperator.IsPrepared()); CHECK_THROWS_AS( preparedOperator.Prepare({.enthalpy = enthalpy, .displacement = displacement}, dependencies), mean_field::eos::EvaluationError ); enthalpy = 1.0; ++dependencies.enthalpy.revision; const auto accepted = preparedOperator.TryPrepare({.enthalpy = enthalpy, .displacement = displacement}, dependencies); REQUIRE(accepted.has_value()); CHECK(preparedOperator.IsPrepared()); } TEST_CASE( "Prepared Pressure Force Reports Invalid Candidate Geometry Without Unwinding", tags::barotrope &tags::pressure &tags::prepared &tags::geometry &tags::unit ) { mean_field::utils::Args args = test_utils::setup_args(); mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); REQUIRE(f.okay()); const prepared_pressure_force_test_utils::Maps maps(f); const mean_field::eos::Polytrope equationOfState(3.0, 0.25); mean_field::operators::PreparedPressureForceOperator preparedOperator(f, *f.domainMapperStateless, equationOfState); mfem::Vector enthalpy(maps.enthalpy.reduced_size()); enthalpy = 1.0; mfem::Vector displacement = maps.displacement.gather(prepared_pressure_force_test_utils::make_affine_displacement(f, -2.0)); auto dependencies = prepared_pressure_force_test_utils::make_dependencies(); const auto rejected = preparedOperator.TryPrepare({.enthalpy = enthalpy, .displacement = displacement}, dependencies); REQUIRE_FALSE(rejected.has_value()); CHECK(rejected.error().reason == mean_field::operators::PressureForcePreparationRejectionReason::invalid_mapping); CHECK(rejected.error().mappingStatus == mean_field::mapping::MappingStatus::non_positive_determinant); CHECK_FALSE(preparedOperator.IsPrepared()); CHECK_THROWS_AS( preparedOperator.Prepare({.enthalpy = enthalpy, .displacement = displacement}, dependencies), std::domain_error ); displacement = 0.0; ++dependencies.displacement.revision; const auto accepted = preparedOperator.TryPrepare({.enthalpy = enthalpy, .displacement = displacement}, dependencies); REQUIRE(accepted.has_value()); CHECK(preparedOperator.IsPrepared()); } TEST_CASE( "Prepared Pressure Force Jacobian Matches Full Stateless Columns " "Through FieldDof Restriction", tags::barotrope &tags::pressure &tags::prepared &tags::field &tags::integration &tags::accuracy ) { mean_field::utils::Args args = test_utils::setup_args(); mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); REQUIRE(f.okay()); const prepared_pressure_force_test_utils::Maps maps(f); const mean_field::eos::Polytrope equationOfState(3.0, 0.25); const mfem::Vector enthalpy = maps.enthalpy.gather(prepared_pressure_force_test_utils::make_positive_enthalpy_true(f, 0.47)); const mfem::Vector displacement = maps.displacement.gather(gravity_prepared_test_utils::make_displacement(f, 0.69)); const mfem::Vector enthalpyDirection = maps.enthalpy.gather(prepared_pressure_force_test_utils::make_enthalpy_direction_true(f, 0.73)); const mfem::Vector displacementDirection = maps.displacement.gather(prepared_pressure_force_test_utils::make_displacement_direction_true(f, 0.83)); mean_field::operators::PreparedPressureForceOperator preparedOperator(f, *f.domainMapperStateless, equationOfState); preparedOperator.Prepare( {.enthalpy = enthalpy, .displacement = displacement}, prepared_pressure_force_test_utils::make_dependencies() ); const mfem::Vector enthalpyTrue = maps.enthalpy.scatter(enthalpy); const mfem::Vector displacementTrue = maps.displacement.scatter(displacement); const mfem::Vector enthalpyDirectionTrue = maps.enthalpy.scatter(enthalpyDirection); const mfem::Vector displacementDirectionTrue = maps.displacement.scatter(displacementDirection); mfem::Vector preparedEnthalpyAction; mfem::Vector kernelEnthalpyActionTrue; preparedOperator.ApplyEnthalpyJacobianAction(enthalpyDirection, preparedEnthalpyAction); mean_field::operators::kernels::apply_pressure_force_enthalpy_action( f, *f.domainMapperStateless, equationOfState, enthalpyTrue, enthalpyDirectionTrue, displacementTrue, kernelEnthalpyActionTrue ); const mfem::Vector kernelEnthalpyAction = maps.displacement.gather(kernelEnthalpyActionTrue); CHECK( prepared_pressure_force_test_utils::relative_difference( preparedEnthalpyAction, kernelEnthalpyAction, f.mesh->GetComm() ) < 2.0e-12 ); mfem::Vector preparedDisplacementAction; mfem::Vector kernelDisplacementActionTrue; preparedOperator.ApplyDisplacementJacobianAction(displacementDirection, preparedDisplacementAction); mean_field::operators::kernels::apply_pressure_force_displacement_action( f, *f.domainMapperStateless, equationOfState, enthalpyTrue, displacementDirectionTrue, displacementTrue, kernelDisplacementActionTrue ); const mfem::Vector kernelDisplacementAction = maps.displacement.gather(kernelDisplacementActionTrue); CHECK( prepared_pressure_force_test_utils::relative_difference( preparedDisplacementAction, kernelDisplacementAction, f.mesh->GetComm() ) < 2.0e-12 ); mfem::Vector fusedAction; preparedOperator.ApplyCompleteJacobianAction(enthalpyDirection, displacementDirection, fusedAction); mfem::Vector expectedFusedAction(kernelEnthalpyAction); expectedFusedAction += kernelDisplacementAction; CHECK( prepared_pressure_force_test_utils::relative_difference(fusedAction, expectedFusedAction, f.mesh->GetComm()) < 2.0e-12 ); } TEST_CASE( "Prepared Pressure Force MFEM Adapter Routes Reduced Coupled FieldDof " "Blocks", tags::barotrope &tags::pressure &tags::prepared &tags::field &tags::integration &tags::jacobian &tags::mfem_operators &tags::unit ) { mean_field::utils::Args args = test_utils::setup_args(); mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); REQUIRE(f.okay()); const prepared_pressure_force_test_utils::Maps maps(f); const mean_field::eos::Polytrope equationOfState(3.0, 0.25); const mfem::Vector enthalpy = maps.enthalpy.gather(prepared_pressure_force_test_utils::make_positive_enthalpy_true(f, 0.53)); const mfem::Vector displacement = maps.displacement.gather(gravity_prepared_test_utils::make_displacement(f, 0.71)); const mfem::Vector enthalpyDirection = maps.enthalpy.gather(prepared_pressure_force_test_utils::make_enthalpy_direction_true(f, 0.89)); const mfem::Vector displacementDirection = maps.displacement.gather(prepared_pressure_force_test_utils::make_displacement_direction_true(f, 0.97)); mean_field::operators::PreparedPressureForceOperator preparedOperator(f, *f.domainMapperStateless, equationOfState); preparedOperator.Prepare( {.enthalpy = enthalpy, .displacement = displacement}, prepared_pressure_force_test_utils::make_dependencies() ); const mean_field::operators::BarotropicEquilibriumLayout layout = prepared_pressure_force_test_utils::make_coupled_layout(maps); mean_field::operators::PreparedPressureForceJacobianOperator adapter(layout, preparedOperator); CHECK(layout.size(prepared_pressure_force_test_utils::enthalpyValue) == maps.enthalpy.reduced_size()); CHECK(layout.size(prepared_pressure_force_test_utils::densityValue) == maps.density.reduced_size()); mfem::BlockVector direction(layout.value_offsets()); direction = 0.0; /* * Populate unrelated columns deliberately. */ direction.GetBlock(prepared_pressure_force_test_utils::densityValue) = 0.37; direction.GetBlock(prepared_pressure_force_test_utils::gravityGradientValue) = -0.41; direction.GetBlock(prepared_pressure_force_test_utils::gravityPotentialValue) = 0.59; direction.GetBlock(prepared_pressure_force_test_utils::barotropicConstantValue) = -0.73; direction.GetBlock(prepared_pressure_force_test_utils::displacementValue) = displacementDirection; direction.GetBlock(prepared_pressure_force_test_utils::enthalpyValue) = enthalpyDirection; mfem::Vector expectedDisplacementAction; preparedOperator.ApplyCompleteJacobianAction(enthalpyDirection, displacementDirection, expectedDisplacementAction); mfem::Vector action; adapter.Mult(direction, action); const mfem::Vector displacementResidualAction = prepared_pressure_force_test_utils::copy_residual_block( action, layout, prepared_pressure_force_test_utils::displacementResidual ); CHECK( prepared_pressure_force_test_utils::relative_difference( displacementResidualAction, expectedDisplacementAction, f.mesh->GetComm() ) < 2.0e-14 ); CHECK( prepared_pressure_force_test_utils::copy_residual_block( action, layout, prepared_pressure_force_test_utils::gravityGradientResidual ) .Norml2() == 0.0 ); CHECK( prepared_pressure_force_test_utils::copy_residual_block( action, layout, prepared_pressure_force_test_utils::gravityPotentialResidual ) .Norml2() == 0.0 ); CHECK( prepared_pressure_force_test_utils::copy_residual_block( action, layout, prepared_pressure_force_test_utils::densityResidual ) .Norml2() == 0.0 ); CHECK( prepared_pressure_force_test_utils::copy_residual_block( action, layout, prepared_pressure_force_test_utils::enthalpyResidual ) .Norml2() == 0.0 ); CHECK( prepared_pressure_force_test_utils::copy_residual_block( action, layout, prepared_pressure_force_test_utils::massResidual ) .Norml2() == 0.0 ); } TEST_CASE( "Pressure Force Residual Converges To A Manufactured Analytic Force", tags::barotrope &tags::pressure &tags::kernels &tags::integration &tags::convergence &tags::h_refinement &tags::analytic_comparison &tags::accuracy ) { constexpr int dimension = 3; constexpr std::array refinementLevels{0, 1}; constexpr double minimumObservedRate = 3.0; constexpr double finestRelativeTolerance = 2.0e-3; constexpr double amplitude = 1.0; constexpr double bumpSharpness = 0.25; constexpr double supportRadiusFraction = 0.90; std::array relativeErrors{}; for (std::size_t levelIndex = 0; levelIndex < refinementLevels.size(); ++levelIndex) { mean_field::utils::Args args = test_utils::setup_args(); mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, refinementLevels[levelIndex]); REQUIRE(f.okay()); REQUIRE(f.mesh->Dimension() == dimension); REQUIRE(f.mesh->GetNE() > 0); const MPI_Comm communicator = f.mesh->GetComm(); constexpr double supportRadius = supportRadiusFraction * mean_field::utils::RADIUS; constexpr double supportRadiusSquared = supportRadius * supportRadius; auto analyticEnthalpyFunction = [supportRadiusSquared](const mfem::Vector &position) { const double normalizedRadiusSquared = (position * position) / supportRadiusSquared; if (normalizedRadiusSquared >= 1.0) { return 0.0; } const double distanceToSupportBoundary = 1.0 - normalizedRadiusSquared; return amplitude * std::exp(-bumpSharpness * normalizedRadiusSquared / distanceToSupportBoundary); }; auto analyticPressureForceFunction = [supportRadiusSquared](const mfem::Vector &position, mfem::Vector &force) { force.SetSize(dimension); force = 0.0; const double normalizedRadiusSquared = (position * position) / supportRadiusSquared; if (normalizedRadiusSquared >= 1.0) { return; } const double distanceToSupportBoundary = 1.0 - normalizedRadiusSquared; const double enthalpy = amplitude * std::exp(-bumpSharpness * normalizedRadiusSquared / distanceToSupportBoundary); const double pressureGradientScale = -2.0 * bumpSharpness * std::pow(enthalpy, 4.0) / (supportRadiusSquared * distanceToSupportBoundary * distanceToSupportBoundary); for (int component = 0; component < dimension; ++component) { force(component) = pressureGradientScale * position(component); } }; mfem::FunctionCoefficient analyticEnthalpyCoefficient(analyticEnthalpyFunction); mfem::VectorFunctionCoefficient analyticPressureForceCoefficient(dimension, analyticPressureForceFunction); mfem::ParGridFunction discreteEnthalpyField(f.enthalpyFes.get()); discreteEnthalpyField.ProjectCoefficient(analyticEnthalpyCoefficient); mfem::Vector discreteEnthalpyTrue; discreteEnthalpyField.GetTrueDofs(discreteEnthalpyTrue); mfem::Vector zeroDisplacement(f.displacementFes->GetTrueVSize()); zeroDisplacement = 0.0; const mean_field::eos::Polytrope barotrope(3.0, 0.25); mfem::Vector discreteResidual; mean_field::operators::kernels::apply_pressure_force_residual( f, *f.domainMapperStateless, barotrope, discreteEnthalpyTrue, zeroDisplacement, discreteResidual ); REQUIRE(discreteResidual.Size() == f.displacementFes->GetTrueVSize()); mfem::Array stellarMarker(f.mesh->attributes.Max()); stellarMarker = 0; const int vacuumAttribute = field_dof_test_utils::vacuum_material_attribute; for (int attributeIndex = 0; attributeIndex < f.mesh->attributes.Size(); ++attributeIndex) { const int attribute = f.mesh->attributes[attributeIndex]; if (attribute != vacuumAttribute) { stellarMarker[attribute - 1] = 1; } } const mfem::Geometry::Type elementGeometry = f.displacementFes->GetFE(0)->GetGeomType(); for (int element = 1; element < f.mesh->GetNE(); ++element) { REQUIRE(f.displacementFes->GetFE(element)->GetGeomType() == elementGeometry); } const int referenceQuadratureOrder = 2 * f.displacementFes->GetMaxElementOrder() + 16; const mfem::IntegrationRule &referenceQuadrature = mfem::IntRules.Get(elementGeometry, referenceQuadratureOrder); auto *analyticForceIntegrator = new mfem::VectorDomainLFIntegrator(analyticPressureForceCoefficient); analyticForceIntegrator->SetIntRule(&referenceQuadrature); mfem::ParLinearForm analyticForceLoad(f.displacementFes.get()); analyticForceLoad.AddDomainIntegrator(analyticForceIntegrator, stellarMarker); analyticForceLoad.Assemble(); std::unique_ptr analyticForceHypreVector(analyticForceLoad.ParallelAssemble()); REQUIRE(analyticForceHypreVector != nullptr); mfem::Vector analyticForceTrue(*analyticForceHypreVector); REQUIRE(analyticForceTrue.Size() == discreteResidual.Size()); const double analyticForceNorm = gravity_prepared_test_utils::global_norm(analyticForceTrue, communicator); REQUIRE(std::isfinite(analyticForceNorm)); REQUIRE(analyticForceNorm > 0.0); mfem::Vector residualError(discreteResidual); residualError -= analyticForceTrue; mfem::ParBilinearForm rieszForm(f.displacementFes.get()); rieszForm.AddDomainIntegrator(new mfem::VectorMassIntegrator()); rieszForm.AddDomainIntegrator(new mfem::VectorDiffusionIntegrator()); rieszForm.Assemble(); rieszForm.Finalize(); std::unique_ptr rieszMatrix(rieszForm.ParallelAssemble()); REQUIRE(rieszMatrix != nullptr); REQUIRE(rieszMatrix->Height() == discreteResidual.Size()); REQUIRE(rieszMatrix->Width() == discreteResidual.Size()); mfem::HypreBoomerAMG rieszPreconditioner(*rieszMatrix); rieszPreconditioner.SetPrintLevel(0); mfem::CGSolver rieszSolver(communicator); rieszSolver.SetOperator(*rieszMatrix); rieszSolver.SetPreconditioner(rieszPreconditioner); rieszSolver.SetRelTol(1.0e-13); rieszSolver.SetAbsTol(1.0e-15); rieszSolver.SetMaxIter(5000); rieszSolver.SetPrintLevel(0); auto calculateDualNorm = [&rieszSolver, communicator](const mfem::Vector &functional) { mfem::Vector rieszRepresentative(functional.Size()); rieszRepresentative = 0.0; rieszSolver.Mult(functional, rieszRepresentative); MFEM_VERIFY( rieszSolver.GetConverged(), "The pressure-force convergence-test Riesz solve " "did not converge." ); const double dualNormSquared = gravity_prepared_test_utils::global_dot(functional, rieszRepresentative, communicator); MFEM_VERIFY(std::isfinite(dualNormSquared), "The pressure-force dual norm is not finite."); MFEM_VERIFY( dualNormSquared >= -100.0 * std::numeric_limits::epsilon(), "The pressure-force Riesz operator produced a " "negative dual norm." ); return std::sqrt(std::max(dualNormSquared, 0.0)); }; const double errorDualNorm = calculateDualNorm(residualError); const double analyticDualNorm = calculateDualNorm(analyticForceTrue); REQUIRE(std::isfinite(errorDualNorm)); REQUIRE(std::isfinite(analyticDualNorm)); REQUIRE(errorDualNorm > 0.0); REQUIRE(analyticDualNorm > 0.0); relativeErrors[levelIndex] = errorDualNorm / analyticDualNorm; INFO("Pressure-force refinement level = " << refinementLevels[levelIndex]); INFO("Pressure-force true DOFs = " << f.displacementFes->GlobalTrueVSize()); INFO("Pressure-force relative dual error = " << relativeErrors[levelIndex]); } for (const double relativeError : relativeErrors) { REQUIRE(std::isfinite(relativeError)); REQUIRE(relativeError > 0.0); } static_assert(refinementLevels.size() == 2, "This reduced convergence test expects exactly two refinement levels."); const double observedRate = std::log(relativeErrors[0] / relativeErrors[1]) / std::log(2.0); INFO("Level 0 pressure-force relative dual error = " << relativeErrors[0]); INFO("Level 1 pressure-force relative dual error = " << relativeErrors[1]); INFO("Level 0 to 1 pressure-force convergence rate = " << observedRate); CHECK(relativeErrors[1] < relativeErrors[0]); CHECK(observedRate > minimumObservedRate); CHECK(relativeErrors[1] < finestRelativeTolerance); }