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,608 @@
#include <concepts>
#include <catch2/catch_test_macros.hpp>
#include <mfem.hpp>
import mean_field;
import test_helpers;
namespace {
struct DeformationCompilationContext final { };
constexpr mean_field::deformation::SurfaceDeformationDescriptor surfaceDescriptor{
.name = "TestRadialSurface",
.spatialDimension = 3,
.motionKind = mean_field::deformation::SurfaceMotionKind::Radial,
.linearOnReferenceGeometry = true,
.requiresStarShapedReferenceSurface = true,
.hasExactDerivativeTranspose = true,
.hasExactPullbackDerivative = true,
.translationTreatment = mean_field::deformation::GeometricGaugeTreatment::ExcludedByParameterization,
.orientationTreatment = mean_field::deformation::GeometricGaugeTreatment::ExcludedByParameterization
};
constexpr mean_field::deformation::InteriorDeformationExtensionDescriptor interiorDescriptor{
.name = "TestRadialInteriorExtension",
.spatialDimension = 3,
.linearOnReferenceGeometry = true,
.requiresRadialFoliation = true,
.requiresAuxiliarySolve = false,
.hasExactDerivativeTranspose = true,
.hasExactPullbackDerivative = true,
.centerBehavior = mean_field::deformation::InteriorCenterBehavior::FixedAtReferenceCenter
};
constexpr mean_field::deformation::VacuumDeformationExtensionDescriptor vacuumDescriptor{
.name = "TestFixedInfinityExtension",
.spatialDimension = 3,
.linearOnReferenceGeometry = true,
.requiresRadialFoliation = true,
.requiresAuxiliarySolve = false,
.hasExactDerivativeTranspose = true,
.hasExactPullbackDerivative = true,
.outerBoundaryBehavior = mean_field::deformation::VacuumOuterBoundaryBehavior::FixedAtReferenceInfinity
};
template <
bool HasBuild = true,
bool HasJacobian = true,
bool HasJacobianTranspose = true,
bool HasPullbackDerivative = true>
class PreparedSurfaceFixture final {
public:
[[nodiscard]] mean_field::deformation::SurfaceDeformationDescriptor descriptor() const noexcept {
return surfaceDescriptor;
}
[[nodiscard]] int parameterCount() const noexcept {
return 4;
}
[[nodiscard]] int surfaceDisplacementSize() const noexcept {
return 12;
}
void buildSurfaceDisplacement(
const mfem::Vector &,
mfem::Vector &
) const
requires HasBuild
{
}
void applyJacobian(
const mfem::Vector &,
const mfem::Vector &,
mfem::Vector &
) const
requires HasJacobian
{
}
void applyJacobianTranspose(
const mfem::Vector &,
const mfem::Vector &,
mfem::Vector &
) const
requires HasJacobianTranspose
{
}
void applyPullbackDerivative(
const mfem::Vector &,
const mfem::Vector &,
const mfem::Vector &,
mfem::Vector &
) const
requires HasPullbackDerivative
{
}
};
template <typename Prepared, bool HasDescriptor = true, bool HasValidation = true, bool HasCompilation = true>
class SurfacePrescriptionFixture final {
public:
using PreparedType = Prepared;
[[nodiscard]] mean_field::deformation::SurfaceDeformationDescriptor descriptor() const noexcept
requires HasDescriptor
{
return surfaceDescriptor;
}
void validate() const
requires HasValidation
{
}
static constexpr bool hasCompilation = HasCompilation;
};
template <
typename Prepared,
bool HasDescriptor,
bool HasValidation,
bool HasCompilation>
requires HasCompilation
Prepared compileSurfaceDeformationPrescription(
const SurfacePrescriptionFixture<
Prepared,
HasDescriptor,
HasValidation,
HasCompilation> &,
const DeformationCompilationContext &
) {
return {};
}
template <
bool HasBuild = true,
bool HasJacobian = true,
bool HasJacobianTranspose = true,
bool HasPullbackDerivative = true,
bool HasScalarDofCount = true,
bool HasSupportQuery = true>
class PreparedInteriorExtensionFixture final {
public:
[[nodiscard]] mean_field::deformation::InteriorDeformationExtensionDescriptor descriptor() const noexcept {
return interiorDescriptor;
}
[[nodiscard]] int surfaceDisplacementSize() const noexcept {
return 12;
}
[[nodiscard]] int interiorDisplacementSize() const noexcept {
return 24;
}
[[nodiscard]] int scalarTrueDofCount() const noexcept
requires HasScalarDofCount
{
return 8;
}
[[nodiscard]] bool hasStellarSupport(const int) const
requires HasSupportQuery
{
return true;
}
void buildInteriorDisplacement(
const mfem::Vector &,
mfem::Vector &
) const
requires HasBuild
{
}
void applyJacobian(
const mfem::Vector &,
const mfem::Vector &,
mfem::Vector &
) const
requires HasJacobian
{
}
void applyJacobianTranspose(
const mfem::Vector &,
const mfem::Vector &,
mfem::Vector &
) const
requires HasJacobianTranspose
{
}
void applyPullbackDerivative(
const mfem::Vector &,
const mfem::Vector &,
const mfem::Vector &,
mfem::Vector &
) const
requires HasPullbackDerivative
{
}
};
template <typename Prepared, bool HasDescriptor = true, bool HasValidation = true, bool HasCompilation = true>
class InteriorExtensionFixture final {
public:
using PreparedType = Prepared;
[[nodiscard]] mean_field::deformation::InteriorDeformationExtensionDescriptor descriptor() const noexcept
requires HasDescriptor
{
return interiorDescriptor;
}
void validate() const
requires HasValidation
{
}
static constexpr bool hasCompilation = HasCompilation;
};
template <
typename Prepared,
bool HasDescriptor,
bool HasValidation,
bool HasCompilation>
requires HasCompilation
Prepared compileInteriorDeformationExtension(
const InteriorExtensionFixture<
Prepared,
HasDescriptor,
HasValidation,
HasCompilation> &,
const DeformationCompilationContext &
) {
return {};
}
template <
bool HasBuild = true,
bool HasJacobian = true,
bool HasJacobianTranspose = true,
bool HasPullbackDerivative = true,
bool HasScalarDofCount = true,
bool HasSupportQuery = true>
class PreparedVacuumExtensionFixture final {
public:
[[nodiscard]] mean_field::deformation::VacuumDeformationExtensionDescriptor descriptor() const noexcept {
return vacuumDescriptor;
}
[[nodiscard]] int surfaceDisplacementSize() const noexcept {
return 12;
}
[[nodiscard]] int vacuumDisplacementSize() const noexcept {
return 30;
}
[[nodiscard]] int scalarTrueDofCount() const noexcept
requires HasScalarDofCount
{
return 10;
}
[[nodiscard]] bool hasVacuumSupport(const int) const
requires HasSupportQuery
{
return true;
}
void buildVacuumDisplacement(
const mfem::Vector &,
mfem::Vector &
) const
requires HasBuild
{
}
void applyJacobian(
const mfem::Vector &,
const mfem::Vector &,
mfem::Vector &
) const
requires HasJacobian
{
}
void applyJacobianTranspose(
const mfem::Vector &,
const mfem::Vector &,
mfem::Vector &
) const
requires HasJacobianTranspose
{
}
void applyPullbackDerivative(
const mfem::Vector &,
const mfem::Vector &,
const mfem::Vector &,
mfem::Vector &
) const
requires HasPullbackDerivative
{
}
};
template <typename Prepared, bool HasDescriptor = true, bool HasValidation = true, bool HasCompilation = true>
class VacuumExtensionFixture final {
public:
using PreparedType = Prepared;
[[nodiscard]] mean_field::deformation::VacuumDeformationExtensionDescriptor descriptor() const noexcept
requires HasDescriptor
{
return vacuumDescriptor;
}
void validate() const
requires HasValidation
{
}
static constexpr bool hasCompilation = HasCompilation;
};
template <
typename Prepared,
bool HasDescriptor,
bool HasValidation,
bool HasCompilation>
requires HasCompilation
Prepared compileVacuumDeformationExtension(
const VacuumExtensionFixture<
Prepared,
HasDescriptor,
HasValidation,
HasCompilation> &,
const DeformationCompilationContext &
) {
return {};
}
using CompletePreparedSurface = PreparedSurfaceFixture<>;
using CompleteSurfacePrescription = SurfacePrescriptionFixture<CompletePreparedSurface>;
using CompletePreparedInteriorExtension = PreparedInteriorExtensionFixture<>;
using CompleteInteriorExtension = InteriorExtensionFixture<CompletePreparedInteriorExtension>;
using CompletePreparedVacuumExtension = PreparedVacuumExtensionFixture<>;
using CompleteVacuumExtension = VacuumExtensionFixture<CompletePreparedVacuumExtension>;
constexpr mean_field::deformation::DomainDeformationDescriptor domainDescriptor{
.surfaceDeformation = surfaceDescriptor,
.stellarInteriorExtension = interiorDescriptor,
.vacuumExtension = vacuumDescriptor,
.linearOnReferenceGeometry = true,
.requiresAuxiliarySolve = false,
.hasExactDerivativeTranspose = true,
.hasExactPullbackDerivative = true
};
template <
bool HasBuild = true,
bool HasJacobian = true,
bool HasJacobianTranspose = true,
bool HasPullbackDerivative = true>
class PreparedDomainDeformationFixture final {
public:
[[nodiscard]] mean_field::deformation::DomainDeformationDescriptor descriptor() const noexcept {
return domainDescriptor;
}
[[nodiscard]] int parameterCount() const noexcept {
return 4;
}
[[nodiscard]] int surfaceDisplacementSize() const noexcept {
return 12;
}
[[nodiscard]] int volumeDisplacementSize() const noexcept {
return 24;
}
void buildVolumeDisplacement(
const mfem::Vector &,
mfem::Vector &
) const
requires HasBuild
{
}
void applyJacobian(
const mfem::Vector &,
const mfem::Vector &,
mfem::Vector &
) const
requires HasJacobian
{
}
void applyJacobianTranspose(
const mfem::Vector &,
const mfem::Vector &,
mfem::Vector &
) const
requires HasJacobianTranspose
{
}
void applyPullbackDerivative(
const mfem::Vector &,
const mfem::Vector &,
const mfem::Vector &,
mfem::Vector &
) const
requires HasPullbackDerivative
{
}
};
} // namespace
TEST_CASE(
"Surface Deformation Contracts Require Complete Forward And Pullback Operations",
tags::surface_deformation_type_contract
) {
STATIC_CHECK(mean_field::deformation::PreparedSurfaceDeformationPrescription<CompletePreparedSurface>);
STATIC_CHECK(mean_field::deformation::SurfaceDeformationPrescription<CompleteSurfacePrescription>);
STATIC_CHECK(
mean_field::deformation::SurfaceDeformationCompilable<
CompleteSurfacePrescription, DeformationCompilationContext>
);
STATIC_CHECK_FALSE(mean_field::deformation::PreparedSurfaceDeformationPrescription<PreparedSurfaceFixture<false>>);
STATIC_CHECK_FALSE(
mean_field::deformation::PreparedSurfaceDeformationPrescription<PreparedSurfaceFixture<true, false>>
);
STATIC_CHECK_FALSE(
mean_field::deformation::PreparedSurfaceDeformationPrescription<PreparedSurfaceFixture<true, true, false>>
);
STATIC_CHECK_FALSE(
mean_field::deformation::PreparedSurfaceDeformationPrescription<PreparedSurfaceFixture<true, true, true, false>>
);
STATIC_CHECK_FALSE(
mean_field::deformation::SurfaceDeformationPrescription<
SurfacePrescriptionFixture<CompletePreparedSurface, false>>
);
STATIC_CHECK_FALSE(
mean_field::deformation::SurfaceDeformationPrescription<
SurfacePrescriptionFixture<CompletePreparedSurface, true, false>>
);
STATIC_CHECK_FALSE(
mean_field::deformation::SurfaceDeformationCompilable<
SurfacePrescriptionFixture<CompletePreparedSurface, true, true, false>, DeformationCompilationContext>
);
}
TEST_CASE(
"Interior Extension Contracts Require Complete Forward And Pullback Operations",
tags::interior_deformation_extension_type_contract
) {
STATIC_CHECK(mean_field::deformation::PreparedInteriorDeformationExtension<CompletePreparedInteriorExtension>);
STATIC_CHECK(mean_field::deformation::InteriorDeformationExtension<CompleteInteriorExtension>);
STATIC_CHECK(
mean_field::deformation::InteriorDeformationExtensionCompilable<
CompleteInteriorExtension, DeformationCompilationContext>
);
STATIC_CHECK_FALSE(
mean_field::deformation::PreparedInteriorDeformationExtension<PreparedInteriorExtensionFixture<false>>
);
STATIC_CHECK_FALSE(
mean_field::deformation::PreparedInteriorDeformationExtension<PreparedInteriorExtensionFixture<true, false>>
);
STATIC_CHECK_FALSE(
mean_field::deformation::PreparedInteriorDeformationExtension<
PreparedInteriorExtensionFixture<true, true, false>>
);
STATIC_CHECK_FALSE(
mean_field::deformation::PreparedInteriorDeformationExtension<
PreparedInteriorExtensionFixture<true, true, true, false>>
);
STATIC_CHECK_FALSE(
mean_field::deformation::PreparedInteriorDeformationExtension<
PreparedInteriorExtensionFixture<true, true, true, true, false>>
);
STATIC_CHECK_FALSE(
mean_field::deformation::PreparedInteriorDeformationExtension<
PreparedInteriorExtensionFixture<true, true, true, true, true, false>>
);
STATIC_CHECK_FALSE(mean_field::deformation::PreparedInteriorDeformationExtension<CompletePreparedVacuumExtension>);
STATIC_CHECK_FALSE(
mean_field::deformation::InteriorDeformationExtension<
InteriorExtensionFixture<CompletePreparedInteriorExtension, true, false>>
);
STATIC_CHECK_FALSE(
mean_field::deformation::InteriorDeformationExtensionCompilable<
InteriorExtensionFixture<CompletePreparedInteriorExtension, true, true, false>,
DeformationCompilationContext>
);
}
TEST_CASE(
"Vacuum Extension Contracts Require Complete Forward And Pullback Operations",
tags::vacuum_deformation_extension_type_contract
) {
STATIC_CHECK(mean_field::deformation::PreparedVacuumDeformationExtension<CompletePreparedVacuumExtension>);
STATIC_CHECK(mean_field::deformation::VacuumDeformationExtension<CompleteVacuumExtension>);
STATIC_CHECK(
mean_field::deformation::VacuumDeformationExtensionCompilable<
CompleteVacuumExtension, DeformationCompilationContext>
);
STATIC_CHECK_FALSE(
mean_field::deformation::PreparedVacuumDeformationExtension<PreparedVacuumExtensionFixture<false>>
);
STATIC_CHECK_FALSE(
mean_field::deformation::PreparedVacuumDeformationExtension<PreparedVacuumExtensionFixture<true, false>>
);
STATIC_CHECK_FALSE(
mean_field::deformation::PreparedVacuumDeformationExtension<PreparedVacuumExtensionFixture<true, true, false>>
);
STATIC_CHECK_FALSE(
mean_field::deformation::PreparedVacuumDeformationExtension<
PreparedVacuumExtensionFixture<true, true, true, false>>
);
STATIC_CHECK_FALSE(
mean_field::deformation::PreparedVacuumDeformationExtension<
PreparedVacuumExtensionFixture<true, true, true, true, false>>
);
STATIC_CHECK_FALSE(
mean_field::deformation::PreparedVacuumDeformationExtension<
PreparedVacuumExtensionFixture<true, true, true, true, true, false>>
);
STATIC_CHECK_FALSE(mean_field::deformation::PreparedVacuumDeformationExtension<CompletePreparedInteriorExtension>);
STATIC_CHECK_FALSE(
mean_field::deformation::VacuumDeformationExtension<
VacuumExtensionFixture<CompletePreparedVacuumExtension, false>>
);
STATIC_CHECK_FALSE(
mean_field::deformation::VacuumDeformationExtensionCompilable<
VacuumExtensionFixture<CompletePreparedVacuumExtension, true, true, false>, DeformationCompilationContext>
);
}
TEST_CASE(
"Prepared Domain Deformation Contracts Require Complete Lift And Pullback Operations",
tags::domain_deformation_type_contract
) {
STATIC_CHECK(mean_field::deformation::PreparedDomainDeformationOperator<PreparedDomainDeformationFixture<>>);
STATIC_CHECK_FALSE(
mean_field::deformation::PreparedDomainDeformationOperator<PreparedDomainDeformationFixture<false>>
);
STATIC_CHECK_FALSE(
mean_field::deformation::PreparedDomainDeformationOperator<PreparedDomainDeformationFixture<true, false>>
);
STATIC_CHECK_FALSE(
mean_field::deformation::PreparedDomainDeformationOperator<PreparedDomainDeformationFixture<true, true, false>>
);
STATIC_CHECK_FALSE(
mean_field::deformation::PreparedDomainDeformationOperator<
PreparedDomainDeformationFixture<true, true, true, false>>
);
}
TEST_CASE(
"Deformation Descriptors Report Geometry And Exact Linearization Capabilities",
tags::deformation_type_contract
) {
STATIC_CHECK(surfaceDescriptor.isValid());
STATIC_CHECK(surfaceDescriptor.supportsExactNewtonLinearization());
STATIC_CHECK(interiorDescriptor.isValid());
STATIC_CHECK(interiorDescriptor.supportsExactNewtonLinearization());
STATIC_CHECK(vacuumDescriptor.isValid());
STATIC_CHECK(vacuumDescriptor.supportsExactNewtonLinearization());
STATIC_CHECK(domainDescriptor.isValid());
STATIC_CHECK(domainDescriptor.supportsExactNewtonLinearization());
constexpr auto invalidInteriorDescriptor = [] {
auto descriptor = interiorDescriptor;
descriptor.centerBehavior = mean_field::deformation::InteriorCenterBehavior::Unspecified;
return descriptor;
}();
constexpr auto invalidVacuumDescriptor = [] {
auto descriptor = vacuumDescriptor;
descriptor.outerBoundaryBehavior = mean_field::deformation::VacuumOuterBoundaryBehavior::Unspecified;
return descriptor;
}();
STATIC_CHECK_FALSE(invalidInteriorDescriptor.isValid());
STATIC_CHECK_FALSE(invalidVacuumDescriptor.isValid());
CHECK(surfaceDescriptor.motionKind == mean_field::deformation::SurfaceMotionKind::Radial);
CHECK(interiorDescriptor.centerBehavior == mean_field::deformation::InteriorCenterBehavior::FixedAtReferenceCenter);
CHECK(
vacuumDescriptor.outerBoundaryBehavior ==
mean_field::deformation::VacuumOuterBoundaryBehavior::FixedAtReferenceInfinity
);
}

View File

@@ -0,0 +1,771 @@
#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 domain_deformation_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]] auto makePreparedDomainDeformation(mean_field::fem::FEM &fem) {
const field::ScalarBoundaryDofMap surfaceDofMap =
field::make_stellar_surface_scalar_dof_map<Schema>(*fem.surfaceDeformationFes);
const deformation::SurfaceDeformationCompilationContext surfaceContext{
*fem.surfaceDeformationFes, surfaceDofMap
};
deformation::PreparedNodalRadialSurface surface = deformation::compileSurfaceDeformationPrescription(
deformation::NodalRadialSurface{referenceCenter(fem.mesh->SpaceDimension())}, surfaceContext
);
const deformation::RadialDeformationExtensionCompilationContext extensionContext =
deformation::makeRadialDeformationExtensionCompilationContext<Schema>(
*fem.surfaceDeformationFes, *fem.displacementFes, *fem.logicalReferenceMesh
);
deformation::PreparedPowerLawRadialInteriorExtension interior =
deformation::compileInteriorDeformationExtension(
deformation::PowerLawRadialInteriorExtension{}, extensionContext
);
deformation::PreparedFixedInfinityRadialVacuumExtension vacuum = deformation::compileVacuumDeformationExtension(
deformation::FixedInfinityRadialVacuumExtension{}, extensionContext
);
return deformation::composePreparedDomainDeformation(
std::move(surface), std::move(interior), std::move(vacuum), *fem.surfaceDeformationFes,
*fem.displacementFes, *fem.logicalReferenceMesh
);
}
[[nodiscard]] int volumeVectorDof(
const int scalarTrueDof,
const int component,
const int scalarTrueDofCount
) {
return scalarTrueDof + component * scalarTrueDofCount;
}
[[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]] double globalInnerProduct(
const mfem::Vector &first,
const mfem::Vector &second,
MPI_Comm communicator
) {
REQUIRE(first.Size() == second.Size());
const double localValue = first * second;
double globalValue = 0.0;
MPI_Allreduce(&localValue, &globalValue, 1, MPI_DOUBLE, MPI_SUM, communicator);
return globalValue;
}
class AnalyticNonlinearSurface final {
public:
[[nodiscard]] deformation::SurfaceDeformationDescriptor descriptor() const noexcept {
return {
.name = "AnalyticNonlinearSurface",
.spatialDimension = 3,
.motionKind = deformation::SurfaceMotionKind::Radial,
.linearOnReferenceGeometry = false,
.requiresStarShapedReferenceSurface = false,
.hasExactDerivativeTranspose = true,
.hasExactPullbackDerivative = true,
.translationTreatment = deformation::GeometricGaugeTreatment::Retained,
.orientationTreatment = deformation::GeometricGaugeTreatment::Retained
};
}
[[nodiscard]] int parameterCount() const noexcept {
return 2;
}
[[nodiscard]] int surfaceDisplacementSize() const noexcept {
return 2;
}
void buildSurfaceDisplacement(
const mfem::Vector &parameters,
mfem::Vector &surfaceDisplacement
) const {
surfaceDisplacement(0) = parameters(0) * parameters(0) + parameters(1);
surfaceDisplacement(1) = parameters(0) * parameters(1);
}
void applyJacobian(
const mfem::Vector &parameters,
const mfem::Vector &parameterDirection,
mfem::Vector &surfaceDisplacementDirection
) const {
surfaceDisplacementDirection(0) = 2.0 * parameters(0) * parameterDirection(0) + parameterDirection(1);
surfaceDisplacementDirection(1) =
parameters(1) * parameterDirection(0) + parameters(0) * parameterDirection(1);
}
void applyJacobianTranspose(
const mfem::Vector &parameters,
const mfem::Vector &surfaceDisplacementDual,
mfem::Vector &parameterDual
) const {
parameterDual(0) =
2.0 * parameters(0) * surfaceDisplacementDual(0) + parameters(1) * surfaceDisplacementDual(1);
parameterDual(1) = surfaceDisplacementDual(0) + parameters(0) * surfaceDisplacementDual(1);
}
void applyPullbackDerivative(
const mfem::Vector &,
const mfem::Vector &parameterDirection,
const mfem::Vector &surfaceDisplacementDual,
mfem::Vector &parameterDualAction
) const {
parameterDualAction(0) = 2.0 * parameterDirection(0) * surfaceDisplacementDual(0) +
parameterDirection(1) * surfaceDisplacementDual(1);
parameterDualAction(1) = parameterDirection(0) * surfaceDisplacementDual(1);
}
};
class AnalyticNonlinearExtensionKernel {
public:
AnalyticNonlinearExtensionKernel(
const int scalarTrueDofCount,
const double coefficientScale
)
: m_scalarTrueDofCount(scalarTrueDofCount),
m_coefficientScale(coefficientScale) {
}
[[nodiscard]] int scalarTrueDofCount() const noexcept {
return m_scalarTrueDofCount;
}
[[nodiscard]] int volumeDisplacementSize() const noexcept {
return 3 * m_scalarTrueDofCount;
}
[[nodiscard]] int sharedScalarDof() const noexcept {
return m_scalarTrueDofCount / 2;
}
void build(
const mfem::Vector &surfaceDisplacement,
mfem::Vector &volumeDisplacement,
const bool stellar
) const {
volumeDisplacement = 0.0;
for (int vectorDof = 0; vectorDof < volumeDisplacementSize(); ++vectorDof) {
if (!hasSupport(vectorDof % m_scalarTrueDofCount, stellar)) {
continue;
}
double linearFirst = 0.0;
double linearSecond = 0.0;
double bilinear = 0.0;
coefficients(vectorDof, linearFirst, linearSecond, bilinear);
volumeDisplacement(vectorDof) = linearFirst * surfaceDisplacement(0) +
linearSecond * surfaceDisplacement(1) +
bilinear * surfaceDisplacement(0) * surfaceDisplacement(1);
}
}
void applyJacobian(
const mfem::Vector &surfaceDisplacement,
const mfem::Vector &surfaceDirection,
mfem::Vector &volumeDirection,
const bool stellar
) const {
volumeDirection = 0.0;
for (int vectorDof = 0; vectorDof < volumeDisplacementSize(); ++vectorDof) {
if (!hasSupport(vectorDof % m_scalarTrueDofCount, stellar)) {
continue;
}
double linearFirst = 0.0;
double linearSecond = 0.0;
double bilinear = 0.0;
coefficients(vectorDof, linearFirst, linearSecond, bilinear);
volumeDirection(vectorDof) = (linearFirst + bilinear * surfaceDisplacement(1)) * surfaceDirection(0) +
(linearSecond + bilinear * surfaceDisplacement(0)) * surfaceDirection(1);
}
}
void applyJacobianTranspose(
const mfem::Vector &surfaceDisplacement,
const mfem::Vector &volumeDual,
mfem::Vector &surfaceDual,
const bool stellar
) const {
surfaceDual = 0.0;
for (int vectorDof = 0; vectorDof < volumeDisplacementSize(); ++vectorDof) {
if (!hasSupport(vectorDof % m_scalarTrueDofCount, stellar)) {
continue;
}
double linearFirst = 0.0;
double linearSecond = 0.0;
double bilinear = 0.0;
coefficients(vectorDof, linearFirst, linearSecond, bilinear);
surfaceDual(0) += (linearFirst + bilinear * surfaceDisplacement(1)) * volumeDual(vectorDof);
surfaceDual(1) += (linearSecond + bilinear * surfaceDisplacement(0)) * volumeDual(vectorDof);
}
}
void applyPullbackDerivative(
const mfem::Vector &surfaceDirection,
const mfem::Vector &volumeDual,
mfem::Vector &surfaceDualAction,
const bool stellar
) const {
surfaceDualAction = 0.0;
for (int vectorDof = 0; vectorDof < volumeDisplacementSize(); ++vectorDof) {
if (!hasSupport(vectorDof % m_scalarTrueDofCount, stellar)) {
continue;
}
double linearFirst = 0.0;
double linearSecond = 0.0;
double bilinear = 0.0;
coefficients(vectorDof, linearFirst, linearSecond, bilinear);
surfaceDualAction(0) += bilinear * surfaceDirection(1) * volumeDual(vectorDof);
surfaceDualAction(1) += bilinear * surfaceDirection(0) * volumeDual(vectorDof);
}
}
[[nodiscard]] bool hasSupport(
const int scalarTrueDof,
const bool stellar
) const noexcept {
return stellar ? scalarTrueDof <= sharedScalarDof() : scalarTrueDof >= sharedScalarDof();
}
private:
void coefficients(
const int vectorDof,
double &linearFirst,
double &linearSecond,
double &bilinear
) const noexcept {
linearFirst = m_coefficientScale * (0.01 + 0.001 * static_cast<double>(vectorDof % 7));
linearSecond = m_coefficientScale * (-0.02 + 0.002 * static_cast<double>(vectorDof % 5));
bilinear = m_coefficientScale * 0.0005 * static_cast<double>(1 + vectorDof % 3);
}
int m_scalarTrueDofCount;
double m_coefficientScale;
};
class AnalyticNonlinearInteriorExtension final {
public:
explicit AnalyticNonlinearInteriorExtension(
const int scalarTrueDofCount,
const bool supportEnabled = true
)
: m_kernel(
scalarTrueDofCount,
1.0
),
m_supportEnabled(supportEnabled) {
}
[[nodiscard]] deformation::InteriorDeformationExtensionDescriptor descriptor() const noexcept {
return {
.name = "AnalyticNonlinearInteriorExtension",
.spatialDimension = 3,
.linearOnReferenceGeometry = false,
.requiresRadialFoliation = false,
.requiresAuxiliarySolve = false,
.hasExactDerivativeTranspose = true,
.hasExactPullbackDerivative = true,
.centerBehavior = deformation::InteriorCenterBehavior::FixedAtReferenceCenter
};
}
[[nodiscard]] int surfaceDisplacementSize() const noexcept {
return 2;
}
[[nodiscard]] int interiorDisplacementSize() const noexcept {
return m_kernel.volumeDisplacementSize();
}
[[nodiscard]] int scalarTrueDofCount() const noexcept {
return m_kernel.scalarTrueDofCount();
}
[[nodiscard]] bool hasStellarSupport(const int scalarTrueDof) const {
return m_supportEnabled && m_kernel.hasSupport(scalarTrueDof, true);
}
void buildInteriorDisplacement(
const mfem::Vector &surface,
mfem::Vector &volume
) const {
m_kernel.build(surface, volume, true);
}
void applyJacobian(
const mfem::Vector &surface,
const mfem::Vector &direction,
mfem::Vector &volume
) const {
m_kernel.applyJacobian(surface, direction, volume, true);
}
void applyJacobianTranspose(
const mfem::Vector &surface,
const mfem::Vector &volume,
mfem::Vector &dual
) const {
m_kernel.applyJacobianTranspose(surface, volume, dual, true);
}
void applyPullbackDerivative(
const mfem::Vector &,
const mfem::Vector &direction,
const mfem::Vector &volume,
mfem::Vector &dual
) const {
m_kernel.applyPullbackDerivative(direction, volume, dual, true);
}
private:
AnalyticNonlinearExtensionKernel m_kernel;
bool m_supportEnabled;
};
class AnalyticNonlinearVacuumExtension final {
public:
explicit AnalyticNonlinearVacuumExtension(const int scalarTrueDofCount)
: m_kernel(
scalarTrueDofCount,
-0.7
) {
}
[[nodiscard]] deformation::VacuumDeformationExtensionDescriptor descriptor() const noexcept {
return {
.name = "AnalyticNonlinearVacuumExtension",
.spatialDimension = 3,
.linearOnReferenceGeometry = false,
.requiresRadialFoliation = false,
.requiresAuxiliarySolve = false,
.hasExactDerivativeTranspose = true,
.hasExactPullbackDerivative = true,
.outerBoundaryBehavior = deformation::VacuumOuterBoundaryBehavior::FixedAtReferenceInfinity
};
}
[[nodiscard]] int surfaceDisplacementSize() const noexcept {
return 2;
}
[[nodiscard]] int vacuumDisplacementSize() const noexcept {
return m_kernel.volumeDisplacementSize();
}
[[nodiscard]] int scalarTrueDofCount() const noexcept {
return m_kernel.scalarTrueDofCount();
}
[[nodiscard]] bool hasVacuumSupport(const int scalarTrueDof) const {
return m_kernel.hasSupport(scalarTrueDof, false);
}
void buildVacuumDisplacement(
const mfem::Vector &surface,
mfem::Vector &volume
) const {
m_kernel.build(surface, volume, false);
}
void applyJacobian(
const mfem::Vector &surface,
const mfem::Vector &direction,
mfem::Vector &volume
) const {
m_kernel.applyJacobian(surface, direction, volume, false);
}
void applyJacobianTranspose(
const mfem::Vector &surface,
const mfem::Vector &volume,
mfem::Vector &dual
) const {
m_kernel.applyJacobianTranspose(surface, volume, dual, false);
}
void applyPullbackDerivative(
const mfem::Vector &,
const mfem::Vector &direction,
const mfem::Vector &volume,
mfem::Vector &dual
) const {
m_kernel.applyPullbackDerivative(direction, volume, dual, false);
}
private:
AnalyticNonlinearExtensionKernel m_kernel;
};
} // namespace domain_deformation_test_utils
TEST_CASE(
"Prepared Domain Deformation Composes Surface Interior And Vacuum Maps With Explicit Ownership",
tags::domain_deformation_composition
) {
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());
auto prepared = domain_deformation_test_utils::makePreparedDomainDeformation(fem);
STATIC_CHECK(deformation::PreparedDomainDeformationOperator<decltype(prepared)>);
const deformation::DomainDeformationDescriptor descriptor = prepared.descriptor();
REQUIRE(descriptor.isValid());
CHECK(descriptor.linearOnReferenceGeometry);
CHECK_FALSE(descriptor.requiresAuxiliarySolve);
CHECK(descriptor.supportsExactNewtonLinearization());
CHECK(prepared.parameterCount() == prepared.surfaceDeformationPrescription().parameterCount());
CHECK(prepared.surfaceDisplacementSize() == prepared.surfaceDeformationPrescription().surfaceDisplacementSize());
CHECK(prepared.volumeDisplacementSize() == fem.displacementFes->GetTrueVSize());
CHECK(prepared.matchesCurrentDiscretization());
const deformation::DomainDeformationDiscretizationDependencies &dependencies =
prepared.discretizationDependencies();
CHECK(dependencies.physicalMeshIdentity == fem.mesh.get());
CHECK(dependencies.logicalReferenceMeshIdentity == fem.logicalReferenceMesh.get());
CHECK(dependencies.surfaceScalarSpaceIdentity == fem.surfaceDeformationFes.get());
CHECK(dependencies.volumeDisplacementSpaceIdentity == fem.displacementFes.get());
CHECK(dependencies.isCurrent());
const deformation::DomainDeformationCompositionReport &composition = prepared.compositionReport();
CHECK(composition.scalarTrueDofCount == prepared.scalarTrueDofCount());
CHECK(composition.assignedScalarDofCount() == prepared.scalarTrueDofCount());
CHECK(composition.stellarInteriorOwnedScalarDofCount > 0);
CHECK(composition.vacuumOwnedScalarDofCount > 0);
CHECK(composition.sharedSurfaceScalarDofCount > 0);
int countedStellarOwners = 0;
int countedVacuumOwners = 0;
int countedSharedDofs = 0;
for (int scalarTrueDof = 0; scalarTrueDof < prepared.scalarTrueDofCount(); ++scalarTrueDof) {
if (prepared.volumeOwner(scalarTrueDof) == deformation::VolumeDeformationOwner::StellarInterior) {
++countedStellarOwners;
} else {
++countedVacuumOwners;
}
countedSharedDofs += prepared.isSharedSurfaceDof(scalarTrueDof) ? 1 : 0;
}
CHECK(countedStellarOwners == composition.stellarInteriorOwnedScalarDofCount);
CHECK(countedVacuumOwners == composition.vacuumOwnedScalarDofCount);
CHECK(countedSharedDofs == composition.sharedSurfaceScalarDofCount);
mfem::Vector zeroParameters(prepared.parameterCount());
zeroParameters = 0.0;
mfem::Vector composedVolume(prepared.volumeDisplacementSize());
prepared.buildVolumeDisplacement(zeroParameters, composedVolume);
CHECK(composedVolume.Norml2() == 0.0);
mfem::Vector parameters(prepared.parameterCount());
for (int parameter = 0; parameter < parameters.Size(); ++parameter) {
const double index = static_cast<double>(parameter + 1);
parameters(parameter) = 0.012 * std::sin(0.19 * index) - 0.004 * std::cos(0.31 * index);
}
prepared.buildVolumeDisplacement(parameters, composedVolume);
mfem::Vector surfaceDisplacement(prepared.surfaceDisplacementSize());
mfem::Vector interiorVolume(prepared.volumeDisplacementSize());
mfem::Vector vacuumVolume(prepared.volumeDisplacementSize());
prepared.surfaceDeformationPrescription().buildSurfaceDisplacement(parameters, surfaceDisplacement);
prepared.stellarInteriorExtension().buildInteriorDisplacement(surfaceDisplacement, interiorVolume);
prepared.vacuumExtension().buildVacuumDisplacement(surfaceDisplacement, vacuumVolume);
constexpr double tolerance = 2.0e-12;
for (int scalarTrueDof = 0; scalarTrueDof < prepared.scalarTrueDofCount(); ++scalarTrueDof) {
for (int component = 0; component < prepared.spatialDimension(); ++component) {
const int volumeDof =
domain_deformation_test_utils::volumeVectorDof(scalarTrueDof, component, prepared.scalarTrueDofCount());
const double expected =
prepared.volumeOwner(scalarTrueDof) == deformation::VolumeDeformationOwner::StellarInterior
? interiorVolume(volumeDof)
: vacuumVolume(volumeDof);
CHECK(std::abs(composedVolume(volumeDof) - expected) <= tolerance);
if (prepared.isSharedSurfaceDof(scalarTrueDof)) {
CHECK(std::abs(interiorVolume(volumeDof) - vacuumVolume(volumeDof)) <= tolerance);
}
}
}
const deformation::PreparedDomainDeformationActionStatistics &statistics = prepared.actionStatistics();
CHECK(statistics.volumeBuildApplications == 2);
CHECK(statistics.jacobianApplications == 0);
CHECK(statistics.jacobianTransposeApplications == 0);
CHECK(statistics.pullbackDerivativeApplications == 0);
CHECK(statistics.geometryInspections == 0);
mfem::Vector wrongParameters(prepared.parameterCount() + 1);
mfem::Vector wrongVolume(prepared.volumeDisplacementSize() + 1);
CHECK_THROWS_AS(prepared.buildVolumeDisplacement(wrongParameters, composedVolume), std::invalid_argument);
CHECK_THROWS_AS(prepared.buildVolumeDisplacement(parameters, wrongVolume), std::invalid_argument);
CHECK_THROWS_AS(prepared.volumeOwner(-1), std::out_of_range);
}
TEST_CASE(
"Prepared Domain Deformation Jacobian Matches Centered Difference And Its Pullback Preserves Virtual Work",
tags::domain_deformation_linearization
) {
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());
auto prepared = domain_deformation_test_utils::makePreparedDomainDeformation(fem);
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);
parameters(parameter) = 0.008 * std::sin(0.13 * index);
direction(parameter) = std::cos(0.17 * index) - 0.25 * std::sin(0.29 * index);
}
constexpr double step = 1.0e-6;
mfem::Vector plusParameters(parameters);
mfem::Vector minusParameters(parameters);
plusParameters.Add(step, direction);
minusParameters.Add(-step, direction);
mfem::Vector plusVolume(prepared.volumeDisplacementSize());
mfem::Vector minusVolume(prepared.volumeDisplacementSize());
mfem::Vector jacobianAction(prepared.volumeDisplacementSize());
prepared.buildVolumeDisplacement(plusParameters, plusVolume);
prepared.buildVolumeDisplacement(minusParameters, minusVolume);
prepared.applyJacobian(parameters, direction, jacobianAction);
mfem::Vector centeredDifference(plusVolume);
centeredDifference -= minusVolume;
centeredDifference /= 2.0 * step;
CHECK(domain_deformation_test_utils::relativeError(jacobianAction, centeredDifference) < 3.0e-10);
mfem::Vector volumeDual(prepared.volumeDisplacementSize());
for (int dof = 0; dof < volumeDual.Size(); ++dof) {
const double index = static_cast<double>(dof + 1);
volumeDual(dof) = std::sin(0.07 * index) + 0.4 * std::cos(0.11 * index);
}
mfem::Vector parameterDual(prepared.parameterCount());
prepared.applyJacobianTranspose(parameters, volumeDual, parameterDual);
const double volumeWork =
domain_deformation_test_utils::globalInnerProduct(jacobianAction, volumeDual, fem.mesh->GetComm());
const double parameterWork =
domain_deformation_test_utils::globalInnerProduct(direction, parameterDual, fem.mesh->GetComm());
const double workScale = std::max({1.0, std::abs(volumeWork), std::abs(parameterWork)});
CHECK(std::abs(volumeWork - parameterWork) <= 8.0e-13 * workScale);
mfem::Vector pullbackAction(prepared.parameterCount());
prepared.applyPullbackDerivative(parameters, direction, volumeDual, pullbackAction);
CHECK(pullbackAction.Norml2() == 0.0);
mfem::Vector plusTranspose(prepared.parameterCount());
mfem::Vector minusTranspose(prepared.parameterCount());
prepared.applyJacobianTranspose(plusParameters, volumeDual, plusTranspose);
prepared.applyJacobianTranspose(minusParameters, volumeDual, minusTranspose);
mfem::Vector transposeCenteredDifference(plusTranspose);
transposeCenteredDifference -= minusTranspose;
transposeCenteredDifference /= 2.0 * step;
CHECK(transposeCenteredDifference.Norml2() <= 1.0e-12 * std::max(1.0, parameterDual.Norml2()));
}
TEST_CASE(
"Nonlinear Domain Deformation Pullback Matches The Directional Derivative Of Its Complete Transpose",
tags::domain_deformation_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 int scalarTrueDofCount = fem.displacementFes->GetTrueVSize() / fem.mesh->SpaceDimension();
CHECK_THROWS_AS(
(deformation::composePreparedDomainDeformation(
domain_deformation_test_utils::AnalyticNonlinearSurface{},
domain_deformation_test_utils::AnalyticNonlinearInteriorExtension{scalarTrueDofCount, false},
domain_deformation_test_utils::AnalyticNonlinearVacuumExtension{scalarTrueDofCount},
*fem.surfaceDeformationFes, *fem.displacementFes, *fem.logicalReferenceMesh
)),
std::invalid_argument
);
CHECK_THROWS_AS(
(deformation::composePreparedDomainDeformation(
domain_deformation_test_utils::AnalyticNonlinearSurface{},
domain_deformation_test_utils::AnalyticNonlinearInteriorExtension{scalarTrueDofCount},
domain_deformation_test_utils::AnalyticNonlinearVacuumExtension{scalarTrueDofCount - 1},
*fem.surfaceDeformationFes, *fem.displacementFes, *fem.logicalReferenceMesh
)),
std::invalid_argument
);
auto prepared = deformation::composePreparedDomainDeformation(
domain_deformation_test_utils::AnalyticNonlinearSurface{},
domain_deformation_test_utils::AnalyticNonlinearInteriorExtension{scalarTrueDofCount},
domain_deformation_test_utils::AnalyticNonlinearVacuumExtension{scalarTrueDofCount}, *fem.surfaceDeformationFes,
*fem.displacementFes, *fem.logicalReferenceMesh
);
REQUIRE_FALSE(prepared.descriptor().linearOnReferenceGeometry);
mfem::Vector parameters(2);
parameters(0) = 0.37;
parameters(1) = -0.21;
mfem::Vector parameterDirection(2);
parameterDirection(0) = -0.42;
parameterDirection(1) = 0.63;
mfem::Vector volumeDual(prepared.volumeDisplacementSize());
for (int vectorDof = 0; vectorDof < volumeDual.Size(); ++vectorDof) {
const double index = static_cast<double>(vectorDof + 1);
volumeDual(vectorDof) = std::sin(0.013 * index) - 0.3 * std::cos(0.021 * index);
}
constexpr double step = 2.0e-6;
mfem::Vector plusParameters(parameters);
mfem::Vector minusParameters(parameters);
plusParameters.Add(step, parameterDirection);
minusParameters.Add(-step, parameterDirection);
mfem::Vector plusVolume(prepared.volumeDisplacementSize());
mfem::Vector minusVolume(prepared.volumeDisplacementSize());
mfem::Vector jacobianAction(prepared.volumeDisplacementSize());
prepared.buildVolumeDisplacement(plusParameters, plusVolume);
prepared.buildVolumeDisplacement(minusParameters, minusVolume);
prepared.applyJacobian(parameters, parameterDirection, jacobianAction);
mfem::Vector centeredJacobian(plusVolume);
centeredJacobian -= minusVolume;
centeredJacobian /= 2.0 * step;
CHECK(domain_deformation_test_utils::relativeError(jacobianAction, centeredJacobian) < 2.0e-10);
mfem::Vector parameterDual(2);
prepared.applyJacobianTranspose(parameters, volumeDual, parameterDual);
const double volumeWork =
domain_deformation_test_utils::globalInnerProduct(jacobianAction, volumeDual, fem.mesh->GetComm());
const double parameterWork =
domain_deformation_test_utils::globalInnerProduct(parameterDirection, parameterDual, fem.mesh->GetComm());
CHECK(
std::abs(volumeWork - parameterWork) <= 2.0e-12 * std::max({1.0, std::abs(volumeWork), std::abs(parameterWork)})
);
mfem::Vector pullbackAction(2);
prepared.applyPullbackDerivative(parameters, parameterDirection, volumeDual, pullbackAction);
mfem::Vector plusTranspose(2);
mfem::Vector minusTranspose(2);
prepared.applyJacobianTranspose(plusParameters, volumeDual, plusTranspose);
prepared.applyJacobianTranspose(minusParameters, volumeDual, minusTranspose);
mfem::Vector centeredPullback(plusTranspose);
centeredPullback -= minusTranspose;
centeredPullback /= 2.0 * step;
CHECK(domain_deformation_test_utils::relativeError(pullbackAction, centeredPullback) < 3.0e-9);
const int sharedScalarDof = scalarTrueDofCount / 2;
REQUIRE(prepared.isSharedSurfaceDof(sharedScalarDof));
CHECK(prepared.volumeOwner(sharedScalarDof) == deformation::VolumeDeformationOwner::StellarInterior);
mfem::Vector surfaceDisplacement(2);
mfem::Vector interiorVolume(prepared.volumeDisplacementSize());
mfem::Vector vacuumVolume(prepared.volumeDisplacementSize());
mfem::Vector composedVolume(prepared.volumeDisplacementSize());
prepared.surfaceDeformationPrescription().buildSurfaceDisplacement(parameters, surfaceDisplacement);
prepared.stellarInteriorExtension().buildInteriorDisplacement(surfaceDisplacement, interiorVolume);
prepared.vacuumExtension().buildVacuumDisplacement(surfaceDisplacement, vacuumVolume);
prepared.buildVolumeDisplacement(parameters, composedVolume);
for (int component = 0; component < prepared.spatialDimension(); ++component) {
const int vectorDof = sharedScalarDof + component * scalarTrueDofCount;
CHECK(composedVolume(vectorDof) == interiorVolume(vectorDof));
CHECK(interiorVolume(vectorDof) != vacuumVolume(vectorDof));
}
}
TEST_CASE(
"Prepared Domain Deformation Accepts Orientation Preserving Shapes And Rejects Folded Volume Maps",
tags::domain_deformation_geometry
) {
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());
auto prepared = domain_deformation_test_utils::makePreparedDomainDeformation(fem);
mfem::Vector volumeDisplacement(prepared.volumeDisplacementSize());
mfem::Vector zeroParameters(prepared.parameterCount());
zeroParameters = 0.0;
const mean_field::deformation::DomainDeformationGeometryReport referenceReport =
prepared.buildValidatedVolumeDisplacement(zeroParameters, volumeDisplacement, 0.99);
CHECK(std::abs(referenceReport.minimumJacobianDeterminant - 1.0) <= 64.0 * std::numeric_limits<double>::epsilon());
CHECK_THROWS_AS(
prepared.buildValidatedVolumeDisplacement(zeroParameters, volumeDisplacement, 1.01), std::domain_error
);
CHECK_THROWS_AS(
prepared.buildValidatedVolumeDisplacement(zeroParameters, volumeDisplacement, -0.01), std::invalid_argument
);
CHECK_THROWS_AS(
prepared.buildValidatedVolumeDisplacement(
zeroParameters, volumeDisplacement, std::numeric_limits<double>::quiet_NaN()
),
std::invalid_argument
);
mfem::Vector boundedParameters(prepared.parameterCount());
boundedParameters = 0.02;
const mean_field::deformation::DomainDeformationGeometryReport boundedReport =
prepared.buildValidatedVolumeDisplacement(boundedParameters, volumeDisplacement);
CHECK(boundedReport.isOrientationPreserving());
CHECK(boundedReport.minimumJacobianDeterminant > 0.0);
mfem::Vector foldingParameters(prepared.parameterCount());
foldingParameters = -2.0;
prepared.buildVolumeDisplacement(foldingParameters, volumeDisplacement);
const mean_field::deformation::DomainDeformationGeometryReport foldingReport =
prepared.inspectMappedGeometry(volumeDisplacement);
CHECK_FALSE(foldingReport.isOrientationPreserving());
CHECK_THROWS_AS(
prepared.buildValidatedVolumeDisplacement(foldingParameters, volumeDisplacement), std::domain_error
);
mfem::Vector nonfiniteParameters(zeroParameters);
nonfiniteParameters(0) = std::numeric_limits<double>::quiet_NaN();
CHECK_THROWS_AS(
prepared.buildValidatedVolumeDisplacement(nonfiniteParameters, volumeDisplacement), std::domain_error
);
CHECK(prepared.actionStatistics().geometryInspections == 6);
}
TEST_CASE(
"Prepared Domain Deformation Rejects Actions After Its Discretization Becomes Stale",
tags::domain_deformation_composition
) {
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());
auto prepared = domain_deformation_test_utils::makePreparedDomainDeformation(fem);
mfem::Vector parameters(prepared.parameterCount());
mfem::Vector parameterDirection(prepared.parameterCount());
mfem::Vector parameterDual(prepared.parameterCount());
mfem::Vector volume(prepared.volumeDisplacementSize());
parameters = 0.0;
parameterDirection = 0.0;
volume = 0.0;
REQUIRE(prepared.matchesCurrentDiscretization());
fem.mesh->UniformRefinement();
REQUIRE_FALSE(prepared.matchesCurrentDiscretization());
CHECK_THROWS_AS(prepared.buildVolumeDisplacement(parameters, volume), std::logic_error);
CHECK_THROWS_AS(prepared.applyJacobian(parameters, parameterDirection, volume), std::logic_error);
CHECK_THROWS_AS(prepared.applyJacobianTranspose(parameters, volume, parameterDual), std::logic_error);
CHECK_THROWS_AS(
prepared.applyPullbackDerivative(parameters, parameterDirection, volume, parameterDual), std::logic_error
);
CHECK_THROWS_AS(prepared.inspectMappedGeometry(volume), std::logic_error);
}

View File

@@ -0,0 +1,536 @@
#include <algorithm>
#include <array>
#include <cmath>
#include <limits>
#include <stdexcept>
#include <catch2/catch_test_macros.hpp>
#include <mfem.hpp>
#include <mpi.h>
import mean_field;
import test_helpers;
namespace nodal_radial_surface_test_utils {
namespace deformation = mean_field::deformation;
namespace domain = mean_field::utils::domain;
namespace field = mean_field::field;
using Schema = domain::CoreEnvelopeVacuumDomainSchema;
using PlanarBoundarySchema = domain::DomainSchema<
domain::MaterialList<>,
domain::BoundaryList<domain::BoundaryAttribute<domain::StellarSurface, 1>>,
domain::RelationList<>>;
[[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]] mfem::Vector projectReferenceSurfacePositions(
mfem::ParFiniteElementSpace &scalarFiniteElementSpace,
const field::ScalarBoundaryDofMap &surfaceDofMap
) {
const mfem::Mesh *mesh = scalarFiniteElementSpace.GetMesh();
REQUIRE(mesh != nullptr);
const int spatialDimension = mesh->SpaceDimension();
mfem::Vector positions(spatialDimension * surfaceDofMap.local_size());
mfem::ParGridFunction coordinateField(&scalarFiniteElementSpace);
for (int component = 0; component < spatialDimension; ++component) {
mfem::FunctionCoefficient coefficient([component](const mfem::Vector &position) {
return position(component);
});
coordinateField.ProjectCoefficient(coefficient);
mfem::Vector coordinateTrueDofs;
coordinateField.GetTrueDofs(coordinateTrueDofs);
const mfem::Vector surfaceCoordinates = surfaceDofMap.gather(coordinateTrueDofs);
for (int surfaceDof = 0; surfaceDof < surfaceDofMap.local_size(); ++surfaceDof) {
positions(spatialDimension * surfaceDof + component) = surfaceCoordinates(surfaceDof);
}
}
return positions;
}
[[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]] double globalMeanReferenceRadius(const deformation::PreparedNodalRadialSurface &surface) {
double localRadiusSum = 0.0;
for (int parameterDof = 0; parameterDof < surface.parameterCount(); ++parameterDof) {
localRadiusSum += surface.referenceRadius(parameterDof);
}
const long long localCount = surface.parameterCount();
double globalRadiusSum = 0.0;
long long globalCount = 0;
MPI_Allreduce(&localRadiusSum, &globalRadiusSum, 1, MPI_DOUBLE, MPI_SUM, MPI_COMM_WORLD);
MPI_Allreduce(&localCount, &globalCount, 1, MPI_LONG_LONG, MPI_SUM, MPI_COMM_WORLD);
REQUIRE(globalCount > 0);
return globalRadiusSum / static_cast<double>(globalCount);
}
struct BinaryRochePotential final {
double primaryMass;
double companionMass;
double separation;
[[nodiscard]] double operator()(
const double x,
const double y,
const double z
) const {
const double primaryDistance = std::sqrt(x * x + y * y + z * z);
const double companionOffset = x - separation;
const double companionDistance = std::sqrt(companionOffset * companionOffset + y * y + z * z);
const double totalMass = primaryMass + companionMass;
const double centerOfMassX = separation * companionMass / totalMass;
const double angularSpeed2 = totalMass / (separation * separation * separation);
const double rotationRadius2 = (x - centerOfMassX) * (x - centerOfMassX) + y * y;
return -primaryMass / primaryDistance - companionMass / companionDistance -
0.5 * angularSpeed2 * rotationRadius2;
}
[[nodiscard]] double firstRadialIntersection(
const double directionX,
const double directionY,
const double directionZ,
const double targetPotential,
const double referenceScale
) const {
double lowerRadius = 1.0e-8 * referenceScale;
double lowerValue =
(*this)(lowerRadius * directionX, lowerRadius * directionY, lowerRadius * directionZ) - targetPotential;
REQUIRE(lowerValue < 0.0);
double upperRadius = lowerRadius;
double upperValue = lowerValue;
constexpr int bracketSamples = 512;
for (int sample = 1; sample <= bracketSamples && upperValue <= 0.0; ++sample) {
upperRadius = 0.45 * separation * static_cast<double>(sample) / bracketSamples;
upperValue = (*this)(upperRadius * directionX, upperRadius * directionY, upperRadius * directionZ) -
targetPotential;
}
REQUIRE(upperValue > 0.0);
for (int iteration = 0; iteration < 80; ++iteration) {
const double middleRadius = 0.5 * (lowerRadius + upperRadius);
const double middleValue =
(*this)(middleRadius * directionX, middleRadius * directionY, middleRadius * directionZ) -
targetPotential;
if (middleValue > 0.0) {
upperRadius = middleRadius;
} else {
lowerRadius = middleRadius;
}
}
return 0.5 * (lowerRadius + upperRadius);
}
};
} // namespace nodal_radial_surface_test_utils
TEST_CASE(
"Nodal Radial Surface Satisfies The Surface Deformation Contract And Validates Its Reference Center",
tags::nodal_radial_surface_validation
) {
namespace deformation = mean_field::deformation;
namespace field = mean_field::field;
STATIC_CHECK(deformation::SurfaceDeformationPrescription<deformation::NodalRadialSurface>);
STATIC_CHECK(deformation::PreparedSurfaceDeformationPrescription<deformation::PreparedNodalRadialSurface>);
STATIC_CHECK(
deformation::SurfaceDeformationCompilable<
deformation::NodalRadialSurface, deformation::SurfaceDeformationCompilationContext>
);
mfem::Vector center(3);
center = 0.0;
const deformation::NodalRadialSurface prescription{center};
const deformation::SurfaceDeformationDescriptor descriptor = prescription.descriptor();
CHECK(descriptor.name == "NodalRadialSurface");
CHECK(descriptor.spatialDimension == 3);
CHECK(descriptor.motionKind == deformation::SurfaceMotionKind::Radial);
CHECK(descriptor.linearOnReferenceGeometry);
CHECK(descriptor.requiresStarShapedReferenceSurface);
CHECK(descriptor.supportsExactNewtonLinearization());
CHECK(descriptor.translationTreatment == deformation::GeometricGaugeTreatment::Retained);
CHECK(descriptor.orientationTreatment == deformation::GeometricGaugeTreatment::Retained);
mfem::Vector emptyCenter;
CHECK_THROWS_AS(deformation::NodalRadialSurface{emptyCenter}, std::invalid_argument);
mfem::Vector nonfiniteCenter(3);
nonfiniteCenter = 0.0;
nonfiniteCenter(1) = std::numeric_limits<double>::quiet_NaN();
CHECK_THROWS_AS(deformation::NodalRadialSurface{nonfiniteCenter}, std::invalid_argument);
mfem::Mesh serialMesh = mfem::Mesh::MakeCartesian2D(4, 3, mfem::Element::QUADRILATERAL, true, 2.0, 1.5);
mfem::ParMesh mesh(MPI_COMM_WORLD, serialMesh);
mfem::H1_FECollection finiteElementCollection(2, mesh.Dimension());
mfem::ParFiniteElementSpace finiteElementSpace(&mesh, &finiteElementCollection);
const field::ScalarBoundaryDofMap surfaceDofMap =
field::make_stellar_surface_scalar_dof_map<nodal_radial_surface_test_utils::PlanarBoundarySchema>(
finiteElementSpace
);
const deformation::SurfaceDeformationCompilationContext context{finiteElementSpace, surfaceDofMap};
CHECK_THROWS_AS(deformation::compileSurfaceDeformationPrescription(prescription, context), std::invalid_argument);
}
TEST_CASE(
"Nodal Radial Surface Resolves Oblate Rotating And Binary Roche Envelopes Between Surface Nodes",
tags::nodal_radial_surface_analytic
) {
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 mean_field::deformation::PreparedNodalRadialSurface surface =
nodal_radial_surface_test_utils::makePreparedSurface(fem);
REQUIRE(surface.spatialDimension() == 3);
const double referenceScale = nodal_radial_surface_test_utils::globalMeanReferenceRadius(surface);
const double equatorialRadius = 1.08 * referenceScale;
const double polarRadius = 0.94 * referenceScale;
mfem::Vector rotatingParameters(surface.parameterCount());
for (int parameterDof = 0; parameterDof < surface.parameterCount(); ++parameterDof) {
const double directionX = surface.radialDirection(parameterDof, 0);
const double directionY = surface.radialDirection(parameterDof, 1);
const double directionZ = surface.radialDirection(parameterDof, 2);
const double targetRadius =
1.0 / std::sqrt(
(directionX * directionX + directionY * directionY) / (equatorialRadius * equatorialRadius) +
directionZ * directionZ / (polarRadius * polarRadius)
);
rotatingParameters(parameterDof) = targetRadius - surface.referenceRadius(parameterDof);
}
mfem::Vector rotatingDisplacement(surface.surfaceDisplacementSize());
surface.buildSurfaceDisplacement(rotatingParameters, rotatingDisplacement);
constexpr double geometricTolerance = 3.0e-12;
for (int parameterDof = 0; parameterDof < surface.parameterCount(); ++parameterDof) {
double movedPosition[3]{};
for (int component = 0; component < 3; ++component) {
const double direction = surface.radialDirection(parameterDof, component);
movedPosition[component] = surface.referenceRadius(parameterDof) * direction +
rotatingDisplacement(surface.surfaceDisplacementDof(parameterDof, component));
}
const double ellipsoidLevel = (movedPosition[0] * movedPosition[0] + movedPosition[1] * movedPosition[1]) /
(equatorialRadius * equatorialRadius) +
movedPosition[2] * movedPosition[2] / (polarRadius * polarRadius);
CHECK(std::abs(ellipsoidLevel - 1.0) <= geometricTolerance);
}
const nodal_radial_surface_test_utils::BinaryRochePotential rochePotential{
.primaryMass = 1.0, .companionMass = 0.7, .separation = 4.0 * referenceScale
};
const double targetPotential = rochePotential(0.0, 0.0, referenceScale);
mfem::Vector rocheParameters(surface.parameterCount());
for (int parameterDof = 0; parameterDof < surface.parameterCount(); ++parameterDof) {
const double targetRadius = rochePotential.firstRadialIntersection(
surface.radialDirection(parameterDof, 0), surface.radialDirection(parameterDof, 1),
surface.radialDirection(parameterDof, 2), targetPotential, referenceScale
);
rocheParameters(parameterDof) = targetRadius - surface.referenceRadius(parameterDof);
}
mfem::Vector rocheDisplacement(surface.surfaceDisplacementSize());
surface.buildSurfaceDisplacement(rocheParameters, rocheDisplacement);
const double potentialTolerance = 2.0e-11 * std::max(1.0, std::abs(targetPotential));
for (int parameterDof = 0; parameterDof < surface.parameterCount(); ++parameterDof) {
double movedPosition[3]{};
for (int component = 0; component < 3; ++component) {
const double direction = surface.radialDirection(parameterDof, component);
movedPosition[component] = surface.referenceRadius(parameterDof) * direction +
rocheDisplacement(surface.surfaceDisplacementDof(parameterDof, component));
}
CHECK(
std::abs(rochePotential(movedPosition[0], movedPosition[1], movedPosition[2]) - targetPotential) <=
potentialTolerance
);
}
auto domainDeformation = mean_field::deformation::compileDomainDeformation(
mean_field::deformation::NodalRadialSurface{
nodal_radial_surface_test_utils::referenceCenter(fem.mesh->SpaceDimension())
},
mean_field::deformation::PowerLawRadialInteriorExtension{},
mean_field::deformation::FixedInfinityRadialVacuumExtension{}, fem
);
mfem::Vector volumeDisplacement(domainDeformation.volumeDisplacementSize());
mfem::ParGridFunction displacementField(fem.displacementFes.get());
mean_field::mapping::GridFunctionMappingEvaluator mappingEvaluator(
*fem.domainMapperStateless, displacementField, *fem.compactificationCoordinate
);
const auto sampleRepresentationError = [&](const mfem::Vector &parameters, const auto &pointError) {
const auto geometry = domainDeformation.buildValidatedVolumeDisplacement(parameters, volumeDisplacement);
REQUIRE(geometry.isOrientationPreserving());
displacementField.SetFromTrueDofs(volumeDisplacement);
mappingEvaluator.InvalidateCache();
double localMaximumError = 0.0;
double localErrorSquared = 0.0;
double localSurfaceArea = 0.0;
long long localSamples = 0;
const int stellarSurfaceAttribute = nodal_radial_surface_test_utils::Schema::template boundary_attribute<
nodal_radial_surface_test_utils::domain::StellarSurface>();
for (int boundaryElement = 0; boundaryElement < fem.mesh->GetNBE(); ++boundaryElement) {
if (fem.mesh->GetBdrAttribute(boundaryElement) != stellarSurfaceAttribute) {
continue;
}
/*
* The stellar surface is a material interface, not an exterior
* boundary of the complete compactified mesh. Resolve the tagged
* boundary element to its underlying mesh face so this sampling
* path works for both internal interfaces and true exterior
* boundaries.
*/
const int face = fem.mesh->GetBdrElementFaceIndex(boundaryElement);
REQUIRE(face >= 0);
mfem::FaceElementTransformations *transformation = fem.mesh->GetFaceElementTransformations(face);
REQUIRE(transformation != nullptr);
REQUIRE(transformation->Elem1 != nullptr);
const mfem::IntegrationRule &rule =
mfem::IntRules.Get(transformation->GetGeometryType(), 2 * mean_field::field::Displacement::vectorOrder);
for (int point = 0; point < rule.GetNPoints(); ++point) {
mean_field::mapping::FaceMappingContext context;
const mfem::IntegrationPoint &integrationPoint = rule.IntPoint(point);
REQUIRE(
mappingEvaluator.EvaluateFace(
*transformation, mean_field::mapping::FaceElementSide::element_1, integrationPoint, context
) == mean_field::mapping::MappingStatus::valid
);
const double error = pointError(context.mapping.physical_position);
REQUIRE(std::isfinite(error));
REQUIRE(context.physical_surface_weight > 0.0);
localMaximumError = std::max(localMaximumError, error);
localErrorSquared += error * error * context.physical_surface_weight;
localSurfaceArea += context.physical_surface_weight;
++localSamples;
}
}
double globalMaximumError = 0.0;
double globalErrorSquared = 0.0;
double globalSurfaceArea = 0.0;
long long globalSamples = 0;
MPI_Allreduce(&localMaximumError, &globalMaximumError, 1, MPI_DOUBLE, MPI_MAX, fem.mesh->GetComm());
MPI_Allreduce(&localErrorSquared, &globalErrorSquared, 1, MPI_DOUBLE, MPI_SUM, fem.mesh->GetComm());
MPI_Allreduce(&localSurfaceArea, &globalSurfaceArea, 1, MPI_DOUBLE, MPI_SUM, fem.mesh->GetComm());
MPI_Allreduce(&localSamples, &globalSamples, 1, MPI_LONG_LONG, MPI_SUM, fem.mesh->GetComm());
REQUIRE(globalSamples > 0);
REQUIRE(globalSurfaceArea > 0.0);
return std::array<double, 2>{globalMaximumError, std::sqrt(globalErrorSquared / globalSurfaceArea)};
};
const auto rotatingErrors =
sampleRepresentationError(rotatingParameters, [equatorialRadius, polarRadius](const mfem::Vector &position) {
return std::abs(
(position(0) * position(0) + position(1) * position(1)) / (equatorialRadius * equatorialRadius) +
position(2) * position(2) / (polarRadius * polarRadius) - 1.0
);
});
const auto rocheErrors =
sampleRepresentationError(rocheParameters, [&rochePotential, targetPotential](const mfem::Vector &position) {
return std::abs(rochePotential(position(0), position(1), position(2)) - targetPotential) /
std::max(1.0, std::abs(targetPotential));
});
INFO("Between-node oblate surface maximum level-set error = " << rotatingErrors[0]);
INFO("Between-node oblate surface RMS level-set error = " << rotatingErrors[1]);
INFO("Between-node Roche surface maximum normalized potential error = " << rocheErrors[0]);
INFO("Between-node Roche surface RMS normalized potential error = " << rocheErrors[1]);
CHECK(rotatingErrors[0] < 5.0e-3);
CHECK(rotatingErrors[1] < 1.0e-3);
CHECK(rocheErrors[0] < 2.0e-2);
CHECK(rocheErrors[1] < 5.0e-3);
const double companionFacingRadius =
rochePotential.firstRadialIntersection(1.0, 0.0, 0.0, targetPotential, referenceScale);
const double companionOpposingRadius =
rochePotential.firstRadialIntersection(-1.0, 0.0, 0.0, targetPotential, referenceScale);
CHECK(std::abs(companionFacingRadius - companionOpposingRadius) > 1.0e-3 * referenceScale);
}
TEST_CASE(
"Nodal Radial Surface Produces The Requested Physical Radial Displacement At Every Surface Coordinate",
tags::nodal_radial_surface_analytic
) {
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::PreparedNodalRadialSurface prepared = nodal_radial_surface_test_utils::makePreparedSurface(fem);
REQUIRE(prepared.parameterCount() > 0);
CHECK(prepared.spatialDimension() == fem.mesh->SpaceDimension());
CHECK(prepared.surfaceDisplacementSize() == prepared.spatialDimension() * prepared.parameterCount());
CHECK(
prepared.globalSurfaceDisplacementSize() ==
static_cast<long long>(prepared.spatialDimension()) * prepared.globalParameterCount()
);
CHECK(
prepared.globalSurfaceDisplacementOffset() ==
static_cast<long long>(prepared.spatialDimension()) * prepared.globalParameterOffset()
);
mfem::Vector parameters(prepared.parameterCount());
for (int parameterDof = 0; parameterDof < parameters.Size(); ++parameterDof) {
parameters(parameterDof) = 0.015 + 0.001 * static_cast<double>(parameterDof % 7);
}
mfem::Vector displacement(prepared.surfaceDisplacementSize());
prepared.buildSurfaceDisplacement(parameters, displacement);
const mfem::Vector referencePositions = nodal_radial_surface_test_utils::projectReferenceSurfacePositions(
*fem.surfaceDeformationFes, prepared.surfaceDofMap()
);
constexpr double tolerance = 2.0e-13;
for (int parameterDof = 0; parameterDof < prepared.parameterCount(); ++parameterDof) {
double radiusSquared = 0.0;
double displacementNorm2 = 0.0;
double radialProjection = 0.0;
for (int component = 0; component < prepared.spatialDimension(); ++component) {
const int surfaceDof = prepared.surfaceDisplacementDof(parameterDof, component);
const double radialCoordinate = referencePositions(surfaceDof) - prepared.referenceCenter()(component);
radiusSquared += radialCoordinate * radialCoordinate;
}
const double radius = std::sqrt(radiusSquared);
REQUIRE(radius > 0.0);
CHECK(std::abs(prepared.referenceRadius(parameterDof) - radius) <= tolerance * radius);
for (int component = 0; component < prepared.spatialDimension(); ++component) {
const int surfaceDof = prepared.surfaceDisplacementDof(parameterDof, component);
const double radialCoordinate = referencePositions(surfaceDof) - prepared.referenceCenter()(component);
const double expectedDirection = radialCoordinate / radius;
const double expectedDisplacement = parameters(parameterDof) * expectedDirection;
CHECK(std::abs(prepared.radialDirection(parameterDof, component) - expectedDirection) <= tolerance);
CHECK(std::abs(displacement(surfaceDof) - expectedDisplacement) <= tolerance);
displacementNorm2 += displacement(surfaceDof) * displacement(surfaceDof);
radialProjection += displacement(surfaceDof) * expectedDirection;
}
CHECK(std::abs(std::sqrt(displacementNorm2) - parameters(parameterDof)) <= tolerance);
CHECK(std::abs(radialProjection - parameters(parameterDof)) <= tolerance);
double movedRadiusSquared = 0.0;
for (int component = 0; component < prepared.spatialDimension(); ++component) {
const int surfaceDof = prepared.surfaceDisplacementDof(parameterDof, component);
const double movedCoordinate =
referencePositions(surfaceDof) + displacement(surfaceDof) - prepared.referenceCenter()(component);
movedRadiusSquared += movedCoordinate * movedCoordinate;
}
CHECK(std::abs(std::sqrt(movedRadiusSquared) - (radius + parameters(parameterDof))) <= tolerance);
}
}
TEST_CASE(
"Nodal Radial Surface Jacobian Matches Centered Difference And Its Transpose Preserves Virtual Work",
tags::nodal_radial_surface_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::PreparedNodalRadialSurface prepared = nodal_radial_surface_test_utils::makePreparedSurface(fem);
mfem::Vector parameters(prepared.parameterCount());
mfem::Vector direction(prepared.parameterCount());
for (int parameterDof = 0; parameterDof < prepared.parameterCount(); ++parameterDof) {
const double index = static_cast<double>(parameterDof + 1);
parameters(parameterDof) = 0.013 * std::sin(0.37 * index);
direction(parameterDof) = std::cos(0.19 * index) - 0.21 * std::sin(0.43 * index);
}
constexpr double step = 1.0e-6;
mfem::Vector plusParameters(parameters);
mfem::Vector minusParameters(parameters);
plusParameters.Add(step, direction);
minusParameters.Add(-step, direction);
mfem::Vector plusDisplacement(prepared.surfaceDisplacementSize());
mfem::Vector minusDisplacement(prepared.surfaceDisplacementSize());
mfem::Vector jacobianAction(prepared.surfaceDisplacementSize());
prepared.buildSurfaceDisplacement(plusParameters, plusDisplacement);
prepared.buildSurfaceDisplacement(minusParameters, minusDisplacement);
prepared.applyJacobian(parameters, direction, jacobianAction);
mfem::Vector centeredDifference(plusDisplacement);
centeredDifference -= minusDisplacement;
centeredDifference /= 2.0 * step;
CHECK(nodal_radial_surface_test_utils::relativeError(jacobianAction, centeredDifference) < 2.0e-11);
mfem::Vector surfaceDual(prepared.surfaceDisplacementSize());
for (int surfaceDof = 0; surfaceDof < surfaceDual.Size(); ++surfaceDof) {
const double index = static_cast<double>(surfaceDof + 1);
surfaceDual(surfaceDof) = std::sin(0.23 * index) + 0.17 * std::cos(0.31 * index);
}
mfem::Vector parameterDual(prepared.parameterCount());
prepared.applyJacobianTranspose(parameters, surfaceDual, parameterDual);
const double surfaceWork = jacobianAction * surfaceDual;
const double parameterWork = direction * parameterDual;
const double workScale = std::max({1.0, std::abs(surfaceWork), std::abs(parameterWork)});
CHECK(std::abs(surfaceWork - parameterWork) <= 3.0e-14 * workScale);
mfem::Vector pullbackDerivative(prepared.parameterCount());
pullbackDerivative = 1.0;
prepared.applyPullbackDerivative(parameters, direction, surfaceDual, pullbackDerivative);
CHECK(pullbackDerivative.Norml2() == 0.0);
mfem::Vector wrongParameters(prepared.parameterCount() + 1);
mfem::Vector wrongSurfaceDisplacement(prepared.surfaceDisplacementSize() + 1);
CHECK_THROWS_AS(prepared.buildSurfaceDisplacement(wrongParameters, plusDisplacement), std::invalid_argument);
CHECK_THROWS_AS(prepared.buildSurfaceDisplacement(parameters, wrongSurfaceDisplacement), std::invalid_argument);
}

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

View File

@@ -0,0 +1,302 @@
#include <algorithm>
#include <numeric>
#include <optional>
#include <stdexcept>
#include <vector>
#include <catch2/catch_test_macros.hpp>
#include <mfem.hpp>
#include <mpi.h>
import mean_field;
import test_helpers;
namespace surface_scalar_dof_test_utils {
namespace domain = mean_field::utils::domain;
namespace field = mean_field::field;
using DefaultSchema = domain::CoreEnvelopeVacuumDomainSchema;
using FirstBoundarySchema = domain::DomainSchema<
domain::MaterialList<>,
domain::BoundaryList<domain::BoundaryAttribute<domain::StellarSurface, 1>>,
domain::RelationList<>>;
using SecondBoundarySchema = domain::DomainSchema<
domain::MaterialList<>,
domain::BoundaryList<domain::BoundaryAttribute<domain::StellarSurface, 2>>,
domain::RelationList<>>;
using MissingBoundarySchema =
domain::DomainSchema<domain::MaterialList<>, domain::BoundaryList<>, domain::RelationList<>>;
[[nodiscard]] mfem::Array<int> make_array(const std::initializer_list<int> values) {
mfem::Array<int> result(static_cast<int>(values.size()));
int index = 0;
for (const int value : values) {
result[index++] = value;
}
return result;
}
[[nodiscard]] mfem::Array<int> expected_boundary_true_dofs(
const mfem::ParFiniteElementSpace &finiteElementSpace,
const int boundaryAttribute
) {
const mfem::Mesh *mesh = finiteElementSpace.GetMesh();
REQUIRE(mesh != nullptr);
REQUIRE(boundaryAttribute > 0);
REQUIRE(boundaryAttribute <= mesh->bdr_attributes.Max());
mfem::Array<int> boundaryMarker(mesh->bdr_attributes.Max());
boundaryMarker = 0;
boundaryMarker[boundaryAttribute - 1] = 1;
mfem::Array<int> expected;
finiteElementSpace.GetEssentialTrueDofs(boundaryMarker, expected);
return expected;
}
void check_equal(
const mfem::Array<int> &actual,
const mfem::Array<int> &expected
) {
REQUIRE(actual.Size() == expected.Size());
for (int index = 0; index < actual.Size(); ++index) {
CAPTURE(index);
CHECK(actual[index] == expected[index]);
}
}
[[nodiscard]] mfem::Mesh make_boundary_mesh() {
return mfem::Mesh::MakeCartesian2D(6, 4, mfem::Element::QUADRILATERAL, true, 3.0, 2.0);
}
template <typename SchemaT>
concept CanMakeStellarSurfaceMap = requires(const mfem::ParFiniteElementSpace &finiteElementSpace) {
field::make_stellar_surface_scalar_dof_map<SchemaT>(finiteElementSpace);
};
} // namespace surface_scalar_dof_test_utils
TEST_CASE(
"Scalar Boundary DOF Map Preserves Canonical Surface Coordinate Indexing",
tags::surface_deformation_dof_unit
) {
namespace field = mean_field::field;
const field::ScalarBoundaryDofMap map(8, surface_scalar_dof_test_utils::make_array({1, 3, 6}), 5, 12);
CHECK(map.volume_true_dof_size() == 8);
CHECK(map.local_size() == 3);
CHECK(map.global_offset() == 5);
CHECK(map.global_size() == 12);
CHECK_FALSE(map.empty());
CHECK(map.volume_true_dof(0) == 1);
CHECK(map.volume_true_dof(1) == 3);
CHECK(map.volume_true_dof(2) == 6);
CHECK(map.global_boundary_dof(0) == 5);
CHECK(map.global_boundary_dof(1) == 6);
CHECK(map.global_boundary_dof(2) == 7);
REQUIRE(map.local_boundary_dof(1).has_value());
REQUIRE(map.local_boundary_dof(3).has_value());
REQUIRE(map.local_boundary_dof(6).has_value());
CHECK(*map.local_boundary_dof(1) == 0);
CHECK(*map.local_boundary_dof(3) == 1);
CHECK(*map.local_boundary_dof(6) == 2);
CHECK_FALSE(map.local_boundary_dof(0).has_value());
mfem::Vector volumeValues(8);
for (int trueDof = 0; trueDof < volumeValues.Size(); ++trueDof) {
volumeValues(trueDof) = 10.0 + trueDof;
}
const mfem::Vector boundaryValues = map.gather(volumeValues);
REQUIRE(boundaryValues.Size() == map.local_size());
CHECK(boundaryValues(0) == 11.0);
CHECK(boundaryValues(1) == 13.0);
CHECK(boundaryValues(2) == 16.0);
const mfem::Vector scattered = map.scatter(boundaryValues);
REQUIRE(scattered.Size() == map.volume_true_dof_size());
for (int trueDof = 0; trueDof < scattered.Size(); ++trueDof) {
const std::optional<int> localBoundaryDof = map.local_boundary_dof(trueDof);
if (localBoundaryDof.has_value()) {
CHECK(scattered(trueDof) == boundaryValues(*localBoundaryDof));
} else {
CHECK(scattered(trueDof) == 0.0);
}
}
CHECK_THROWS_AS(
(field::ScalarBoundaryDofMap(8, surface_scalar_dof_test_utils::make_array({3, 1}), 0, 2)), std::invalid_argument
);
CHECK_THROWS_AS(
(field::ScalarBoundaryDofMap(8, surface_scalar_dof_test_utils::make_array({1, 3, 6}), -1, 3)),
std::invalid_argument
);
CHECK_THROWS_AS(
(field::ScalarBoundaryDofMap(8, surface_scalar_dof_test_utils::make_array({1, 3, 6}), 4, 6)),
std::invalid_argument
);
CHECK_THROWS_AS(map.global_boundary_dof(-1), std::out_of_range);
CHECK_THROWS_AS(map.global_boundary_dof(map.local_size()), std::out_of_range);
}
TEST_CASE(
"Stellar Surface Coordinates Use The Scalar Displacement Basis And Only Surface DOFs",
tags::surface_deformation_dof_topology
) {
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());
REQUIRE(fem.surfaceDeformationFes != nullptr);
REQUIRE(fem.displacementFes != nullptr);
CHECK(fem.surfaceDeformationFes->GetVDim() == 1);
CHECK(fem.surfaceDeformationFes->FEColl() == fem.displacementFes->FEColl());
CHECK(fem.surfaceDeformationFes.get() != fem.enthalpyFes.get());
const field::ScalarBoundaryDofMap surfaceCoordinates =
field::make_stellar_surface_scalar_dof_map<surface_scalar_dof_test_utils::DefaultSchema>(
*fem.surfaceDeformationFes
);
const mfem::Array<int> expected = surface_scalar_dof_test_utils::expected_boundary_true_dofs(
*fem.surfaceDeformationFes,
surface_scalar_dof_test_utils::DefaultSchema::template boundary_attribute<domain::StellarSurface>()
);
surface_scalar_dof_test_utils::check_equal(surfaceCoordinates.boundary_true_dofs(), expected);
CHECK(surfaceCoordinates.global_size() > 0);
CHECK(surfaceCoordinates.global_size() < fem.surfaceDeformationFes->GlobalTrueVSize());
const field::FieldDofMap displacementMap =
field::make_field_dof_map<field::Displacement, surface_scalar_dof_test_utils::DefaultSchema>(
*fem.displacementFes
);
const field::FieldBoundaryDofMap vectorSurface = field::make_field_boundary_dof_map<
field::Displacement, domain::StellarSurface, surface_scalar_dof_test_utils::DefaultSchema>(
*fem.displacementFes, displacementMap
);
long long localVectorSurfaceSize = vectorSurface.size();
long long globalVectorSurfaceSize = 0;
MPI_Allreduce(
&localVectorSurfaceSize, &globalVectorSurfaceSize, 1, MPI_LONG_LONG, MPI_SUM,
fem.surfaceDeformationFes->GetComm()
);
CHECK(
globalVectorSurfaceSize == static_cast<long long>(fem.mesh->SpaceDimension()) * surfaceCoordinates.global_size()
);
}
TEST_CASE(
"Scalar Boundary DOF Resolution Uses Semantic Schema Boundary Attributes",
tags::surface_deformation_dof_schema
) {
namespace domain = mean_field::utils::domain;
namespace field = mean_field::field;
STATIC_CHECK(
surface_scalar_dof_test_utils::CanMakeStellarSurfaceMap<surface_scalar_dof_test_utils::FirstBoundarySchema>
);
STATIC_CHECK_FALSE(
surface_scalar_dof_test_utils::CanMakeStellarSurfaceMap<surface_scalar_dof_test_utils::MissingBoundarySchema>
);
mfem::Mesh serialMesh = surface_scalar_dof_test_utils::make_boundary_mesh();
mfem::ParMesh mesh(MPI_COMM_WORLD, serialMesh);
mfem::H1_FECollection finiteElementCollection(2, mesh.Dimension());
mfem::ParFiniteElementSpace finiteElementSpace(&mesh, &finiteElementCollection);
const field::ScalarBoundaryDofMap firstBoundary =
field::make_stellar_surface_scalar_dof_map<surface_scalar_dof_test_utils::FirstBoundarySchema>(
finiteElementSpace
);
const field::ScalarBoundaryDofMap secondBoundary =
field::make_stellar_surface_scalar_dof_map<surface_scalar_dof_test_utils::SecondBoundarySchema>(
finiteElementSpace
);
const mfem::Array<int> expectedFirst = surface_scalar_dof_test_utils::expected_boundary_true_dofs(
finiteElementSpace,
surface_scalar_dof_test_utils::FirstBoundarySchema::template boundary_attribute<domain::StellarSurface>()
);
const mfem::Array<int> expectedSecond = surface_scalar_dof_test_utils::expected_boundary_true_dofs(
finiteElementSpace,
surface_scalar_dof_test_utils::SecondBoundarySchema::template boundary_attribute<domain::StellarSurface>()
);
surface_scalar_dof_test_utils::check_equal(firstBoundary.boundary_true_dofs(), expectedFirst);
surface_scalar_dof_test_utils::check_equal(secondBoundary.boundary_true_dofs(), expectedSecond);
bool localMapsDiffer = firstBoundary.local_size() != secondBoundary.local_size();
if (!localMapsDiffer) {
for (int localDof = 0; localDof < firstBoundary.local_size(); ++localDof) {
if (firstBoundary.volume_true_dof(localDof) != secondBoundary.volume_true_dof(localDof)) {
localMapsDiffer = true;
break;
}
}
}
int localDifference = localMapsDiffer ? 1 : 0;
int globalDifference = 0;
MPI_Allreduce(&localDifference, &globalDifference, 1, MPI_INT, MPI_MAX, finiteElementSpace.GetComm());
CHECK(globalDifference == 1);
}
TEST_CASE(
"Scalar Boundary Coordinates Have Deterministic Contiguous Parallel Ordering",
tags::surface_deformation_dof_parallel
) {
namespace field = mean_field::field;
mfem::Mesh serialMesh = surface_scalar_dof_test_utils::make_boundary_mesh();
mfem::ParMesh mesh(MPI_COMM_WORLD, serialMesh);
mfem::H1_FECollection finiteElementCollection(2, mesh.Dimension());
mfem::ParFiniteElementSpace finiteElementSpace(&mesh, &finiteElementCollection);
const field::ScalarBoundaryDofMap first =
field::make_stellar_surface_scalar_dof_map<surface_scalar_dof_test_utils::FirstBoundarySchema>(
finiteElementSpace
);
const field::ScalarBoundaryDofMap second =
field::make_stellar_surface_scalar_dof_map<surface_scalar_dof_test_utils::FirstBoundarySchema>(
finiteElementSpace
);
surface_scalar_dof_test_utils::check_equal(first.boundary_true_dofs(), second.boundary_true_dofs());
CHECK(first.global_offset() == second.global_offset());
CHECK(first.global_size() == second.global_size());
for (int localDof = 0; localDof < first.local_size(); ++localDof) {
CAPTURE(localDof);
CHECK(first.global_boundary_dof(localDof) == first.global_offset() + localDof);
if (localDof > 0) {
CHECK(first.volume_true_dof(localDof - 1) < first.volume_true_dof(localDof));
}
}
int communicatorSize = 1;
int communicatorRank = 0;
MPI_Comm_size(finiteElementSpace.GetComm(), &communicatorSize);
MPI_Comm_rank(finiteElementSpace.GetComm(), &communicatorRank);
const long long localSize = first.local_size();
std::vector<long long> localSizes(static_cast<std::size_t>(communicatorSize));
MPI_Allgather(&localSize, 1, MPI_LONG_LONG, localSizes.data(), 1, MPI_LONG_LONG, finiteElementSpace.GetComm());
const long long expectedOffset = std::accumulate(localSizes.begin(), localSizes.begin() + communicatorRank, 0LL);
const long long expectedGlobalSize = std::accumulate(localSizes.begin(), localSizes.end(), 0LL);
CHECK(first.global_offset() == expectedOffset);
CHECK(first.global_size() == expectedGlobalSize);
}

View File

@@ -8,6 +8,8 @@
#include <catch2/catch_test_macros.hpp>
#include <mfem.hpp>
import mean_field;
import test_helpers;
@@ -96,19 +98,18 @@ namespace {
} // namespace
TEST_CASE(
"Stellar Model Owns Structure And Surface Prescriptions",
tags::stellar_model_type_contract
"Stellar Model Owns Structure Prescription And Surface Condition",
tags::stellar_model_type_contract &tags::surface_condition_type_contract
) {
STATIC_CHECK(mean_field::models::StructurePrescription<mean_field::models::structure::PolytropicStructure>);
STATIC_CHECK(mean_field::models::StructurePrescription<StellarModelTestStructure>);
STATIC_CHECK_FALSE(mean_field::models::StructurePrescription<StructureWithoutSeed>);
STATIC_CHECK(
mean_field::models::SurfacePrescription<
mean_field::surface::ConstantPressureSurface, mean_field::eos::Polytrope>
mean_field::models::SurfaceCondition<mean_field::surface::ConstantPressureSurface, mean_field::eos::Polytrope>
);
STATIC_CHECK_FALSE(
mean_field::models::SurfacePrescription<SurfaceWithoutPhysicalQuantity, mean_field::eos::Polytrope>
mean_field::models::SurfaceCondition<SurfaceWithoutPhysicalQuantity, mean_field::eos::Polytrope>
);
STATIC_CHECK_FALSE(std::derived_from<StellarModelTestStructure, mean_field::models::structure::StructureBase>);
@@ -141,14 +142,68 @@ TEST_CASE(
decltype(model.structurePrescription()), const mean_field::models::structure::PolytropicStructure &>
);
STATIC_CHECK(
std::same_as<decltype(model.surfacePrescription()), const mean_field::surface::ConstantPressureSurface &>
std::same_as<decltype(model.surfaceCondition()), const mean_field::surface::ConstantPressureSurface &>
);
STATIC_CHECK(std::same_as<decltype(model.equationOfState()), const mean_field::eos::Polytrope &>);
STATIC_CHECK(
std::same_as<
typename PolytropicStellarModel::SurfaceDeformationPrescriptionType,
mean_field::deformation::NodalRadialSurface>
);
STATIC_CHECK(
std::same_as<
typename PolytropicStellarModel::StellarInteriorDeformationExtensionType,
mean_field::deformation::PowerLawRadialInteriorExtension>
);
STATIC_CHECK(
std::same_as<
typename PolytropicStellarModel::VacuumDeformationExtensionType,
mean_field::deformation::FixedInfinityRadialVacuumExtension>
);
CHECK(model.targetMass() == 1.0);
CHECK(model.compiledSurfaceConstraint().targetPressure() == mean_field::eos::PressureValue{0.0});
CHECK(&model.equationOfState() == &model.structurePrescription().equationOfState());
CHECK(model.surfacePrescription().targetPressure() == mean_field::eos::PressureValue{0.0});
CHECK(model.surfaceCondition().targetPressure() == mean_field::eos::PressureValue{0.0});
CHECK(model.surfaceDeformationPrescription().descriptor().name == "NodalRadialSurface");
CHECK(model.stellarInteriorDeformationExtension().radialPower() == 2.0);
CHECK(model.vacuumDeformationExtension().descriptor().name == "FixedInfinityRadialVacuumExtension");
}
TEST_CASE(
"Stellar Model Owns Explicit Surface Interior And Vacuum Deformation Policies",
tags::stellar_model_deformation_ownership
) {
mfem::Vector referenceCenter(3);
referenceCenter(0) = 0.125;
referenceCenter(1) = -0.25;
referenceCenter(2) = 0.375;
mean_field::models::StellarModel model{
mean_field::models::structure::PolytropicStructure{mean_field::eos::Polytrope{3.0, 0.25}, 1.0},
mean_field::surface::ConstantPressureSurface{mean_field::eos::PressureValue{0.0}},
mean_field::deformation::NodalRadialSurface{referenceCenter},
mean_field::deformation::PowerLawRadialInteriorExtension{3.0},
mean_field::deformation::FixedInfinityRadialVacuumExtension{}
};
CHECK(model.surfaceDeformationPrescription().referenceCenter()(0) == referenceCenter(0));
CHECK(model.surfaceDeformationPrescription().referenceCenter()(1) == referenceCenter(1));
CHECK(model.surfaceDeformationPrescription().referenceCenter()(2) == referenceCenter(2));
CHECK(model.stellarInteriorDeformationExtension().radialPower() == 3.0);
CHECK(
model.vacuumDeformationExtension().descriptor().outerBoundaryBehavior ==
mean_field::deformation::VacuumOuterBoundaryBehavior::FixedAtReferenceInfinity
);
const auto *surfaceDeformationAddress = &model.surfaceDeformationPrescription();
const auto *interiorDeformationAddress = &model.stellarInteriorDeformationExtension();
const auto *vacuumDeformationAddress = &model.vacuumDeformationExtension();
auto movedModel = std::move(model);
CHECK(&movedModel.surfaceDeformationPrescription() == surfaceDeformationAddress);
CHECK(&movedModel.stellarInteriorDeformationExtension() == interiorDeformationAddress);
CHECK(&movedModel.vacuumDeformationExtension() == vacuumDeformationAddress);
}
TEST_CASE(
@@ -184,7 +239,7 @@ TEST_CASE(
const mean_field::models::structure::PolytropicStructure *structureAddress = &originalModel.structurePrescription();
const mean_field::surface::ConstantPressureSurface *surfaceAddress = &originalModel.surfacePrescription();
const mean_field::surface::ConstantPressureSurface *surfaceAddress = &originalModel.surfaceCondition();
const mean_field::eos::Polytrope *equationOfStateAddress = &originalModel.equationOfState();
@@ -193,7 +248,7 @@ TEST_CASE(
mean_field::models::StellarModel movedModel{std::move(originalModel)};
CHECK(&movedModel.structurePrescription() == structureAddress);
CHECK(&movedModel.surfacePrescription() == surfaceAddress);
CHECK(&movedModel.surfaceCondition() == surfaceAddress);
CHECK(&movedModel.equationOfState() == equationOfStateAddress);
CHECK(&movedModel.compiledSurfaceConstraint() == compiledSurfaceConstraintAddress);
CHECK(movedModel.targetMass() == 1.0);
@@ -221,7 +276,7 @@ TEST_CASE(
}
TEST_CASE(
"Stellar Model Supports Custom Structure And Surface Prescriptions",
"Stellar Model Supports Custom Structure Prescriptions And Surface Conditions",
tags::barotrope &tags::unit &tags::model
) {
const auto tracker = std::make_shared<StellarModelExtensionTracker>();
@@ -261,9 +316,12 @@ TEST_CASE(
const mean_field::models::structure::PolytropicStructure *sourceStructureAddress =
&sourceModel.structurePrescription();
const mean_field::surface::ConstantPressureSurface *sourceSurfaceAddress = &sourceModel.surfacePrescription();
const mean_field::surface::ConstantPressureSurface *sourceSurfaceAddress = &sourceModel.surfaceCondition();
const mean_field::eos::Polytrope *sourceEquationOfStateAddress = &sourceModel.equationOfState();
const auto *sourceSurfaceDeformationAddress = &sourceModel.surfaceDeformationPrescription();
const auto *sourceInteriorDeformationAddress = &sourceModel.stellarInteriorDeformationExtension();
const auto *sourceVacuumDeformationAddress = &sourceModel.vacuumDeformationExtension();
const mean_field::eos::PressureValue sourceTargetPressure =
sourceModel.compiledSurfaceConstraint().targetPressure();
@@ -272,10 +330,16 @@ TEST_CASE(
CHECK(&destinationModel.structurePrescription() == sourceStructureAddress);
CHECK(&destinationModel.surfacePrescription() == sourceSurfaceAddress);
CHECK(&destinationModel.surfaceCondition() == sourceSurfaceAddress);
CHECK(&destinationModel.equationOfState() == sourceEquationOfStateAddress);
CHECK(&destinationModel.surfaceDeformationPrescription() == sourceSurfaceDeformationAddress);
CHECK(&destinationModel.stellarInteriorDeformationExtension() == sourceInteriorDeformationAddress);
CHECK(&destinationModel.vacuumDeformationExtension() == sourceVacuumDeformationAddress);
CHECK(destinationModel.targetMass() == 1.25);
CHECK(destinationModel.compiledSurfaceConstraint().targetPressure() == sourceTargetPressure);
@@ -307,6 +371,13 @@ TEST_CASE(
CHECK(views[0].targetMass() == 1.0);
CHECK(views[1].targetMass() == 2.5);
CHECK(views[1].surfaceCondition().targetPressure == 0.375);
CHECK(views[0].surfaceDeformation().name == "NodalRadialSurface");
CHECK(views[0].surfaceDeformation().motionKind == mean_field::deformation::SurfaceMotionKind::Radial);
CHECK(views[0].stellarInteriorDeformation().name == "PowerLawRadialInteriorExtension");
CHECK(
views[0].vacuumDeformation().outerBoundaryBehavior ==
mean_field::deformation::VacuumOuterBoundaryBehavior::FixedAtReferenceInfinity
);
REQUIRE(views[1].surfaceDependencies().stateFields.size() == 1);
CHECK(
views[1].surfaceDependencies().residualRowField ==
@@ -350,6 +421,9 @@ TEST_CASE(
REQUIRE(pressure.has_value());
CHECK(view.targetMass() == movedModel.targetMass());
CHECK(view.surfaceDeformation() == movedModel.surfaceDeformationPrescription().descriptor());
CHECK(view.stellarInteriorDeformation() == movedModel.stellarInteriorDeformationExtension().descriptor());
CHECK(view.vacuumDeformation() == movedModel.vacuumDeformationExtension().descriptor());
CHECK(
pressure->value() == mean_field::eos::evaluate<mean_field::eos::quantity::Pressure>(
movedModel.equationOfState(), mean_field::eos::DensityValue{0.7}
@@ -358,3 +432,48 @@ TEST_CASE(
);
CHECK(seed.radius.Size() == 8);
}
TEST_CASE(
"Stellar Model Compiles Its Deformation Policies Against The Finite Element Discretization",
tags::stellar_model_deformation_compilation
) {
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());
mfem::Vector referenceCenter(fem.mesh->SpaceDimension());
referenceCenter = 0.0;
const mean_field::models::StellarModel model{
mean_field::models::structure::PolytropicStructure{mean_field::eos::Polytrope{3.0, 0.25}, 1.0},
mean_field::surface::ConstantPressureSurface{mean_field::eos::PressureValue{0.0}},
mean_field::deformation::NodalRadialSurface{referenceCenter},
mean_field::deformation::PowerLawRadialInteriorExtension{3.0},
mean_field::deformation::FixedInfinityRadialVacuumExtension{}
};
auto prepared = model.compileDomainDeformation(fem);
STATIC_CHECK(mean_field::deformation::PreparedDomainDeformationOperator<decltype(prepared)>);
CHECK(prepared.matchesCurrentDiscretization());
CHECK(prepared.stellarInteriorExtension().radialPower() == 3.0);
CHECK(prepared.discretizationDependencies().physicalMeshIdentity == fem.mesh.get());
CHECK(prepared.discretizationDependencies().logicalReferenceMeshIdentity == fem.logicalReferenceMesh.get());
CHECK(prepared.parameterCount() == prepared.surfaceDeformationPrescription().parameterCount());
CHECK(prepared.volumeDisplacementSize() == fem.displacementFes->GetTrueVSize());
STATIC_CHECK_FALSE(std::is_copy_constructible_v<mean_field::deformation::PreparedDomainDeformationRuntime>);
STATIC_CHECK(std::is_nothrow_move_constructible_v<mean_field::deformation::PreparedDomainDeformationRuntime>);
mean_field::deformation::PreparedDomainDeformationRuntime runtime{std::move(prepared)};
STATIC_CHECK(mean_field::deformation::PreparedDomainDeformationOperator<decltype(runtime)>);
mean_field::deformation::PreparedDomainDeformationRuntime movedRuntime{std::move(runtime)};
CHECK(movedRuntime.matchesCurrentDiscretization());
CHECK(movedRuntime.parameterCount() > 0);
CHECK(movedRuntime.volumeDisplacementSize() == fem.displacementFes->GetTrueVSize());
CHECK(movedRuntime.compositionReport().assignedScalarDofCount() == fem.surfaceDeformationFes->GetTrueVSize());
mfem::Vector zeroParameters(movedRuntime.parameterCount());
mfem::Vector volumeDisplacement(movedRuntime.volumeDisplacementSize());
zeroParameters = 0.0;
movedRuntime.buildVolumeDisplacement(zeroParameters, volumeDisplacement);
CHECK(volumeDisplacement.Norml2() == 0.0);
}

View File

@@ -388,21 +388,21 @@ namespace stellar_equilibrium_test_utils {
[[nodiscard]] mean_field::operators::StellarEquilibriumDependencies make_dependencies() {
return {
.discretization = {.identity = 1009, .revision = 3},
.density = {.identity = 1013, .revision = 5},
.displacement = {.identity = 1019, .revision = 7},
.gravityGradient = {.identity = 1021, .revision = 11},
.gravityPotential = {.identity = 1031, .revision = 13},
.enthalpy = {.identity = 1033, .revision = 17},
.bernoulliConstant = {.identity = 1039, .revision = 19},
.rotation = {.identity = 1049, .revision = 23},
.targetMass = {.identity = 1051, .revision = 29}
.discretization = {.identity = 1009, .revision = 3},
.density = {.identity = 1013, .revision = 5},
.surfaceDeformation = {.identity = 1019, .revision = 7},
.gravityGradient = {.identity = 1021, .revision = 11},
.gravityPotential = {.identity = 1031, .revision = 13},
.enthalpy = {.identity = 1033, .revision = 17},
.bernoulliConstant = {.identity = 1039, .revision = 19},
.rotation = {.identity = 1049, .revision = 23},
.targetMass = {.identity = 1051, .revision = 29}
};
}
void increment_all_state_revisions(mean_field::operators::StellarEquilibriumDependencies &dependencies) {
++dependencies.density.revision;
++dependencies.displacement.revision;
++dependencies.surfaceDeformation.revision;
++dependencies.gravityGradient.revision;
++dependencies.gravityPotential.revision;
++dependencies.enthalpy.revision;
@@ -424,7 +424,9 @@ namespace stellar_equilibrium_test_utils {
assign_value_block(state, layout, densityValue, reducedDensity);
}
assign_value_block(state, layout, displacementValue, project_displacement(f, 0.63));
mfem::Vector surfaceDeformation(layout.size(displacementValue));
surfaceDeformation = 0.0;
assign_value_block(state, layout, displacementValue, surfaceDeformation);
assign_value_block(state, layout, gravityGradientValue, project_gravity_gradient(f, 0.43));
assign_value_block(state, layout, gravityPotentialValue, project_gravity_potential(f, 0.47));
@@ -454,7 +456,11 @@ namespace stellar_equilibrium_test_utils {
assign_value_block(direction, layout, densityValue, reducedDensityDirection);
}
assign_value_block(direction, layout, displacementValue, project_displacement_direction(f, 0.79));
mfem::Vector surfaceDeformationDirection(layout.size(displacementValue));
for (int parameter = 0; parameter < surfaceDeformationDirection.Size(); ++parameter) {
surfaceDeformationDirection(parameter) = 0.11 * std::cos(0.41 * static_cast<double>(parameter) + 0.79);
}
assign_value_block(direction, layout, displacementValue, surfaceDeformationDirection);
assign_value_block(direction, layout, gravityGradientValue, project_gravity_direction(f, 0.83));
assign_value_block(direction, layout, gravityPotentialValue, project_potential_direction(f, 0.89));
@@ -497,7 +503,7 @@ namespace stellar_equilibrium_test_utils {
) {
const mean_field::operators::StellarEquilibriumLayout &layout = stellarOperator.GetLayout();
const mfem::Vector reducedDensity = const_value_view(state, layout, densityValue);
const mfem::Vector displacement = const_value_view(state, layout, displacementValue);
const mfem::Vector &displacement = stellarOperator.GetGeneratedVolumeDisplacement();
const mfem::Vector gravityGradient = const_value_view(state, layout, gravityGradientValue);
const mfem::Vector gravityPotential = const_value_view(state, layout, gravityPotentialValue);
@@ -513,7 +519,10 @@ namespace stellar_equilibrium_test_utils {
stellarOperator.GetGravityOperator().Mult(gravityState, gravity);
stellarOperator.GetBarotropicClosureOperator().BuildResidual(closure);
stellarOperator.GetDisplacementOperator().BuildResidual(displacementResidualValue);
stellarOperator.GetCenteringConstraintOperator().ApplyResidualRows(displacementResidualValue);
mfem::Vector surfaceShapeResidualValue(stellarOperator.GetDomainDeformation().parameterCount());
stellarOperator.GetDomainDeformation().applyJacobianTranspose(
stellarOperator.GetSurfaceDeformationParameters(), displacementResidualValue, surfaceShapeResidualValue
);
stellarOperator.GetHydrostaticOperator().BuildResidual(hydrostatic);
stellarOperator.GetSurfaceConstraintOperator().ApplyResidualRows(hydrostatic);
stellarOperator.GetMassNormalizationOperator().BuildResidual(mass);
@@ -536,7 +545,7 @@ namespace stellar_equilibrium_test_utils {
residual_view(result, layout, densityResidual) = closure;
residual_view(result, layout, displacementResidual) = displacementResidualValue;
residual_view(result, layout, displacementResidual) = surfaceShapeResidualValue;
residual_view(result, layout, enthalpyResidual) = hydrostatic;
@@ -551,8 +560,12 @@ namespace stellar_equilibrium_test_utils {
) {
const mean_field::operators::StellarEquilibriumLayout &layout = stellarOperator.GetLayout();
const mfem::Vector reducedDensityDirection = const_value_view(direction, layout, densityValue);
const mfem::Vector displacementDirection = const_value_view(direction, layout, displacementValue);
const mfem::Vector reducedDensityDirection = const_value_view(direction, layout, densityValue);
const mfem::Vector surfaceDeformationDirection = const_value_view(direction, layout, displacementValue);
mfem::Vector displacementDirection(stellarOperator.GetDomainDeformation().volumeDisplacementSize());
stellarOperator.GetDomainDeformation().applyJacobian(
stellarOperator.GetSurfaceDeformationParameters(), surfaceDeformationDirection, displacementDirection
);
const mfem::Vector gravityGradientDirection = const_value_view(direction, layout, gravityGradientValue);
const mfem::Vector gravityPotentialDirection = const_value_view(direction, layout, gravityPotentialValue);
const mfem::Vector reducedEnthalpyDirection = const_value_view(direction, layout, enthalpyValue);
@@ -578,7 +591,16 @@ namespace stellar_equilibrium_test_utils {
reducedDensityDirection, displacementDirection, gravityGradientDirection, reducedEnthalpyDirection,
displacementAction
);
stellarOperator.GetCenteringConstraintOperator().ApplyJacobianRows(displacementDirection, displacementAction);
mfem::Vector surfaceShapeAction(stellarOperator.GetDomainDeformation().parameterCount());
stellarOperator.GetDomainDeformation().applyJacobianTranspose(
stellarOperator.GetSurfaceDeformationParameters(), displacementAction, surfaceShapeAction
);
mfem::Vector pullbackDerivative(stellarOperator.GetDomainDeformation().parameterCount());
stellarOperator.GetDomainDeformation().applyPullbackDerivative(
stellarOperator.GetSurfaceDeformationParameters(), surfaceDeformationDirection,
stellarOperator.GetFullMechanicalResidual(), pullbackDerivative
);
surfaceShapeAction += pullbackDerivative;
stellarOperator.GetHydrostaticOperator().ApplyCompleteJacobianAction(
reducedEnthalpyDirection, gravityPotentialDirection, bernoulliDirection(0), displacementDirection,
@@ -608,7 +630,7 @@ namespace stellar_equilibrium_test_utils {
residual_view(result, layout, densityResidual) = closureAction;
residual_view(result, layout, displacementResidual) = displacementAction;
residual_view(result, layout, displacementResidual) = surfaceShapeAction;
residual_view(result, layout, enthalpyResidual) = hydrostaticAction;
@@ -644,8 +666,8 @@ namespace stellar_equilibrium_test_utils {
} // namespace stellar_equilibrium_test_utils
TEST_CASE(
"Prepared Stellar Equilibrium Uses Supported Field DOFs For Solver Blocks",
tags::barotrope &tags::prepared &tags::field &tags::unit
"Prepared Stellar Equilibrium Uses Surface Parameters For Its Root Geometry Block",
tags::reduced_stellar_geometry &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);
@@ -660,12 +682,22 @@ TEST_CASE(
);
CHECK(stellarOperator.GetTargetMass() == stellarModel.targetMass());
CHECK(stellarOperator.GetDomainDeformation().matchesCurrentDiscretization());
const mean_field::field::ScalarBoundaryDofMap surfaceDeformationMap =
mean_field::field::make_stellar_surface_scalar_dof_map<stellar_equilibrium_test_utils::DomainSchema>(
*f.surfaceDeformationFes
);
CHECK(stellarOperator.GetDomainDeformation().parameterCount() == surfaceDeformationMap.local_size());
CHECK(stellarOperator.GetDomainDeformation().volumeDisplacementSize() == f.displacementFes->GetTrueVSize());
const auto &layout = stellarOperator.GetLayout();
const stellar_equilibrium_test_utils::FieldMaps maps(f);
CHECK(layout.size(stellar_equilibrium_test_utils::densityValue) == maps.density.reduced_size());
CHECK(layout.size(stellar_equilibrium_test_utils::displacementValue) == maps.displacement.reduced_size());
CHECK(
layout.size(stellar_equilibrium_test_utils::displacementValue) ==
stellarOperator.GetDomainDeformation().parameterCount()
);
CHECK(layout.size(stellar_equilibrium_test_utils::gravityGradientValue) == maps.gravityFlux.reduced_size());
CHECK(layout.size(stellar_equilibrium_test_utils::gravityPotentialValue) == maps.gravityPotential.reduced_size());
CHECK(layout.size(stellar_equilibrium_test_utils::enthalpyValue) == maps.enthalpy.reduced_size());
@@ -676,7 +708,10 @@ TEST_CASE(
layout.size(stellar_equilibrium_test_utils::gravityPotentialResidual) == maps.gravityPotential.reduced_size()
);
CHECK(layout.size(stellar_equilibrium_test_utils::densityResidual) == maps.density.reduced_size());
CHECK(layout.size(stellar_equilibrium_test_utils::displacementResidual) == maps.displacement.reduced_size());
CHECK(
layout.size(stellar_equilibrium_test_utils::displacementResidual) ==
stellarOperator.GetDomainDeformation().parameterCount()
);
CHECK(layout.size(stellar_equilibrium_test_utils::enthalpyResidual) == maps.enthalpy.reduced_size());
CHECK(layout.size(stellar_equilibrium_test_utils::massResidual) == 1);
@@ -715,9 +750,8 @@ TEST_CASE(
}
TEST_CASE(
"Prepared Stellar Equilibrium Jacobian Is The Exact Restricted Full "
"Child Jacobian",
tags::barotrope_prepared_jacobian_accuracy &tags::field
"Prepared Stellar Equilibrium Jacobian Is The Lifted And Pulled Back Full Child Jacobian",
tags::barotrope_prepared_jacobian_accuracy &tags::reduced_stellar_geometry &tags::field
) {
mean_field::utils::Args args = test_utils::setup_args();
mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0);
@@ -834,8 +868,8 @@ TEST_CASE(
}
TEST_CASE(
"Prepared Stellar Equilibrium Replaces Center Force Rows With A Translational Centering Constraint",
tags::translational_centering_enforcement
"Reduced Stellar Geometry Uses Surface Parameters And Generates An Orientation Preserving Volume Map",
tags::reduced_stellar_geometry &tags::prepared
) {
mean_field::utils::Args args = test_utils::setup_args();
mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0);
@@ -847,75 +881,25 @@ TEST_CASE(
f, *f.domainMapperStateless, stellarModel
);
const auto &layout = stellarOperator.GetLayout();
mfem::Vector state = stellar_equilibrium_test_utils::make_state(f, layout);
const auto &layout = stellarOperator.GetLayout();
const mfem::Vector state = stellar_equilibrium_test_utils::make_state(f, layout);
mfem::Vector translation(f.mesh->SpaceDimension());
translation(0) = 0.17;
translation(1) = -0.11;
translation(2) = 0.08;
mfem::VectorConstantCoefficient translationCoefficient(translation);
mfem::ParGridFunction translationField(f.displacementFes.get());
translationField.ProjectCoefficient(translationCoefficient);
mfem::Vector translationTrue;
translationField.GetTrueDofs(translationTrue);
stellar_equilibrium_test_utils::assign_value_block(
state, layout, stellar_equilibrium_test_utils::displacementValue, translationTrue
);
const auto report = stellarOperator.Prepare(
const auto report = stellarOperator.Prepare(
state, stellar_equilibrium_test_utils::make_dependencies(), stellar_equilibrium_test_utils::make_zero_rotation()
);
CHECK(report.centeringConstraint.cachedCenterDisplacement);
const auto &centerRows = stellarOperator.GetCenteringConstraintOperator().GetCenterRows();
CHECK(stellar_equilibrium_test_utils::global_sum(centerRows.size(), f.mesh->GetComm()) == f.mesh->SpaceDimension());
const int surfaceParameterCount = stellarOperator.GetDomainDeformation().parameterCount();
const int volumeDisplacementSize = stellarOperator.GetDomainDeformation().volumeDisplacementSize();
mfem::Vector residual;
stellarOperator.BuildResidual(residual);
const mfem::Vector displacementState = stellar_equilibrium_test_utils::const_value_view(
state, layout, stellar_equilibrium_test_utils::displacementValue
);
const mfem::Vector displacementResidual = stellar_equilibrium_test_utils::const_residual_view(
residual, layout, stellar_equilibrium_test_utils::displacementResidual
);
for (const int centerRow : centerRows.reduced_dofs()) {
CAPTURE(centerRow);
CHECK(displacementResidual(centerRow) == displacementState(centerRow));
}
mfem::Vector translationDirection(layout.value_offsets().Last());
translationDirection = 0.0;
stellar_equilibrium_test_utils::assign_value_block(
translationDirection, layout, stellar_equilibrium_test_utils::displacementValue, translationTrue
);
mfem::Vector action;
stellarOperator.Mult(translationDirection, action);
const mfem::Vector displacementDirection = stellar_equilibrium_test_utils::const_value_view(
translationDirection, layout, stellar_equilibrium_test_utils::displacementValue
);
const mfem::Vector displacementAction = stellar_equilibrium_test_utils::const_residual_view(
action, layout, stellar_equilibrium_test_utils::displacementResidual
);
for (const int centerRow : centerRows.reduced_dofs()) {
CAPTURE(centerRow);
CHECK(displacementAction(centerRow) == displacementDirection(centerRow));
}
mfem::Vector nonDisplacementDirection = stellar_equilibrium_test_utils::make_direction(f, layout);
stellar_equilibrium_test_utils::value_view(
nonDisplacementDirection, layout, stellar_equilibrium_test_utils::displacementValue
) = 0.0;
stellarOperator.Mult(nonDisplacementDirection, action);
const mfem::Vector nonDisplacementAction = stellar_equilibrium_test_utils::const_residual_view(
action, layout, stellar_equilibrium_test_utils::displacementResidual
);
for (const int centerRow : centerRows.reduced_dofs()) {
CAPTURE(centerRow);
CHECK(nonDisplacementAction(centerRow) == 0.0);
}
CHECK(layout.size(stellar_equilibrium_test_utils::displacementValue) == surfaceParameterCount);
CHECK(layout.size(stellar_equilibrium_test_utils::displacementResidual) == surfaceParameterCount);
CHECK(surfaceParameterCount < volumeDisplacementSize);
CHECK(stellarOperator.GetSurfaceDeformationParameters().Size() == surfaceParameterCount);
CHECK(stellarOperator.GetGeneratedVolumeDisplacement().Size() == volumeDisplacementSize);
CHECK(report.generatedVolumeDisplacement);
CHECK(report.generatedGeometry.isOrientationPreserving());
CHECK(report.generatedDisplacement.identity != 0);
CHECK(report.generatedDisplacement.revision == 1);
}
TEST_CASE(
@@ -949,7 +933,7 @@ TEST_CASE(
CHECK(report.displacement.DidAnyWork());
CHECK(report.massNormalization.DidAnyWork());
CHECK(report.surfaceConstraint.DidAnyWork());
CHECK(report.centeringConstraint.DidAnyWork());
CHECK(report.generatedVolumeDisplacement);
CHECK(report.assembledResidual);
CHECK(stellarOperator.IsPrepared());
@@ -1227,7 +1211,7 @@ TEST_CASE(
TEST_CASE(
"Prepared Stellar Equilibrium Complete Jacobian Matches Every "
"Coupled Centered Difference Block",
tags::barotrope &tags::prepared &tags::jacobian &tags::accuracy &tags::geometry
tags::barotrope &tags::prepared &tags::jacobian &tags::accuracy &tags::reduced_stellar_geometry
) {
mean_field::utils::Args args = test_utils::setup_args();
mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0);
@@ -1398,7 +1382,7 @@ TEST_CASE(
TEST_CASE(
"Prepared Stellar Equilibrium Selectively Invalidates Rows And Never "
"Reprepares In Krylov Mult",
tags::barotrope &tags::prepared &tags::contexts &tags::mfem_operators
tags::barotrope &tags::prepared &tags::contexts &tags::mfem_operators &tags::reduced_stellar_geometry
) {
mean_field::utils::Args args = test_utils::setup_args();
mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0);
@@ -1423,10 +1407,14 @@ TEST_CASE(
stellarOperator.GetDisplacementOperator().GetResidualPreparationCount();
const std::uint64_t massPreparations = stellarOperator.GetMassNormalizationOperator().GetPreparationCount();
const std::uint64_t rootAssemblies = stellarOperator.GetStatistics().residualAssemblies;
const std::uint64_t geometryBuilds = stellarOperator.GetStatistics().generatedGeometryBuilds;
const auto generatedDisplacement = stellarOperator.GetGeneratedDisplacementDependency();
const auto repeated = stellarOperator.Prepare(state, dependencies, rotation);
CHECK_FALSE(repeated.DidAnyWork());
CHECK(stellarOperator.GetStatistics().residualAssemblies == rootAssemblies);
CHECK(stellarOperator.GetStatistics().generatedGeometryBuilds == geometryBuilds);
CHECK(stellarOperator.GetGeneratedDisplacementDependency() == generatedDisplacement);
const mfem::Vector direction = stellar_equilibrium_test_utils::make_direction(f, layout);
mfem::Vector action;
@@ -1440,6 +1428,26 @@ TEST_CASE(
CHECK(stellarOperator.GetMassNormalizationOperator().GetPreparationCount() == massPreparations);
CHECK(stellarOperator.GetStatistics().residualAssemblies == rootAssemblies);
CHECK(stellarOperator.GetStatistics().jacobianApplications == 3);
CHECK(stellarOperator.GetStatistics().generatedGeometryBuilds == geometryBuilds);
mfem::Vector surfaceDeformation =
stellar_equilibrium_test_utils::value_view(state, layout, stellar_equilibrium_test_utils::displacementValue);
for (int parameter = 0; parameter < surfaceDeformation.Size(); ++parameter) {
surfaceDeformation(parameter) += 1.0e-4;
}
++dependencies.surfaceDeformation.revision;
const auto surfaceDeformationReport = stellarOperator.Prepare(state, dependencies, rotation);
CHECK(surfaceDeformationReport.generatedVolumeDisplacement);
CHECK(surfaceDeformationReport.generatedGeometry.isOrientationPreserving());
CHECK(surfaceDeformationReport.generatedDisplacement.identity == generatedDisplacement.identity);
CHECK(surfaceDeformationReport.generatedDisplacement.revision == generatedDisplacement.revision + 1);
CHECK(stellarOperator.GetStatistics().generatedGeometryBuilds == geometryBuilds + 1);
CHECK(surfaceDeformationReport.gravity.DidAnyWork());
CHECK(surfaceDeformationReport.barotropicClosure.DidAnyWork());
CHECK(surfaceDeformationReport.hydrostatic.DidAnyWork());
CHECK(surfaceDeformationReport.displacement.DidAnyWork());
CHECK(surfaceDeformationReport.massNormalization.DidAnyWork());
stellar_equilibrium_test_utils::value_view(state, layout, stellar_equilibrium_test_utils::gravityPotentialValue)
.Add(0.03, stellar_equilibrium_test_utils::project_potential_direction(f, 0.41));
@@ -1476,8 +1484,79 @@ TEST_CASE(
}
TEST_CASE(
"Prepared Stellar Equilibrium Matches The Analytic N1 Lane Emden "
"State Up To The Mixed Projection Floor",
"Accepted Reduced Geometries Remain Valid Across Prepared Stellar Physics Quadrature Rules",
tags::reduced_stellar_geometry &tags::prepared &tags::geometry &tags::self_consistency
) {
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 auto stellarModel = stellar_equilibrium_test_utils::make_stellar_model(barotrope, 1.15);
auto parameterGeometry = stellarModel.compileDomainDeformation(f);
const auto &surface = parameterGeometry.surfaceDeformationPrescription();
mean_field::operators::PreparedStellarEquilibriumOperator stellarOperator(
f, *f.domainMapperStateless, stellarModel
);
const auto &layout = stellarOperator.GetLayout();
mfem::Vector state = stellar_equilibrium_test_utils::make_state(f, layout);
auto dependencies = stellar_equilibrium_test_utils::make_dependencies();
mfem::Vector uniformExpansion(surface.parameterCount());
mfem::Vector uniformContraction(surface.parameterCount());
mfem::Vector oblateSurface(surface.parameterCount());
uniformExpansion = 1.0e-2;
uniformContraction = -1.0e-2;
for (int parameter = 0; parameter < surface.parameterCount(); ++parameter) {
const double polarDirection = surface.radialDirection(parameter, 2);
oblateSurface(parameter) =
1.0e-2 * surface.referenceRadius(parameter) * (1.0 - 3.0 * polarDirection * polarDirection);
}
const std::array<const mfem::Vector *, 3> shapes{
&uniformExpansion,
&uniformContraction,
&oblateSurface,
};
for (int shape = 0; shape < static_cast<int>(shapes.size()); ++shape) {
stellar_equilibrium_test_utils::assign_value_block(
state, layout, stellar_equilibrium_test_utils::displacementValue, *shapes[shape]
);
if (shape > 0) {
++dependencies.surfaceDeformation.revision;
}
const auto report =
stellarOperator.Prepare(state, dependencies, stellar_equilibrium_test_utils::make_zero_rotation());
CAPTURE(shape);
CHECK(report.generatedVolumeDisplacement);
CHECK(report.generatedGeometry.isOrientationPreserving());
CHECK(std::isfinite(report.generatedGeometry.minimumJacobianDeterminant));
const auto independentGeometry = stellarOperator.GetDomainDeformation().inspectMappedGeometry(
stellarOperator.GetGeneratedVolumeDisplacement()
);
CHECK(independentGeometry.isOrientationPreserving());
CHECK(
std::abs(
independentGeometry.minimumJacobianDeterminant - report.generatedGeometry.minimumJacobianDeterminant
) <= 64.0 * std::numeric_limits<double>::epsilon() *
std::max(1.0, std::abs(report.generatedGeometry.minimumJacobianDeterminant))
);
mfem::Vector residual;
stellarOperator.BuildResidual(residual);
for (int entry = 0; entry < residual.Size(); ++entry) {
REQUIRE(std::isfinite(residual(entry)));
}
}
}
TEST_CASE(
"Reduced Stellar Equilibrium Preserves The Analytic N1 Floor Virtual Work And Rotational Shape Descent",
tags::barotrope &tags::prepared &tags::analytic_comparison &tags::accuracy &tags::gravity &tags::hydro
&tags::residuals
) {
@@ -1910,6 +1989,159 @@ TEST_CASE(
CHECK(analyticMassError < 5.0e-5 * targetMass);
CHECK(analyticMassError < 0.10 * perturbedMassError);
/*
* Return to the analytic spherical state before testing the reduced
* mechanical dual and its rotational shape response.
*/
stellar_equilibrium_test_utils::increment_all_state_revisions(dependencies);
stellarOperator.Prepare(analyticState, dependencies, zeroRotation);
auto parameterGeometry = stellarModel.compileDomainDeformation(f);
const auto &surface = parameterGeometry.surfaceDeformationPrescription();
mfem::Vector oblateSurfaceDirection(surface.parameterCount());
for (int parameter = 0; parameter < surface.parameterCount(); ++parameter) {
const double polarDirection = surface.radialDirection(parameter, 2);
oblateSurfaceDirection(parameter) =
surface.referenceRadius(parameter) * (1.0 - 3.0 * polarDirection * polarDirection);
}
const mfem::Vector surfaceParameters = stellar_equilibrium_test_utils::const_value_view(
analyticState, layout, stellar_equilibrium_test_utils::displacementValue
);
mfem::Vector liftedOblateDirection(parameterGeometry.volumeDisplacementSize());
parameterGeometry.applyJacobian(surfaceParameters, oblateSurfaceDirection, liftedOblateDirection);
mfem::Vector nonrotatingResidual;
stellarOperator.BuildResidual(nonrotatingResidual);
const mfem::Vector nonrotatingShapeResidual = stellar_equilibrium_test_utils::const_residual_view(
nonrotatingResidual, layout, stellar_equilibrium_test_utils::displacementResidual
);
const double reducedVirtualWork =
gravity_prepared_test_utils::global_dot(oblateSurfaceDirection, nonrotatingShapeResidual, f.mesh->GetComm());
const double volumeVirtualWork = gravity_prepared_test_utils::global_dot(
liftedOblateDirection, stellarOperator.GetFullMechanicalResidual(), f.mesh->GetComm()
);
const double virtualWorkScale = std::max({1.0, std::abs(reducedVirtualWork), std::abs(volumeVirtualWork)});
INFO("Reduced mechanical virtual work = " << reducedVirtualWork);
INFO("Lifted volume mechanical virtual work = " << volumeVirtualWork);
CHECK(std::abs(reducedVirtualWork - volumeVirtualWork) <= 2.0e-12 * virtualWorkScale);
const double keplerianAngularSpeed =
std::sqrt(mean_field::utils::G * targetMass / (stellarRadius * stellarRadius * stellarRadius));
const double angularSpeed = 0.25 * keplerianAngularSpeed;
mfem::Vector angularVelocity(3);
angularVelocity = 0.0;
angularVelocity(2) = angularSpeed;
mfem::Vector rotationCenter(3);
rotationCenter = 0.0;
const mean_field::physics::RigidRotation rotation(angularVelocity, rotationCenter);
++dependencies.rotation.revision;
stellarOperator.Prepare(analyticState, dependencies, rotation);
mfem::Vector rotatingSphericalResidual;
stellarOperator.BuildResidual(rotatingSphericalResidual);
const mfem::Vector rotatingShapeResidual = stellar_equilibrium_test_utils::const_residual_view(
rotatingSphericalResidual, layout, stellar_equilibrium_test_utils::displacementResidual
);
mfem::Vector rotationInducedShapeResidual(rotatingShapeResidual);
rotationInducedShapeResidual -= nonrotatingShapeResidual;
const double rotationInducedWork = gravity_prepared_test_utils::global_dot(
rotationInducedShapeResidual, oblateSurfaceDirection, f.mesh->GetComm()
);
const double rotationInducedNorm =
stellar_equilibrium_test_utils::global_norm(rotationInducedShapeResidual, f.mesh->GetComm());
const double oblateDirectionNorm =
stellar_equilibrium_test_utils::global_norm(oblateSurfaceDirection, f.mesh->GetComm());
const double rotationWorkScale = rotationInducedNorm * oblateDirectionNorm;
INFO("Rotation-induced reduced shape residual norm = " << rotationInducedNorm);
INFO("Rotation-induced work against the oblate surface direction = " << rotationInducedWork);
REQUIRE(rotationInducedNorm > 0.0);
REQUIRE(oblateDirectionNorm > 0.0);
REQUIRE(rotationWorkScale > 0.0);
CHECK(rotationInducedWork < -1.0e-3 * rotationWorkScale);
mfem::Vector oblateDirection(layout.value_offsets().Last());
oblateDirection = 0.0;
stellar_equilibrium_test_utils::assign_value_block(
oblateDirection, layout, stellar_equilibrium_test_utils::displacementValue, oblateSurfaceDirection
);
const auto deformationStatisticsBefore = stellarOperator.GetDomainDeformation().actionStatistics();
mfem::Vector oblateJacobianAction;
stellarOperator.Mult(oblateDirection, oblateJacobianAction);
const auto deformationStatisticsAfter = stellarOperator.GetDomainDeformation().actionStatistics();
CHECK(
deformationStatisticsAfter.pullbackDerivativeApplications ==
deformationStatisticsBefore.pullbackDerivativeApplications + 1
);
const mfem::Vector oblateShapeJacobianAction = stellar_equilibrium_test_utils::const_residual_view(
oblateJacobianAction, layout, stellar_equilibrium_test_utils::displacementResidual
);
/*
* Test descent for the rotation-induced departure from the nonrotating
* state. The analytic finite-element state has a nonzero projection floor,
* so minimizing the absolute residual would mix that unrelated floor into
* the rotational response. The sign of the best correction along this
* single trial coordinate is deliberately not prescribed: a shape-only
* correction holds the thermodynamic and gravity unknowns fixed, whereas
* the physical oblate equilibrium is a coupled response of every block.
*/
const double residualDirectionalDerivative = gravity_prepared_test_utils::global_dot(
rotationInducedShapeResidual, oblateShapeJacobianAction, f.mesh->GetComm()
);
const double jacobianDirectionNormSquared = gravity_prepared_test_utils::global_dot(
oblateShapeJacobianAction, oblateShapeJacobianAction, f.mesh->GetComm()
);
REQUIRE(std::isfinite(residualDirectionalDerivative));
REQUIRE(jacobianDirectionNormSquared > 0.0);
REQUIRE(std::abs(residualDirectionalDerivative) > 1.0e-12 * rotationWorkScale);
const double optimalLinearizedAmplitude = -residualDirectionalDerivative / jacobianDirectionNormSquared;
const double appliedShapeAmplitude =
std::copysign(std::min(0.25 * std::abs(optimalLinearizedAmplitude), 2.0e-2), optimalLinearizedAmplitude);
REQUIRE(appliedShapeAmplitude != 0.0);
mfem::Vector predictedShapeResidual(rotationInducedShapeResidual);
predictedShapeResidual.Add(appliedShapeAmplitude, oblateShapeJacobianAction);
const double predictedShapeNorm =
stellar_equilibrium_test_utils::global_norm(predictedShapeResidual, f.mesh->GetComm());
CHECK(predictedShapeNorm < rotationInducedNorm);
mfem::Vector correctedShapeState(analyticState);
stellar_equilibrium_test_utils::value_view(
correctedShapeState, layout, stellar_equilibrium_test_utils::displacementValue
)
.Add(appliedShapeAmplitude, oblateSurfaceDirection);
++dependencies.surfaceDeformation.revision;
stellarOperator.Prepare(correctedShapeState, dependencies, rotation);
mfem::Vector nonlinearCorrectedResidual;
stellarOperator.BuildResidual(nonlinearCorrectedResidual);
mfem::Vector nonlinearShapeDeparture(
stellar_equilibrium_test_utils::const_residual_view(
nonlinearCorrectedResidual, layout, stellar_equilibrium_test_utils::displacementResidual
)
);
nonlinearShapeDeparture -= nonrotatingShapeResidual;
const double nonlinearShapeDepartureNorm =
stellar_equilibrium_test_utils::global_norm(nonlinearShapeDeparture, f.mesh->GetComm());
INFO("Optimal linearized shape amplitude = " << optimalLinearizedAmplitude);
INFO("Applied shape amplitude = " << appliedShapeAmplitude);
INFO("Rotation-induced shape residual norm = " << rotationInducedNorm);
INFO("Predicted corrected rotational departure norm = " << predictedShapeNorm);
INFO("Nonlinear corrected rotational departure norm = " << nonlinearShapeDepartureNorm);
CHECK(nonlinearShapeDepartureNorm < rotationInducedNorm);
}
TEST_CASE(
@@ -2558,7 +2790,7 @@ TEST_CASE(
displacementBlock.Add(appliedOblateAmplitude, oblateDisplacement);
}
++dependencies.displacement.revision;
++dependencies.surfaceDeformation.revision;
stellarOperator.Prepare(oblateState, dependencies, rotation);

View File

@@ -988,7 +988,7 @@ TEST_CASE(
CHECK(gradient_projection_gap < 5.0e-3);
CHECK(potential_projection_gap < maximum_numerical_potential_error);
constexpr double virial_target = 1.0e-5;
constexpr double virial_target = 1.0e-6;
CHECK(binding_error < virial_target);
CHECK(virial_error < virial_target);

View File

@@ -191,7 +191,7 @@ namespace {
TEST_CASE(
"Constant Pressure Surface Prescribes Only A Pressure Quantity",
tags::surface_prescription_type_contract
tags::surface_condition_type_contract
) {
STATIC_CHECK(std::same_as<surface::ConstantPressureSurface::PhysicalQuantity, eos::quantity::Pressure>);
STATIC_CHECK(std::constructible_from<surface::ConstantPressureSurface, eos::PressureValue>);

View File

@@ -303,22 +303,68 @@ export namespace field_dof_test_utils {
} // namespace field_dof_test_utils
export namespace tags {
inline constexpr auto geometry = make_tag("geometry");
inline constexpr auto physics = make_tag("physics");
inline constexpr auto unit = make_tag("unit");
inline constexpr auto mesh = make_tag("mesh");
inline constexpr auto integration = make_tag("integration");
inline constexpr auto solver = make_tag("solver");
inline constexpr auto integrator = make_tag("integrator");
inline constexpr auto mapping = make_tag("mapping");
inline constexpr auto utils = make_tag("utils");
inline constexpr auto mfem_operators = make_tag("operators");
inline constexpr auto initialization = make_tag("initialization");
inline constexpr auto accuracy = make_tag("accuracy");
inline constexpr auto closure = make_tag("closure");
inline constexpr auto kernels = make_tag("kernels");
inline constexpr auto surface = make_tag("surface");
inline constexpr auto model = make_tag("model");
inline constexpr auto geometry = make_tag("geometry");
inline constexpr auto physics = make_tag("physics");
inline constexpr auto unit = make_tag("unit");
inline constexpr auto mesh = make_tag("mesh");
inline constexpr auto integration = make_tag("integration");
inline constexpr auto solver = make_tag("solver");
inline constexpr auto integrator = make_tag("integrator");
inline constexpr auto mapping = make_tag("mapping");
inline constexpr auto utils = make_tag("utils");
inline constexpr auto mfem_operators = make_tag("operators");
inline constexpr auto initialization = make_tag("initialization");
inline constexpr auto accuracy = make_tag("accuracy");
inline constexpr auto closure = make_tag("closure");
inline constexpr auto kernels = make_tag("kernels");
inline constexpr auto surface = make_tag("surface");
inline constexpr auto model = make_tag("model");
inline constexpr auto deformation = geometry & solver & make_tag("deformation");
inline constexpr auto deformation_type_contract = deformation & unit & make_tag("type_contract");
inline constexpr auto surface_deformation = deformation & surface & make_tag("surface_deformation");
inline constexpr auto interior_deformation_extension = deformation & make_tag("interior_extension");
inline constexpr auto vacuum_deformation_extension = deformation & make_tag("vacuum_extension");
inline constexpr auto deformation_pullback = deformation & make_tag("pullback");
inline constexpr auto surface_deformation_type_contract = deformation & surface & unit &
make_tag("surface_deformation") &
make_tag("type_contract") & make_tag("pullback");
inline constexpr auto interior_deformation_extension_type_contract =
deformation & unit & make_tag("interior_extension") & make_tag("type_contract") & make_tag("pullback");
inline constexpr auto vacuum_deformation_extension_type_contract =
deformation & unit & make_tag("vacuum_extension") & make_tag("type_contract") & make_tag("pullback");
inline constexpr auto surface_deformation_dof =
surface & geometry & mesh & make_tag("surface_deformation") & make_tag("dof");
inline constexpr auto surface_deformation_dof_unit = surface_deformation_dof & unit;
inline constexpr auto surface_deformation_dof_topology =
surface_deformation_dof & integration & make_tag("topology");
inline constexpr auto surface_deformation_dof_schema = surface_deformation_dof & integration & make_tag("schema");
inline constexpr auto surface_deformation_dof_parallel =
surface_deformation_dof & integration & make_tag("parallel");
inline constexpr auto nodal_radial_surface =
surface & geometry & make_tag("surface_deformation") & make_tag("nodal_radial");
inline constexpr auto nodal_radial_surface_validation = nodal_radial_surface & unit & make_tag("validation");
inline constexpr auto nodal_radial_surface_analytic =
nodal_radial_surface & integration & accuracy & make_tag("analytic_comparison");
inline constexpr auto nodal_radial_surface_linearization =
nodal_radial_surface & integration & make_tag("jacobian") & make_tag("adjoint");
inline constexpr auto radial_deformation_extension = deformation & mapping & make_tag("radial_extension");
inline constexpr auto radial_deformation_extension_validation =
radial_deformation_extension & unit & make_tag("validation");
inline constexpr auto radial_deformation_extension_analytic =
radial_deformation_extension & integration & accuracy & make_tag("analytic_comparison");
inline constexpr auto radial_deformation_extension_linearization =
radial_deformation_extension & integration & make_tag("jacobian") & make_tag("adjoint");
inline constexpr auto radial_deformation_extension_mapping =
radial_deformation_extension & integration & make_tag("determinant");
inline constexpr auto domain_deformation = deformation & mapping & solver & make_tag("domain_deformation");
inline constexpr auto domain_deformation_type_contract = domain_deformation & unit & make_tag("type_contract");
inline constexpr auto domain_deformation_composition = domain_deformation & integration & make_tag("composition");
inline constexpr auto domain_deformation_linearization =
domain_deformation & integration & make_tag("jacobian") & make_tag("adjoint");
inline constexpr auto domain_deformation_geometry =
domain_deformation & integration & geometry & make_tag("determinant");
inline constexpr auto reduced_stellar_geometry =
domain_deformation & integration & physics & make_tag("reduced_coordinates");
inline constexpr auto field = sub_tag(mesh & physics, "field");
inline constexpr auto field_dof = field & make_tag("dof");
@@ -412,10 +458,13 @@ export namespace tags {
equation_of_state_consumer_contract & make_tag("pressure_force");
inline constexpr auto structure_seed_equation_of_state_contract =
equation_of_state_consumer_contract & make_tag("structure_seed");
inline constexpr auto stellar_model_type_contract = barotrope & model & unit & make_tag("type_contract");
inline constexpr auto stellar_model_runtime_view = barotrope & model & unit & make_tag("runtime_view");
inline constexpr auto surface_prescription_type_contract =
surface & physics & unit & make_tag("prescription") & make_tag("type_contract");
inline constexpr auto stellar_model_type_contract = barotrope & model & unit & make_tag("type_contract");
inline constexpr auto stellar_model_runtime_view = barotrope & model & unit & make_tag("runtime_view");
inline constexpr auto stellar_model_deformation_ownership = model & deformation & unit & make_tag("ownership");
inline constexpr auto stellar_model_deformation_compilation =
model & deformation & integration & make_tag("compilation");
inline constexpr auto surface_condition_type_contract =
surface & physics & unit & make_tag("condition") & make_tag("type_contract");
inline constexpr auto surface_constraint_compilation =
surface & physics & unit & make_tag("constraint_compilation");
inline constexpr auto surface_constraint_jacobian = surface_constraint_compilation & jacobian;

View File

@@ -526,6 +526,41 @@ TEST_CASE(
CHECK(true);
}
TEST_CASE(
"Reduced Stellar Equilibrium Form Encodes Surface Shape Coordinates And Couplings",
tags::reduced_stellar_geometry &tags::unit &tags::utils
) {
using form = blocks::surface_deformed_stellar_equilibrium_form;
using jacobian = blocks::surface_deformed_stellar_equilibrium_jacobian_form;
constexpr auto surface_parameters =
blocks::get_value_block<form>(blocks::surface_deformation_field.parameters_term);
constexpr auto surface_shape =
blocks::get_residual_block<form>(blocks::surface_deformation_field.shape_equilibrium_term);
STATIC_REQUIRE(form::value_block_count == 6);
STATIC_REQUIRE(form::residual_block_count == 6);
STATIC_REQUIRE(static_cast<int>(surface_parameters) == 1);
STATIC_REQUIRE(static_cast<int>(surface_shape) == 3);
STATIC_REQUIRE((blocks::valid_jacobian_form<form, jacobian>));
STATIC_REQUIRE(
blocks::has_jacobian_coupling_v<
blocks::gravity::gradient::residual, blocks::surface_deformation::parameters::value, jacobian>
);
STATIC_REQUIRE(
blocks::has_jacobian_coupling_v<
blocks::surface_deformation::shape_equilibrium::residual, blocks::surface_deformation::parameters::value,
jacobian>
);
STATIC_REQUIRE_FALSE(blocks::contains_type_v<blocks::displacement::geometry::value, typename form::value_blocks>);
STATIC_REQUIRE_FALSE(
blocks::contains_type_v<blocks::displacement::geometry::residual, typename form::residual_blocks>
);
CHECK(true);
}
TEST_CASE(
"Block Validators Reject Structurally Malformed Forms",
tags::unit &tags::solver &tags::utils
@@ -623,4 +658,4 @@ TEST_CASE(
const std::array<int, form::value_block_count> invalid_value_sizes{11, 13, 17, 19, 23, 2};
CHECK_THROWS(blocks::form_layout<form>(invalid_value_sizes, residual_sizes));
}
}