feat(surface): surface deformation prescriptions

restricted the unknown state vector to surface deformation and implemented one prescription, NodalRadialSurface, while the full volumetric displacment field is reconstructed analytically from that. This reduced the number of degrees of freedom in the system by a factor of 80 while also removing many null vectors from the system.
This commit is contained in:
2026-09-01 11:50:13 -04:00
parent 0a7f18c5c7
commit 85500fef3b
40 changed files with 8924 additions and 1164 deletions

View File

@@ -0,0 +1,491 @@
#include <algorithm>
#include <cmath>
#include <limits>
#include <stdexcept>
#include <utility>
#include <catch2/catch_test_macros.hpp>
#include <mfem.hpp>
#include <mpi.h>
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<Schema>(*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<double>::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<deformation::PowerLawRadialInteriorExtension>);
STATIC_CHECK(
deformation::PreparedInteriorDeformationExtension<deformation::PreparedPowerLawRadialInteriorExtension>
);
STATIC_CHECK(deformation::VacuumDeformationExtension<deformation::FixedInfinityRadialVacuumExtension>);
STATIC_CHECK(
deformation::PreparedVacuumDeformationExtension<deformation::PreparedFixedInfinityRadialVacuumExtension>
);
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<double>::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<radial_extension_test_utils::Schema>(
*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<domain::StellarSurface, radial_extension_test_utils::Schema>(
*fem.surfaceDeformationFes
);
const field::ScalarBoundaryDofMap infinitySurfaceMap =
field::make_scalar_boundary_dof_map<domain::InfinitySurface, radial_extension_test_utils::Schema>(
*fem.surfaceDeformationFes
);
CHECK_THROWS_AS(
deformation::RadialDeformationExtensionCompilationContext(
*fem.surfaceDeformationFes, *fem.displacementFes, *fem.logicalReferenceMesh, stellarSurfaceMap,
stellarSurfaceMap,
domain::make_attribute_marker<domain::Stellar, radial_extension_test_utils::Schema>(*fem.mesh),
domain::make_attribute_marker<domain::Vacuum, radial_extension_test_utils::Schema>(*fem.mesh),
radial_extension_test_utils::Schema::template boundary_attribute<domain::StellarSurface>(),
radial_extension_test_utils::Schema::template boundary_attribute<domain::StellarSurface>()
),
std::invalid_argument
);
CHECK_THROWS_AS(
deformation::makeRadialDeformationExtensionCompilationContext<radial_extension_test_utils::Schema>(
*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<double>::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<int> stellarMarker =
domain::make_attribute_marker<domain::Stellar, radial_extension_test_utils::Schema>(*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<double>(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<double>::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<radial_extension_test_utils::Schema>(
*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<double>(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<double>(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<radial_extension_test_utils::Schema>(
*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<double>(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<double>(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)})
);
}