#include #include #include #include #include import mean_field; import test_helpers; namespace pressure_force_kernel_test_utils { [[nodiscard]] mfem::Vector make_deterministic_vector( const int size, const double phase ) { mfem::Vector vector(size); for (int index = 0; index < size; ++index) { const double position = static_cast(index + 1); vector(index) = 0.71 + 0.19 * std::sin(0.31 * position + phase) + 0.08 * std::cos(0.17 * position - 0.5 * phase); } return vector; } [[nodiscard]] mfem::Vector make_zero_displacement(const mean_field::fem::FEM &f) { mfem::Vector displacementTrue(f.displacementFes->GetTrueVSize()); displacementTrue = 0.0; return displacementTrue; } [[nodiscard]] mfem::Vector make_vacuum_only_enthalpy(const mean_field::fem::FEM &f) { mfem::Vector enthalpyTrue = make_deterministic_vector(f.enthalpyFes->GetTrueVSize(), 0.43); using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema; const mean_field::field::FieldDofMap enthalpyMap = mean_field::field::make_field_dof_map(*f.enthalpyFes); for (int reducedDof = 0; reducedDof < enthalpyMap.reduced_size(); ++reducedDof) { enthalpyTrue(enthalpyMap.true_dof(reducedDof)) = 0.0; } return enthalpyTrue; } [[nodiscard]] mfem::Vector make_positive_asymmetric_enthalpy(const mean_field::fem::FEM &f) { mfem::FunctionCoefficient coefficient([](const mfem::Vector &position) { return 1.10 + 0.07 * position(0) - 0.04 * position(1) + 0.03 * position(2); }); mfem::ParGridFunction enthalpyField(f.enthalpyFes.get()); enthalpyField.ProjectCoefficient(coefficient); mfem::Vector enthalpyTrue; enthalpyField.GetTrueDofs(enthalpyTrue); return enthalpyTrue; } [[nodiscard]] mfem::Vector make_component_test_field( const mean_field::fem::FEM &f, const int component, const int coordinate ) { const int dimension = f.mesh->Dimension(); MFEM_VERIFY(component >= 0 && component < dimension, "The requested vector component is invalid."); MFEM_VERIFY(coordinate >= -1 && coordinate < dimension, "The requested coordinate is invalid."); /* * coordinate == -1 gives the rigid translation e_component. * * Otherwise this gives * * w = x_coordinate e_component. */ mfem::VectorFunctionCoefficient coefficient( dimension, [component, coordinate, dimension](const mfem::Vector &position, mfem::Vector &value) { value.SetSize(dimension); value = 0.0; value(component) = coordinate < 0 ? 1.0 : position(coordinate); } ); mfem::ParGridFunction field(f.displacementFes.get()); field.ProjectCoefficient(coefficient); mfem::Vector fieldTrue; field.GetTrueDofs(fieldTrue); return fieldTrue; } [[nodiscard]] double global_dot( const mfem::Vector &left, const mfem::Vector &right, MPI_Comm communicator ) { MFEM_VERIFY(left.Size() == right.Size(), "The global dot-product vectors have different sizes."); const double localDot = left * right; double globalDot = 0.0; MPI_Allreduce(&localDot, &globalDot, 1, MPI_DOUBLE, MPI_SUM, communicator); return globalDot; } [[nodiscard]] double integrate_pressure( const mean_field::fem::FEM &f, const mean_field::mapping::DomainMapper &domainMapper, const mean_field::eos::Polytrope &barotrope, const mfem::Vector &enthalpyTrue, const mfem::Vector &displacementTrue ) { MFEM_VERIFY( enthalpyTrue.Size() == f.enthalpyFes->GetTrueVSize(), "The pressure-integral enthalpy vector has the wrong size." ); MFEM_VERIFY( displacementTrue.Size() == f.displacementFes->GetTrueVSize(), "The pressure-integral displacement vector has the wrong size." ); mfem::Vector enthalpyLocal(f.enthalpyFes->GetVSize()); const mfem::Operator *enthalpyProlongation = f.enthalpyFes->GetProlongationMatrix(); if (enthalpyProlongation != nullptr) { enthalpyProlongation->Mult(enthalpyTrue, enthalpyLocal); } else { enthalpyLocal = enthalpyTrue; } mfem::Vector displacementLocal(f.displacementFes->GetVSize()); const mfem::Operator *displacementProlongation = f.displacementFes->GetProlongationMatrix(); if (displacementProlongation != nullptr) { displacementProlongation->Mult(displacementTrue, displacementLocal); } else { displacementLocal = displacementTrue; } const double pressureExtraOrderValue = barotrope.polytropic_index() * static_cast(mean_field::field::Enthalpy::Scalar::familyOrder); MFEM_VERIFY( std::isfinite(pressureExtraOrderValue) && pressureExtraOrderValue >= 0.0 && pressureExtraOrderValue <= static_cast(std::numeric_limits::max()), "The pressure-integral EOS order is invalid." ); const int pressureExtraOrder = static_cast(std::ceil(pressureExtraOrderValue)); using EnthalpyField = mean_field::field::Field; mean_field::mapping::DomainMapper::Workspace workspace(f.mesh->Dimension()); mean_field::mapping::VolumeMappingContext mappingContext; mfem::Array enthalpyDofs; mfem::Array displacementDofs; mfem::Array compactificationDofs; mfem::Vector elementEnthalpy; mfem::Vector elementDisplacement; mfem::Vector elementCompactification; mfem::Vector enthalpyShape; double localPressureIntegral = 0.0; const int vacuumAttribute = field_dof_test_utils::vacuum_material_attribute; for (int elementId = 0; elementId < f.mesh->GetNE(); ++elementId) { mfem::ElementTransformation *transformation = f.mesh->GetElementTransformation(elementId); MFEM_VERIFY( transformation != nullptr, "The pressure-integral reference received a null " "element transformation." ); if (transformation->Attribute == vacuumAttribute) { continue; } const mfem::FiniteElement &enthalpyElement = *f.enthalpyFes->GetFE(elementId); const mfem::FiniteElement &displacementElement = *f.displacementFes->GetFE(elementId); const mfem::FiniteElement &compactificationElement = *f.compactificationFes->GetFE(elementId); mfem::DofTransformation *enthalpyDofTransformation = f.enthalpyFes->GetElementDofs(elementId, enthalpyDofs); mfem::DofTransformation *displacementDofTransformation = f.displacementFes->GetElementVDofs(elementId, displacementDofs); mfem::DofTransformation *compactificationDofTransformation = f.compactificationFes->GetElementDofs(elementId, compactificationDofs); enthalpyLocal.GetSubVector(enthalpyDofs, elementEnthalpy); displacementLocal.GetSubVector(displacementDofs, elementDisplacement); f.compactificationCoordinate->GetSubVector(compactificationDofs, elementCompactification); if (enthalpyDofTransformation != nullptr) { enthalpyDofTransformation->InvTransformPrimal(elementEnthalpy); } if (displacementDofTransformation != nullptr) { displacementDofTransformation->InvTransformPrimal(elementDisplacement); } if (compactificationDofTransformation != nullptr) { compactificationDofTransformation->InvTransformPrimal(elementCompactification); } const mean_field::mapping::ElementDisplacementData displacementData = mean_field::mapping::ElementDisplacementDataFromElementVDofs(displacementElement, elementDisplacement); const mean_field::mapping::ElementCompactificationData compactificationData( compactificationElement, elementCompactification ); const mean_field::mapping::ElementMappingData mappingData{ .displacement = displacementData, .compactification = compactificationData }; const mean_field::quadrature::Query query = EnthalpyField::make_query( mean_field::quadrature::QuadratureRole::diagnostic, transformation->OrderW(), std::array{pressureExtraOrder}, mean_field::utils::DOMAINS::STELLAR, mean_field::quadrature::MappingKind::general ); const mean_field::quadrature::MfemRule rule = f.quadratureFactory->get(query, transformation->GetGeometryType()); MFEM_VERIFY(rule.integration_rule != nullptr, "The pressure-integral quadrature rule is null."); enthalpyShape.SetSize(enthalpyElement.GetDof()); for (int quadratureIndex = 0; quadratureIndex < rule.integration_rule->GetNPoints(); ++quadratureIndex) { const mfem::IntegrationPoint &integrationPoint = rule.integration_rule->IntPoint(quadratureIndex); transformation->SetIntPoint(&integrationPoint); const mean_field::mapping::MappingStatus mappingStatus = domainMapper.EvaluateVolume( mappingData, *transformation, integrationPoint, workspace, mappingContext ); MFEM_VERIFY( mappingStatus == mean_field::mapping::MappingStatus::valid, "Stateless mapping failed in the " "independent pressure integral. Element: " << elementId << ", attribute: " << transformation->Attribute << ", quadrature point: " << quadratureIndex << ", status: " << static_cast(mappingStatus) ); enthalpyElement.CalcShape(integrationPoint, enthalpyShape); const double enthalpyValue = elementEnthalpy * enthalpyShape; const double pressureValue = mean_field::eos::evaluate( barotrope, mean_field::eos::SpecificEnthalpyValue{enthalpyValue} ) .value(); const double contribution = pressureValue * mappingContext.quadrature.weight; MFEM_VERIFY( std::isfinite(pressureValue) && std::isfinite(contribution), "The independent pressure integral " "encountered a non-finite value." ); localPressureIntegral += contribution; } } double globalPressureIntegral = 0.0; MPI_Allreduce(&localPressureIntegral, &globalPressureIntegral, 1, MPI_DOUBLE, MPI_SUM, f.mesh->GetComm()); return globalPressureIntegral; } } // namespace pressure_force_kernel_test_utils TEST_CASE( "Pressure Force Residual Vanishes For Zero Enthalpy", tags::barotrope &tags::pressure &tags::kernels &tags::integration ) { 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 mean_field::eos::Polytrope barotrope(3.0, 0.25); mfem::Vector enthalpyTrue(f.enthalpyFes->GetTrueVSize()); enthalpyTrue = 0.0; const mfem::Vector displacementTrue = pressure_force_kernel_test_utils::make_zero_displacement(f); mfem::Vector residualTrue; mean_field::operators::kernels::apply_pressure_force_residual( f, *f.domainMapperStateless, barotrope, enthalpyTrue, displacementTrue, residualTrue ); REQUIRE(residualTrue.Size() == f.displacementFes->GetTrueVSize()); const double residualNorm = gravity_prepared_test_utils::global_norm(residualTrue, f.mesh->GetComm()); CHECK(residualNorm == 0.0); } TEST_CASE( "Pressure Force Residual Excludes Vacuum Enthalpy Exactly", tags::barotrope &tags::pressure &tags::kernels &tags::integration ) { 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 mean_field::eos::Polytrope barotrope(3.0, 0.25); const mfem::Vector enthalpyTrue = pressure_force_kernel_test_utils::make_vacuum_only_enthalpy(f); const double enthalpyNorm = gravity_prepared_test_utils::global_norm(enthalpyTrue, f.mesh->GetComm()); /* * Ensure this is a real exclusion test rather than another * all-zero-input test. */ REQUIRE(enthalpyNorm > 0.0); const mfem::Vector displacementTrue = pressure_force_kernel_test_utils::make_zero_displacement(f); mfem::Vector residualTrue; mean_field::operators::kernels::apply_pressure_force_residual( f, *f.domainMapperStateless, barotrope, enthalpyTrue, displacementTrue, residualTrue ); REQUIRE(residualTrue.Size() == f.displacementFes->GetTrueVSize()); const double residualNorm = gravity_prepared_test_utils::global_norm(residualTrue, f.mesh->GetComm()); CHECK(residualNorm == 0.0); } TEST_CASE( "Pressure Force Residual Is Nonzero For Positive Stellar Pressure", tags::barotrope &tags::pressure &tags::kernels &tags::integration ) { 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 mean_field::eos::Polytrope barotrope(3.0, 0.25); /* * With n = 3 and K = 1/4: * * P(1) = 1/4. */ mfem::Vector enthalpyTrue(f.enthalpyFes->GetTrueVSize()); enthalpyTrue = 1.0; const mfem::Vector displacementTrue = pressure_force_kernel_test_utils::make_zero_displacement(f); mfem::Vector residualTrue; mean_field::operators::kernels::apply_pressure_force_residual( f, *f.domainMapperStateless, barotrope, enthalpyTrue, displacementTrue, residualTrue ); const double residualNorm = gravity_prepared_test_utils::global_norm(residualTrue, f.mesh->GetComm()); INFO("Positive-pressure residual norm = " << residualNorm); CHECK(std::isfinite(residualNorm)); CHECK(residualNorm > 100.0 * std::numeric_limits::epsilon()); } TEST_CASE( "Pressure Force Residual Does No Work Against Rigid Translations", tags::barotrope &tags::pressure &tags::kernels &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()); REQUIRE(f.displacementFes->GetOrdering() == mfem::Ordering::byNODES); const mean_field::eos::Polytrope barotrope(3.0, 0.25); const mfem::Vector enthalpyTrue = pressure_force_kernel_test_utils::make_positive_asymmetric_enthalpy(f); const mfem::Vector displacementTrue = pressure_force_kernel_test_utils::make_zero_displacement(f); mfem::Vector residualTrue; mean_field::operators::kernels::apply_pressure_force_residual( f, *f.domainMapperStateless, barotrope, enthalpyTrue, displacementTrue, residualTrue ); const double residualNorm = gravity_prepared_test_utils::global_norm(residualTrue, f.mesh->GetComm()); REQUIRE(residualNorm > 0.0); const int dimension = f.mesh->Dimension(); for (int component = 0; component < dimension; ++component) { const mfem::Vector translationTrue = pressure_force_kernel_test_utils::make_component_test_field(f, component, -1); const double translationNorm = gravity_prepared_test_utils::global_norm(translationTrue, f.mesh->GetComm()); const double translationWork = pressure_force_kernel_test_utils::global_dot(translationTrue, residualTrue, f.mesh->GetComm()); const double dotProductScale = std::fmax(residualNorm * translationNorm, 1.0); CAPTURE(component, translationWork, dotProductScale); CHECK(std::abs(translationWork) <= 5.0e-12 * dotProductScale); } } TEST_CASE( "Pressure Force Residual Matches Independent Pressure Integral", tags::barotrope &tags::pressure &tags::kernels &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()); REQUIRE(f.displacementFes->GetOrdering() == mfem::Ordering::byNODES); const mean_field::eos::Polytrope barotrope(3.0, 0.25); const mfem::Vector enthalpyTrue = pressure_force_kernel_test_utils::make_positive_asymmetric_enthalpy(f); const mfem::Vector displacementTrue = pressure_force_kernel_test_utils::make_zero_displacement(f); mfem::Vector residualTrue; mean_field::operators::kernels::apply_pressure_force_residual( f, *f.domainMapperStateless, barotrope, enthalpyTrue, displacementTrue, residualTrue ); const int dimension = f.mesh->Dimension(); REQUIRE(dimension == 3); mfem::DenseMatrix virtualWork(dimension, dimension); for (int component = 0; component < dimension; ++component) { for (int coordinate = 0; coordinate < dimension; ++coordinate) { const mfem::Vector affineTestTrue = pressure_force_kernel_test_utils::make_component_test_field(f, component, coordinate); virtualWork(component, coordinate) = pressure_force_kernel_test_utils::global_dot(affineTestTrue, residualTrue, f.mesh->GetComm()); } } const double pressureIntegral = pressure_force_kernel_test_utils::integrate_pressure( f, *f.domainMapperStateless, barotrope, enthalpyTrue, displacementTrue ); REQUIRE(std::isfinite(pressureIntegral)); REQUIRE(pressureIntegral > 100.0 * std::numeric_limits::epsilon()); double meanDiagonalWork = 0.0; for (int component = 0; component < dimension; ++component) { meanDiagonalWork += virtualWork(component, component); } meanDiagonalWork /= static_cast(dimension); const double comparisonTolerance = 1.0e-6 * std::abs(pressureIntegral); INFO("Independent pressure integral = " << pressureIntegral); INFO("Expected diagonal virtual work = " << -pressureIntegral); INFO("Mean diagonal virtual work = " << meanDiagonalWork); INFO("Comparison tolerance = " << comparisonTolerance); /* * This separate mean check gives a compact diagnostic if all three * diagonal components drift together. */ CHECK(std::abs(meanDiagonalWork + pressureIntegral) <= comparisonTolerance); for (int component = 0; component < dimension; ++component) { for (int coordinate = 0; coordinate < dimension; ++coordinate) { const double computedWork = virtualWork(component, coordinate); const double expectedWork = component == coordinate ? -pressureIntegral : 0.0; CAPTURE(component, coordinate, computedWork, expectedWork, pressureIntegral, comparisonTolerance); CHECK(std::abs(computedWork - expectedWork) <= comparisonTolerance); } } const double relativeMeanError = std::abs(meanDiagonalWork + pressureIntegral) / std::abs(pressureIntegral); INFO("Relative mean diagonal error = " << relativeMeanError); CHECK(relativeMeanError <= 1.0e-6); } TEST_CASE( "Pressure Force Residual Matches Deformed Pressure Volume Variation", tags::barotrope &tags::pressure &tags::kernels &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 mean_field::eos::Polytrope barotrope(3.0, 0.25); /* * This field is positive but spatially nonuniform, so the test * exercises a genuinely nonuniform pressure distribution. */ const mfem::Vector enthalpyTrue = pressure_force_kernel_test_utils::make_deterministic_vector(f.enthalpyFes->GetTrueVSize(), 0.37); /* * make_displacement() contains anisotropic diagonal terms and * quadratic cross terms. A scale of 0.67 therefore provides a * nonzero, nonspherical, valid base geometry. */ const mfem::Vector baseDisplacementTrue = gravity_prepared_test_utils::make_displacement(f, 0.67); /* * Differentiate along the same smooth deformation family. Thus * * d(epsilon) = (0.67 + epsilon) d_shape. * * This gives a controlled geometry path while still evaluating * the derivative at a genuinely deformed base state. */ const mfem::Vector displacementVariationTrue = gravity_prepared_test_utils::make_displacement(f, 1.0); const double baseDisplacementNorm = gravity_prepared_test_utils::global_norm(baseDisplacementTrue, f.mesh->GetComm()); const double variationNorm = gravity_prepared_test_utils::global_norm(displacementVariationTrue, f.mesh->GetComm()); REQUIRE(baseDisplacementNorm > 100.0 * std::numeric_limits::epsilon()); REQUIRE(variationNorm > 100.0 * std::numeric_limits::epsilon()); mfem::Vector residualTrue; mean_field::operators::kernels::apply_pressure_force_residual( f, *f.domainMapperStateless, barotrope, enthalpyTrue, baseDisplacementTrue, residualTrue ); REQUIRE(residualTrue.Size() == f.displacementFes->GetTrueVSize()); const double residualWork = pressure_force_kernel_test_utils::global_dot(displacementVariationTrue, residualTrue, f.mesh->GetComm()); REQUIRE(std::isfinite(residualWork)); REQUIRE(std::abs(residualWork) > 100.0 * std::numeric_limits::epsilon()); /* * The relatively broad initial sweep lets us see the expected * centered-difference convergence before reaching the quadrature * and representation plateau. */ constexpr std::array differenceSteps{1.0e-2, 5.0e-3, 2.5e-3, 1.25e-3}; double bestRelativeDiscrepancy = std::numeric_limits::infinity(); for (const double differenceStep : differenceSteps) { mfem::Vector displacementPlus(baseDisplacementTrue); mfem::Vector displacementMinus(baseDisplacementTrue); displacementPlus.Add(differenceStep, displacementVariationTrue); displacementMinus.Add(-differenceStep, displacementVariationTrue); const double pressureIntegralPlus = pressure_force_kernel_test_utils::integrate_pressure( f, *f.domainMapperStateless, barotrope, enthalpyTrue, displacementPlus ); const double pressureIntegralMinus = pressure_force_kernel_test_utils::integrate_pressure( f, *f.domainMapperStateless, barotrope, enthalpyTrue, displacementMinus ); REQUIRE(std::isfinite(pressureIntegralPlus)); REQUIRE(std::isfinite(pressureIntegralMinus)); const double pressureVolumeDerivative = (pressureIntegralPlus - pressureIntegralMinus) / (2.0 * differenceStep); REQUIRE(std::isfinite(pressureVolumeDerivative)); double comparisonScale = std::abs(residualWork); if (std::abs(pressureVolumeDerivative) > comparisonScale) { comparisonScale = std::abs(pressureVolumeDerivative); } REQUIRE(comparisonScale > 100.0 * std::numeric_limits::epsilon()); const double absoluteDiscrepancy = std::abs(residualWork + pressureVolumeDerivative); const double relativeDiscrepancy = absoluteDiscrepancy / comparisonScale; if (relativeDiscrepancy < bestRelativeDiscrepancy) { bestRelativeDiscrepancy = relativeDiscrepancy; } INFO("Difference step = " << differenceStep); INFO("Pressure residual work = " << residualWork); INFO("Pressure-volume derivative = " << pressureVolumeDerivative); INFO("Residual work plus derivative = " << residualWork + pressureVolumeDerivative); INFO("Relative discrepancy = " << relativeDiscrepancy); /* * The signs must be opposite because the implemented pressure * force is the negative variation of the pressure-volume * functional. */ CHECK(residualWork * pressureVolumeDerivative < 0.0); } INFO("Best pressure-volume relative discrepancy = " << bestRelativeDiscrepancy); /* * This is intentionally a provisional but meaningful threshold. * We will tighten it after measuring the convergence plateau. */ CHECK(bestRelativeDiscrepancy < 1.0e-8); }