#include #include #include #include #include #include #include #include import mean_field; import test_helpers; namespace radial_extension_test_utils { namespace deformation = mean_field::deformation; namespace domain = mean_field::utils::domain; namespace field = mean_field::field; using Schema = domain::CoreEnvelopeVacuumDomainSchema; [[nodiscard]] mfem::Vector referenceCenter(const int spatialDimension) { mfem::Vector center(spatialDimension); center = 0.0; return center; } [[nodiscard]] deformation::PreparedNodalRadialSurface makePreparedSurface(const mean_field::fem::FEM &fem) { const field::ScalarBoundaryDofMap surfaceDofMap = field::make_stellar_surface_scalar_dof_map(*fem.surfaceDeformationFes); const deformation::SurfaceDeformationCompilationContext context{*fem.surfaceDeformationFes, surfaceDofMap}; return deformation::compileSurfaceDeformationPrescription( deformation::NodalRadialSurface{referenceCenter(fem.mesh->SpaceDimension())}, context ); } [[nodiscard]] double globalInnerProduct( const mfem::Vector &first, const mfem::Vector &second, MPI_Comm communicator ) { REQUIRE(first.Size() == second.Size()); const double local = first * second; double global = 0.0; MPI_Allreduce(&local, &global, 1, MPI_DOUBLE, MPI_SUM, communicator); return global; } [[nodiscard]] double relativeError( const mfem::Vector &actual, const mfem::Vector &expected ) { REQUIRE(actual.Size() == expected.Size()); mfem::Vector difference(actual); difference -= expected; return difference.Norml2() / std::max(expected.Norml2(), std::numeric_limits::epsilon()); } [[nodiscard]] int mfemByNodesVectorDof( const int scalarTrueDof, const int component, const int scalarTrueDofCount ) { return scalarTrueDof + component * scalarTrueDofCount; } } // namespace radial_extension_test_utils TEST_CASE( "Radial Interior And Vacuum Extensions Advertise Closed Form Boundary Behavior", tags::radial_deformation_extension_validation ) { namespace deformation = mean_field::deformation; STATIC_CHECK(deformation::InteriorDeformationExtension); STATIC_CHECK( deformation::PreparedInteriorDeformationExtension ); STATIC_CHECK(deformation::VacuumDeformationExtension); STATIC_CHECK( deformation::PreparedVacuumDeformationExtension ); const deformation::PowerLawRadialInteriorExtension interior; const deformation::InteriorDeformationExtensionDescriptor interiorDescriptor = interior.descriptor(); CHECK(interior.radialPower() == 2.0); CHECK(interiorDescriptor.name == "PowerLawRadialInteriorExtension"); CHECK(interiorDescriptor.linearOnReferenceGeometry); CHECK(interiorDescriptor.requiresRadialFoliation); CHECK_FALSE(interiorDescriptor.requiresAuxiliarySolve); CHECK(interiorDescriptor.supportsExactNewtonLinearization()); CHECK(interiorDescriptor.centerBehavior == deformation::InteriorCenterBehavior::FixedAtReferenceCenter); const deformation::FixedInfinityRadialVacuumExtension vacuum; const deformation::VacuumDeformationExtensionDescriptor vacuumDescriptor = vacuum.descriptor(); CHECK(vacuumDescriptor.name == "FixedInfinityRadialVacuumExtension"); CHECK(vacuumDescriptor.linearOnReferenceGeometry); CHECK(vacuumDescriptor.requiresRadialFoliation); CHECK_FALSE(vacuumDescriptor.requiresAuxiliarySolve); CHECK(vacuumDescriptor.supportsExactNewtonLinearization()); CHECK(vacuumDescriptor.outerBoundaryBehavior == deformation::VacuumOuterBoundaryBehavior::FixedAtReferenceInfinity); CHECK_THROWS_AS(deformation::PowerLawRadialInteriorExtension{0.5}, std::invalid_argument); CHECK_THROWS_AS( deformation::PowerLawRadialInteriorExtension{std::numeric_limits::infinity()}, std::invalid_argument ); } TEST_CASE( "Radial Extensions Reproduce The Stellar Surface Fix Reference Infinity And Preserve Positive Volume Maps", tags::radial_deformation_extension_analytic &tags::radial_deformation_extension_mapping ) { namespace deformation = mean_field::deformation; namespace domain = mean_field::utils::domain; namespace field = mean_field::field; mean_field::utils::Args args = test_utils::setup_args(); mean_field::fem::FEM fem = mean_field::fem::setup_fem(args.mesh_file, args, 0); REQUIRE(fem.okay()); const deformation::PreparedNodalRadialSurface surface = radial_extension_test_utils::makePreparedSurface(fem); const deformation::RadialDeformationExtensionCompilationContext context = deformation::makeRadialDeformationExtensionCompilationContext( *fem.surfaceDeformationFes, *fem.displacementFes, *fem.logicalReferenceMesh ); const deformation::PreparedPowerLawRadialInteriorExtension interior = deformation::compileInteriorDeformationExtension(deformation::PowerLawRadialInteriorExtension{}, context); const deformation::PreparedPowerLawRadialInteriorExtension cubicInterior = deformation::compileInteriorDeformationExtension(deformation::PowerLawRadialInteriorExtension{3.0}, context); const deformation::PreparedFixedInfinityRadialVacuumExtension vacuum = deformation::compileVacuumDeformationExtension(deformation::FixedInfinityRadialVacuumExtension{}, context); REQUIRE(interior.surfaceDisplacementSize() == surface.surfaceDisplacementSize()); REQUIRE(vacuum.surfaceDisplacementSize() == surface.surfaceDisplacementSize()); REQUIRE(interior.interiorDisplacementSize() == fem.displacementFes->GetTrueVSize()); REQUIRE(vacuum.vacuumDisplacementSize() == fem.displacementFes->GetTrueVSize()); constexpr double surfaceAmplitude = 0.02; mfem::Vector parameters(surface.parameterCount()); parameters = surfaceAmplitude; mfem::Vector surfaceDisplacement(surface.surfaceDisplacementSize()); surface.buildSurfaceDisplacement(parameters, surfaceDisplacement); mfem::Vector interiorDisplacement(interior.interiorDisplacementSize()); mfem::Vector vacuumDisplacement(vacuum.vacuumDisplacementSize()); interior.buildInteriorDisplacement(surfaceDisplacement, interiorDisplacement); vacuum.buildVacuumDisplacement(surfaceDisplacement, vacuumDisplacement); const int spatialDimension = fem.mesh->SpaceDimension(); const field::ScalarBoundaryDofMap stellarSurfaceMap = field::make_scalar_boundary_dof_map( *fem.surfaceDeformationFes ); const field::ScalarBoundaryDofMap infinitySurfaceMap = field::make_scalar_boundary_dof_map( *fem.surfaceDeformationFes ); CHECK_THROWS_AS( deformation::RadialDeformationExtensionCompilationContext( *fem.surfaceDeformationFes, *fem.displacementFes, *fem.logicalReferenceMesh, stellarSurfaceMap, stellarSurfaceMap, domain::make_attribute_marker(*fem.mesh), domain::make_attribute_marker(*fem.mesh), radial_extension_test_utils::Schema::template boundary_attribute(), radial_extension_test_utils::Schema::template boundary_attribute() ), std::invalid_argument ); CHECK_THROWS_AS( deformation::makeRadialDeformationExtensionCompilationContext( *fem.surfaceDeformationFes, *fem.displacementFes, *fem.mesh ), std::invalid_argument ); const double stellarSurfaceRadius = context.stellarSurfaceLogicalRadius(); const double infinitySurfaceRadius = context.infinitySurfaceLogicalRadius(); REQUIRE(stellarSurfaceRadius > 0.0); REQUIRE(infinitySurfaceRadius > stellarSurfaceRadius); constexpr double tolerance = 2.0e-11; bool hasInterpolatedSurfacePoint = false; for (int scalarDof = 0; scalarDof < interior.scalarTrueDofCount(); ++scalarDof) { const double referenceRadius = context.logicalRadius(scalarDof); const int interpolationEntryCount = context.surfaceInterpolationEntryCount(scalarDof); if (interpolationEntryCount == 0) { CHECK(referenceRadius <= 64.0 * std::numeric_limits::epsilon() * infinitySurfaceRadius); } else { double interpolationWeightSum = 0.0; for (int entry = 0; entry < interpolationEntryCount; ++entry) { const int surfaceCoordinate = context.surfaceGlobalCoordinate(scalarDof, entry); CHECK(surfaceCoordinate >= 0); CHECK(surfaceCoordinate < stellarSurfaceMap.global_size()); interpolationWeightSum += context.surfaceInterpolationWeight(scalarDof, entry); } CHECK(std::abs(interpolationWeightSum - 1.0) <= tolerance); hasInterpolatedSurfacePoint |= interpolationEntryCount > 1; } if (interior.hasStellarSupport(scalarDof)) { const double expectedWeight = referenceRadius == 0.0 ? 0.0 : std::pow(referenceRadius / stellarSurfaceRadius, 2.0); CHECK(std::abs(interior.radialWeight(scalarDof) - expectedWeight) <= tolerance); const double expectedCubicWeight = referenceRadius == 0.0 ? 0.0 : std::pow(referenceRadius / stellarSurfaceRadius, 3.0); CHECK(std::abs(cubicInterior.radialWeight(scalarDof) - expectedCubicWeight) <= tolerance); } else { CHECK(interior.radialWeight(scalarDof) == 0.0); for (int component = 0; component < spatialDimension; ++component) { const int volumeVectorDof = radial_extension_test_utils::mfemByNodesVectorDof( scalarDof, component, interior.scalarTrueDofCount() ); CHECK(interiorDisplacement(volumeVectorDof) == 0.0); } } if (vacuum.hasVacuumSupport(scalarDof)) { const double expectedWeight = (infinitySurfaceRadius - referenceRadius) / (infinitySurfaceRadius - stellarSurfaceRadius); CHECK(std::abs(vacuum.radialWeight(scalarDof) - expectedWeight) <= tolerance); } else { CHECK(vacuum.radialWeight(scalarDof) == 0.0); for (int component = 0; component < spatialDimension; ++component) { const int volumeVectorDof = radial_extension_test_utils::mfemByNodesVectorDof( scalarDof, component, vacuum.scalarTrueDofCount() ); CHECK(vacuumDisplacement(volumeVectorDof) == 0.0); } } } CHECK(hasInterpolatedSurfacePoint); const double componentValues[3]{1.25, -0.75, 2.5}; mfem::Vector constantSurfaceDisplacement(surface.surfaceDisplacementSize()); for (int surfaceDof = 0; surfaceDof < surface.parameterCount(); ++surfaceDof) { for (int component = 0; component < spatialDimension; ++component) { constantSurfaceDisplacement(spatialDimension * surfaceDof + component) = componentValues[component]; } } mfem::Vector constantInteriorDisplacement(interior.interiorDisplacementSize()); interior.buildInteriorDisplacement(constantSurfaceDisplacement, constantInteriorDisplacement); mfem::Vector radialWeightTrueDofs(interior.scalarTrueDofCount()); for (int scalarDof = 0; scalarDof < interior.scalarTrueDofCount(); ++scalarDof) { radialWeightTrueDofs(scalarDof) = interior.hasStellarSupport(scalarDof) ? interior.radialWeight(scalarDof) : 0.0; } mfem::ParGridFunction radialWeightField(fem.surfaceDeformationFes.get()); mfem::ParGridFunction constantVectorField(fem.displacementFes.get()); radialWeightField.SetFromTrueDofs(radialWeightTrueDofs); constantVectorField.SetFromTrueDofs(constantInteriorDisplacement); const mfem::Array stellarMarker = domain::make_attribute_marker(*fem.mesh); int sampledStellarElement = -1; for (int element = 0; element < fem.mesh->GetNE() && sampledStellarElement < 0; ++element) { const int attribute = fem.mesh->GetAttribute(element); if (attribute > 0 && attribute <= stellarMarker.Size() && stellarMarker[attribute - 1] != 0) { sampledStellarElement = element; } } REQUIRE(sampledStellarElement >= 0); const mfem::IntegrationPoint &samplePoint = mfem::Geometries.GetCenter(fem.mesh->GetElementBaseGeometry(sampledStellarElement)); const double sampledRadialWeight = radialWeightField.GetValue(sampledStellarElement, samplePoint); mfem::Vector sampledVector(spatialDimension); constantVectorField.GetVectorValue(sampledStellarElement, samplePoint, sampledVector); for (int component = 0; component < spatialDimension; ++component) { CHECK(std::abs(sampledVector(component) - componentValues[component] * sampledRadialWeight) <= tolerance); } mfem::Vector arbitrarySurfaceDisplacement(surface.surfaceDisplacementSize()); for (int dof = 0; dof < arbitrarySurfaceDisplacement.Size(); ++dof) { const double index = static_cast(dof + 1); arbitrarySurfaceDisplacement(dof) = 0.03 * std::sin(0.29 * index) - 0.01 * std::cos(0.17 * index); } mfem::Vector arbitraryInteriorDisplacement(interior.interiorDisplacementSize()); mfem::Vector arbitraryVacuumDisplacement(vacuum.vacuumDisplacementSize()); interior.buildInteriorDisplacement(arbitrarySurfaceDisplacement, arbitraryInteriorDisplacement); vacuum.buildVacuumDisplacement(arbitrarySurfaceDisplacement, arbitraryVacuumDisplacement); for (int surfaceDof = 0; surfaceDof < stellarSurfaceMap.local_size(); ++surfaceDof) { const int scalarDof = stellarSurfaceMap.volume_true_dof(surfaceDof); for (int component = 0; component < spatialDimension; ++component) { const int surfaceVectorDof = spatialDimension * surfaceDof + component; const int volumeVectorDof = radial_extension_test_utils::mfemByNodesVectorDof(scalarDof, component, interior.scalarTrueDofCount()); CHECK( std::abs( arbitraryInteriorDisplacement(volumeVectorDof) - arbitrarySurfaceDisplacement(surfaceVectorDof) ) <= tolerance ); CHECK( std::abs( arbitraryVacuumDisplacement(volumeVectorDof) - arbitrarySurfaceDisplacement(surfaceVectorDof) ) <= tolerance ); } } for (int infinityDof = 0; infinityDof < infinitySurfaceMap.local_size(); ++infinityDof) { const int scalarDof = infinitySurfaceMap.volume_true_dof(infinityDof); for (int component = 0; component < spatialDimension; ++component) { const int volumeVectorDof = radial_extension_test_utils::mfemByNodesVectorDof(scalarDof, component, vacuum.scalarTrueDofCount()); CHECK(std::abs(vacuumDisplacement(volumeVectorDof)) <= tolerance); } } mfem::Vector combinedDisplacement(interiorDisplacement); for (int scalarDof = 0; scalarDof < vacuum.scalarTrueDofCount(); ++scalarDof) { if (!vacuum.hasVacuumSupport(scalarDof) || interior.hasStellarSupport(scalarDof)) { continue; } for (int component = 0; component < spatialDimension; ++component) { const int volumeVectorDof = radial_extension_test_utils::mfemByNodesVectorDof(scalarDof, component, vacuum.scalarTrueDofCount()); combinedDisplacement(volumeVectorDof) = vacuumDisplacement(volumeVectorDof); } } mfem::ParGridFunction displacement(fem.displacementFes.get()); displacement.SetFromTrueDofs(combinedDisplacement); double localMinimumDeterminant = std::numeric_limits::infinity(); for (int element = 0; element < fem.mesh->GetNE(); ++element) { mfem::ElementTransformation *transformation = fem.mesh->GetElementTransformation(element); const mfem::FiniteElement *finiteElement = fem.displacementFes->GetFE(element); const mfem::IntegrationRule &rule = mfem::IntRules.Get(transformation->GetGeometryType(), finiteElement->GetOrder() + 2); for (int point = 0; point < rule.GetNPoints(); ++point) { transformation->SetIntPoint(&rule.IntPoint(point)); mfem::DenseMatrix displacementGradient; displacement.GetVectorGradient(*transformation, displacementGradient); for (int component = 0; component < spatialDimension; ++component) { displacementGradient(component, component) += 1.0; } localMinimumDeterminant = std::min(localMinimumDeterminant, displacementGradient.Det()); } } double globalMinimumDeterminant = 0.0; MPI_Allreduce(&localMinimumDeterminant, &globalMinimumDeterminant, 1, MPI_DOUBLE, MPI_MIN, fem.mesh->GetComm()); CHECK(globalMinimumDeterminant > 0.0); } TEST_CASE( "Radial Extension Jacobians Match Centered Differences And Their Transposes Preserve Virtual Work", tags::radial_deformation_extension_linearization ) { namespace deformation = mean_field::deformation; mean_field::utils::Args args = test_utils::setup_args(); mean_field::fem::FEM fem = mean_field::fem::setup_fem(args.mesh_file, args, 0); REQUIRE(fem.okay()); const deformation::RadialDeformationExtensionCompilationContext context = deformation::makeRadialDeformationExtensionCompilationContext( *fem.surfaceDeformationFes, *fem.displacementFes, *fem.logicalReferenceMesh ); const deformation::PreparedPowerLawRadialInteriorExtension interior = deformation::compileInteriorDeformationExtension(deformation::PowerLawRadialInteriorExtension{}, context); const deformation::PreparedFixedInfinityRadialVacuumExtension vacuum = deformation::compileVacuumDeformationExtension(deformation::FixedInfinityRadialVacuumExtension{}, context); mfem::Vector surface(interior.surfaceDisplacementSize()); mfem::Vector direction(interior.surfaceDisplacementSize()); for (int dof = 0; dof < surface.Size(); ++dof) { const double index = static_cast(dof + 1); surface(dof) = 0.01 * std::sin(0.17 * index); direction(dof) = std::cos(0.13 * index) - 0.2 * std::sin(0.31 * index); } constexpr double step = 1.0e-6; mfem::Vector plusSurface(surface); mfem::Vector minusSurface(surface); plusSurface.Add(step, direction); minusSurface.Add(-step, direction); auto checkLinearization = [&](const auto &prepared, const int volumeSize, const auto &build) { mfem::Vector plus(volumeSize); mfem::Vector minus(volumeSize); mfem::Vector jacobian(volumeSize); build(prepared, plusSurface, plus); build(prepared, minusSurface, minus); prepared.applyJacobian(surface, direction, jacobian); mfem::Vector centeredDifference(plus); centeredDifference -= minus; centeredDifference /= 2.0 * step; CHECK(radial_extension_test_utils::relativeError(jacobian, centeredDifference) < 2.0e-10); mfem::Vector volumeDual(volumeSize); for (int dof = 0; dof < volumeDual.Size(); ++dof) { const double index = static_cast(dof + 1); volumeDual(dof) = std::sin(0.07 * index) + 0.3 * std::cos(0.11 * index); } mfem::Vector surfaceDual(surface.Size()); prepared.applyJacobianTranspose(surface, volumeDual, surfaceDual); const double volumeWork = radial_extension_test_utils::globalInnerProduct(jacobian, volumeDual, fem.mesh->GetComm()); const double surfaceWork = radial_extension_test_utils::globalInnerProduct(direction, surfaceDual, fem.mesh->GetComm()); const double scale = std::max({1.0, std::abs(volumeWork), std::abs(surfaceWork)}); CHECK(std::abs(volumeWork - surfaceWork) <= 5.0e-13 * scale); mfem::Vector pullback(surface.Size()); pullback = 1.0; prepared.applyPullbackDerivative(surface, direction, volumeDual, pullback); CHECK(pullback.Norml2() == 0.0); }; checkLinearization( interior, interior.interiorDisplacementSize(), [](const auto &prepared, const mfem::Vector &input, mfem::Vector &output) { prepared.buildInteriorDisplacement(input, output); } ); checkLinearization( vacuum, vacuum.vacuumDisplacementSize(), [](const auto &prepared, const mfem::Vector &input, mfem::Vector &output) { prepared.buildVacuumDisplacement(input, output); } ); mfem::Vector wrongSurface(surface.Size() + 1); mfem::Vector interiorOutput(interior.interiorDisplacementSize()); mfem::Vector vacuumOutput(vacuum.vacuumDisplacementSize()); CHECK_THROWS_AS(interior.buildInteriorDisplacement(wrongSurface, interiorOutput), std::invalid_argument); CHECK_THROWS_AS(vacuum.buildVacuumDisplacement(wrongSurface, vacuumOutput), std::invalid_argument); } TEST_CASE( "Logical Radial Deformation Remains Conforming And Orientation Preserving After Mesh Refinement", tags::radial_deformation_extension_mapping ) { namespace deformation = mean_field::deformation; mean_field::utils::Args args = test_utils::setup_args(); mean_field::fem::FEM fem = mean_field::fem::setup_fem(args.mesh_file, args, 1); REQUIRE(fem.okay()); deformation::PreparedNodalRadialSurface surface = radial_extension_test_utils::makePreparedSurface(fem); const deformation::RadialDeformationExtensionCompilationContext context = deformation::makeRadialDeformationExtensionCompilationContext( *fem.surfaceDeformationFes, *fem.displacementFes, *fem.logicalReferenceMesh ); deformation::PreparedPowerLawRadialInteriorExtension interior = deformation::compileInteriorDeformationExtension(deformation::PowerLawRadialInteriorExtension{}, context); deformation::PreparedFixedInfinityRadialVacuumExtension vacuum = deformation::compileVacuumDeformationExtension(deformation::FixedInfinityRadialVacuumExtension{}, context); auto prepared = deformation::composePreparedDomainDeformation( std::move(surface), std::move(interior), std::move(vacuum), *fem.surfaceDeformationFes, *fem.displacementFes, *fem.logicalReferenceMesh ); mfem::Vector parameters(prepared.parameterCount()); mfem::Vector direction(prepared.parameterCount()); for (int parameter = 0; parameter < parameters.Size(); ++parameter) { const double index = static_cast(parameter + 1); const double polarDirection = prepared.surfaceDeformationPrescription().radialDirection(parameter, 2); const double quadrupoleValue = 0.5 * (3.0 * polarDirection * polarDirection - 1.0); parameters(parameter) = 0.006 - 0.001 * quadrupoleValue; direction(parameter) = std::sin(0.07 * index) - 0.4 * std::cos(0.13 * index); } mfem::Vector volumeDisplacement(prepared.volumeDisplacementSize()); prepared.buildVolumeDisplacement(parameters, volumeDisplacement); const deformation::DomainDeformationGeometryReport geometry = prepared.inspectMappedGeometry(volumeDisplacement); CAPTURE(geometry.minimumJacobianDeterminant); REQUIRE(geometry.isOrientationPreserving()); mfem::Vector jacobianAction(prepared.volumeDisplacementSize()); mfem::Vector volumeDual(prepared.volumeDisplacementSize()); for (int dof = 0; dof < volumeDual.Size(); ++dof) { const double index = static_cast(dof + 1); volumeDual(dof) = std::cos(0.017 * index) + 0.2 * std::sin(0.023 * index); } prepared.applyJacobian(parameters, direction, jacobianAction); mfem::Vector parameterDual(prepared.parameterCount()); prepared.applyJacobianTranspose(parameters, volumeDual, parameterDual); const double volumeWork = radial_extension_test_utils::globalInnerProduct(jacobianAction, volumeDual, fem.mesh->GetComm()); const double parameterWork = radial_extension_test_utils::globalInnerProduct(direction, parameterDual, fem.mesh->GetComm()); CHECK( std::abs(volumeWork - parameterWork) <= 2.0e-12 * std::max({1.0, std::abs(volumeWork), std::abs(parameterWork)}) ); }