Files
MeanField/tests/deformation/radial_extensions.cpp

492 lines
25 KiB
C++
Raw Permalink Normal View History

#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)})
);
}